GoSuda

gosuda.org/sqlite3: Driver database/sql com CGO_ENABLED=0 sobre IPC de Memória Compartilhada

By Lemon Mint views ...

gosuda.org/sqlite3 é um driver database/sql para Go com CGO_ENABLED=0 que executa o amalgamation C oficial do SQLite 3.54.1 upstream dentro de um processo worker isolado, comunicando-se por meio de anéis de memória compartilhada HQQ. Ele se registra com o nome de driver padrão "sqlite3", oferece suporte aos parâmetros de consulta do DSN de github.com/mattn/go-sqlite3, incorpora binários de servidor pré-compilados com atestação SLSA para seis alvos de sistema operacional e arquitetura, e utiliza pipelining de requisições com pré-busca de lotes de 64 KiB para superar o cgo em processo em consultas de múltiplas linhas.

1go get gosuda.org/sqlite3@v0.3.3540100

Por que Mais um Driver SQLite para Go?

O SQLite é um dos softwares mais implantados do mundo, porém, usá-lo a partir de Go historicamente obrigou os engenheiros a uma escolha incômoda entre simplicidade de build e fidelidade do motor.

De um lado está o github.com/mattn/go-sqlite3, o padrão consolidado da comunidade. Ele envolve diretamente o amalgamation C oficial do SQLite por meio de cgo. Obtém-se o planejador de consultas C real, a semântica completa de concorrência do WAL e o gerenciamento de memória C previsível. O custo é CGO_ENABLED=1: builds locais e pipelines de CI tornam-se mais lentos, imagens de contêiner mínimas (scratch ou distroless) exigem bibliotecas de runtime C correspondentes, a compilação cruzada para outro sistema operacional ou arquitetura de CPU requer um cross-toolchain C completo, e qualquer corrupção de memória no código C compartilha o espaço de endereçamento do processo Go.

Do outro lado estão os ports puros em Go do SQLite, gerados pela tradução mecânica do amalgamation C para código-fonte Go sobre um runtime C emulado (libc). Eles restauram CGO_ENABLED=0, o que torna os builds triviais. Sob cargas de trabalho sustentadas, entretanto, o heap C traduzido e as alocações por linha em Go aumentam a pressão sobre o coletor de lixo, e casos sutis de bloqueio, timeouts de ocupação ou interrupção de statements podem divergir das builds C nativas.

Construímos o gosuda.org/sqlite3 para que aplicações Go não precisem escolher entre CGO_ENABLED=0 e o motor C real do SQLite. Ao compilar o amalgamation C oficial do SQLite 3.54.1 em um servidor dedicado de memória compartilhada (hqq-sqlite-server) e comunicar-se por meio de gosuda.org/hqq, seu binário Go compila com CGO_ENABLED=0 enquanto executa o SQLite em C não modificado e otimizado com -O3 através de uma fronteira de processo.

Execução Sem Configuração: Do go get ao sql.Open

Substituir o github.com/mattn/go-sqlite3 em uma base de código Go existente exige alterar apenas o caminho do import anônimo. O driver se registra como "sqlite3" (e como "hqq-sqlite" para seleção explícita) e aceita as mesmas strings de consulta do DSN:

 1package main
 2
 3import (
 4	"context"
 5	"database/sql"
 6	"fmt"
 7	"log"
 8
 9	_ "gosuda.org/sqlite3"
10)
11
12func main() {
13	dsn := "file:app.db?_journal_mode=WAL&_busy_timeout=5000&_txlock=immediate&_foreign_keys=on"
14	db, err := sql.Open("sqlite3", dsn)
15	if err != nil {
16		log.Fatal(err)
17	}
18	defer db.Close()
19
20	ctx := context.Background()
21	if _, err := db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)`); err != nil {
22		log.Fatal(err)
23	}
24	if _, err := db.ExecContext(ctx, `INSERT INTO users (name) VALUES (?)`, "Ada Lovelace"); err != nil {
25		log.Fatal(err)
26	}
27
28	var id int64
29	var name string
30	if err := db.QueryRowContext(ctx, `SELECT id, name FROM users WHERE name = ?`, "Ada Lovelace").Scan(&id, &name); err != nil {
31		log.Fatal(err)
32	}
33	fmt.Printf("user #%d: %s\n", id, name)
34}

Binários Worker Incorporados e Atestados por SLSA

Uma pergunta natural ao saber que uma biblioteca utiliza um processo worker em C é: "Preciso instalar ou distribuir um binário C separado junto ao meu programa Go?"

Não. O gosuda.org/sqlite3 incorpora binários hqq-sqlite-server pré-compilados para todos os seis alvos principais, diretamente em arquivos Go com tags de plataforma (internal/embedded/embed_<goos>_<goarch>.go). Como as restrições de build do Go compilam apenas o arquivo correspondente ao seu GOOS e GOARCH alvo, a compilação cruzada com GOOS=linux GOARCH=arm64 go build incorpora automaticamente apenas o binário de servidor linux/arm64, de aproximadamente 1,4 MiB.

Sistema Operacional AlvoArquitetura Alvo (GOARCH)Binário IncorporadoToolchain de Build e Proveniência
Linuxamd64, arm64hqq-sqlite-server_linux_<arch>GCC -O3, Proveniência SLSA Build L3
macOSarm64, amd64hqq-sqlite-server_darwin_<arch>AppleClang -O3, Proveniência SLSA Build L3
Windowsamd64hqq-sqlite-server_windows_amd64.exeMSVC -O2, Proveniência SLSA Build L3
Windowsarm64hqq-sqlite-server_windows_arm64.exezig cc -target aarch64-windows-gnu -O3, SLSA L3

Quando o sql.Open estabelece sua primeira conexão, o autolaunch.go materializa o binário incorporado no diretório de cache do usuário ($XDG_CACHE_HOME/hqq/bin/<sha256>/), verifica seu digest SHA-256 criptográfico contra o mapa fixado ReleaseHashes em hashes.go por meio de gosuda.org/hqq/secureexec, e inicia o worker com --parent-stdin, de modo que seu tempo de vida fique estritamente vinculado ao seu processo Go.

Compatibilidade de DSN com mattn/go-sqlite3 por Implementação Independente (Clean-Room)

Para funcionar como um substituto direto genuíno, o analisador de DSN (dsn.go) implementa uma réplica clean-room dos parâmetros de conexão e de pragmas utilizados em bases de código mattn/go-sqlite3:

Parâmetro de Consulta do DSNValores AceitosEfeito no SQLite
_busy_timeout / _timeoutInteiro em milissegundos (ex.: 5000)Chama sqlite3_busy_timeout(db, ms) na abertura da sessão.
_journal_mode / _journalDELETE, TRUNCATE, PERSIST, MEMORY, WAL, OFFExecuta PRAGMA journal_mode = <mode>.
_txlockimmediate, exclusive, deferredSeleciona BEGIN IMMEDIATE, BEGIN EXCLUSIVE ou BEGIN DEFERRED para db.BeginTx.
_foreign_keys / _fkon, true, 1, off, false, 0Executa PRAGMA foreign_keys = ON/OFF.
_synchronous / _sync0/OFF, 1/NORMAL, 2/FULL, 3/EXTRAExecuta PRAGMA synchronous = <level>.
_cache_sizeInteiro em páginas ou -KiB (ex.: -20000)Executa PRAGMA cache_size = <val>.
_locauto ou fuso horário IANA (ex.: UTC, Asia/Seoul)Converte colunas de texto DATETIME/TIMESTAMP em *time.Time no local de destino.
modero, rw, rwc, memoryDefine SQLITE_OPEN_READONLY, SQLITE_OPEN_READWRITE, SQLITE_OPEN_CREATE ou SQLITE_OPEN_MEMORY.
_case_sensitive_like, _query_only, _recursive_triggers, _secure_delete, _AutoVacuumBooleano ou inteiros de modoAplica o PRAGMA de sessão correspondente na inicialização da conexão.

Transformando o Isolamento de Processo em Vantagem de Vazão

À primeira vista, executar o SQLite em um processo separado parece necessariamente mais lento do que chamar sqlite3_step em processo via cgo. Na prática, ocorre o oposto assim que uma consulta retorna mais de uma única linha.

O Custo Oculto das Chamadas cgo Linha a Linha

Em um driver SQLite convencional baseado em cgo, o database/sql conduz a iteração uma linha e uma coluna por vez. Ler 1.000 linhas com 4 colunas dispara:

  • 1.000 chamadas cgo a sqlite3_step
  • 4.000 chamadas cgo a sqlite3_column_type e sqlite3_column_int64 / sqlite3_column_text
  • Milhares de travessias individuais de fronteira entre o escalonador do Go e a pilha C

Pipelining de Requisições e Pré-busca de Lotes de 64 KiB

O gosuda.org/sqlite3 elimina as travessias de fronteira por linha ao redesenhar o protocolo de comunicação em torno de dois mecanismos nos arquivos stmt.go, rows.go e sqlite3/server/sqlite3_hooks.c:

 1Cliente Go (stmt.go / rows.go)                      Worker C11 (sqlite3_hooks.c)
 2------------------------------                      ----------------------------
 31. Pipeline em 1 voo:
 4   ResetAsync + BindValuesAsync
 5   + StmtInfoAsync + FetchRowsAsync(128)  =======>  reset -> bind -> stmt_info ->
 6                                                    loop sqlite3_step() até 128 linhas
 7                                          <=======  Lote 1 (linhas 1..128 empacotadas em slot de 64 KiB)
 82. Disparar imediatamente a pré-busca:
 9   FetchRowsAsync(128) (Lote 2)           =======>  loop sqlite3_step() para as linhas 129..256
10   Enquanto o C avança o Lote 2, o chamador
11   Go lê as linhas 1..128 do Lote 1!      <=======  Lote 2 pronto na memória compartilhada
  1. Pipelining de Consulta em Voo Único (stmt.go): Quando QueryContext ou Stmt.QueryContext executa um statement somente leitura, s.query() submete reset, bind_values, stmt_info (na primeira execução) e fetch_rows de forma assíncrona em um único voo de memória compartilhada. Todas as quatro operações são executadas em sequência no worker C e retornam em uma única ida e volta.
  2. Laços Compactos de Lote em C e Pré-busca Assíncrona (rows.go): Dentro de fetch_rows (sqlite3_hooks.c), o worker C executa sqlite3_step(stmt) em um laço nativo em C, codificando até FetchSize linhas (padrão 128) diretamente no slot de resposta de 64 KiB da memória compartilhada, até que o limite do lote seja atingido, o slot esteja cheio ou ocorra SQLITE_DONE. Assim que o rows.ingest() decodifica o Lote 1, ele dispara FetchRowsAsync para o Lote 2 antes de devolver a primeira linha ao chamador. Enquanto seu código Go lê o Lote 1, o worker C já está avançando o Lote 2 em outro núcleo de CPU.

No Apple M4 (darwin/arm64), a comparação entre gosuda.org/sqlite3 (CGO_ENABLED=0) e github.com/mattn/go-sqlite3 (CGO_ENABLED=1) sobre o mesmo banco de dados SQLite mostra o efeito do agrupamento em lotes e do pipelining:

Carga de Trabalhogosuda.org/sqlite3 (CGO_ENABLED=0)mattn/go-sqlite3 (CGO_ENABLED=1)Velocidade Relativa
Seleção Pontual (QueryRow por PK)3,52 µs/op (9 B, 1 alocação)3,56 µs/op (232 B, 11 alocações)1,01x (paridade, 11x menos alocações)
Leitura de 100 Linhas (rows.Next)20,41 µs/op (30,5 KB, 211 alocações)36,34 µs/op (21,8 KB, 309 alocações)1,78x mais rápido
Leitura de 1.000 Linhas (rows.Next)125,92 µs/op (296 KB, 2.030 alocações)349,10 µs/op (216 KB, 3.009 alocações)2,77x mais rápido
Inserção Preparada (Autocommit)5,33 µs/op (0 B, 0 alocações)5,88 µs/op (120 B, 5 alocações)1,10x mais rápido (0 alocações)
Lote de Transação de 100 Linhas189,99 µs/op (16 B, 1 alocação)191,50 µs/op (12,1 KB, 503 alocações)1,01x (500x menos alocações)

Cancelamento, Isolamento de Caminhos e Recuperação do Pool de Conexões

Executar o SQLite em um worker isolado também fortalece a forma como o driver lida com timeouts, SQL não confiável e fronteiras do sistema de arquivos:

  • Cancelamento Determinístico de Contexto (conn.go e errors.go): Cada chamada do driver arma uma guarda leve context.AfterFunc (cn.guard). Se o context.Context do chamador for cancelado ou seu prazo expirar enquanto uma consulta recursiva lenta ou uma espera por bloqueio estiver em execução no servidor, a guarda marca a conexão como inválida, encerra o vínculo local de sessão e envia um quadro Cancel() fora de banda. O worker C invoca sqlite3_interrupt(db) imediatamente, desbloqueando sqlite3_step, enquanto o driver Go mapeia o status SQLITE_INTERRUPT resultante para context.Canceled ou context.DeadlineExceeded e instrui o database/sql a substituir a conexão no pool.
  • Sandbox de Diretórios (sqlsrv_pathrules.c): O worker C aplica as regras de caminho --allow-dir em toda chamada a open_v2, resolvendo links simbólicos e rejeitando travessias relativas .. para fora do diretório permitido. sqlite3_load_extension e SQLITE_LIMIT_ATTACHED são desabilitados por padrão, de modo que injeção de SQL não possa escapar do diretório do banco de dados nem carregar objetos compartilhados arbitrários.

Layout do Repositório

O repositório é estruturado de modo que o pacote de nível superior forneça o driver database/sql de substituição direta, enquanto launcher/ e sqlite3/ permanecem acessíveis para implantações avançadas com servidor personalizado:

 1gosuda.org/sqlite3
 2├── driver.go / api.go         # Registro do driver database/sql ("sqlite3") e API Connector
 3├── dsn.go                     # Analisador de DSN clean-room compatível com mattn/go-sqlite3 e configuração de pragmas
 4├── conn.go / tx.go            # Ciclo de vida da sessão, guarda de contexto e máquina de estados de transação
 5├── stmt.go / rows.go          # Pipelining de requisições em voo único e pré-busca de lotes de 64 KiB
 6├── errors.go                  # Códigos de erro primários/estendidos tipados do SQLite e correspondência de sentinelas
 7├── autolaunch.go / hashes.go  # Materialização zero-config do servidor incorporado e fixação SHA-256
 8├── internal/embedded/         # Binários //go:embed com tags de plataforma (linux, darwin, windows)
 9├── launcher/                  # Supervisor de processo configurável para implantações de servidor personalizado
10└── sqlite3/                   # Cliente Go hqq-bindgen gerado e código-fonte do servidor SQLite C11
11    └── server/                # Implementação do servidor C11 (sqlite3_hooks.c, sqlsrv_policy.c)

Primeiros Passos

Instale o módulo com go get e abra seu banco de dados com sql.Open("sqlite3", dsn):

1go get gosuda.org/sqlite3@v0.3.3540100