Go 客户端
本节目标
读完本篇,你应能:
- 用
go get github.com/duckdb/duckdb-go/v2引入官方 Go 驱动,并理解 cgo 与预编译 DuckDB 库的关系 - 通过标准库
database/sql打开内存库 / 文件库,配置 DSN,正确Close - 用
ExecContext/QueryContext、占位符与事务完成 CRUD - 用 Appender 做大批量行插入;用
duckdb.Composite扫描 LIST / STRUCT / JSON - 注册简单的 标量 UDF,并按
duckdb.Error.Type分类处理错误 - 避开连接池、临时表、并发与 Windows/交叉编译等常见坑
若还不熟悉 DuckDB SQL 与导入导出,建议先读 SQL 基础 与 导入与导出。本篇 API 以 Go Client 官方文档 为准。
概念:为何选 Go 客户端
官方 Go 客户端 duckdb-go 是符合 Go database/sql 的 SQL 驱动。业务代码用与 PostgreSQL、SQLite 相同的 *sql.DB / Query / Scan 习惯即可嵌入 DuckDB。
在此之上,客户端还提供 DuckDB 特有能力:
| 能力 | 入口 | 典型场景 |
|---|---|---|
| Appender | duckdb.NewAppenderFromConn | Go 侧生成大量行时的高速写入 |
| Arrow(可选) | -tags=duckdb_arrow | 列式批次进出、与 Arrow 生态互操作 |
| UDF | RegisterScalarUDF / RegisterTableUDF | 在 SQL 中调用 Go 函数 |
| Profiling | GetProfilingInfo | 读取算子级耗时 |
模块路径:自
v2.5.0起仓库迁至github.com/duckdb/duckdb-go(旧路径github.com/marcboeker/go-duckdb)。新项目请用新路径;旧依赖按官方 Migration 指南改 import。
安装与环境
go get github.com/duckdb/duckdb-go/v2
DuckDB 核心为 C++,Go 客户端通过 cgo 链接。默认会静态链接预编译的 DuckDB 库(macOS / Linux 的 amd64、arm64,以及 Windows amd64),一般不必再单独安装 DuckDB CLI。
构建前确认本机有 C 工具链:
# Debian / Ubuntu 示例
sudo apt-get update && sudo apt-get install build-essential
# macOS:安装 Xcode Command Line Tools
xcode-select --install
Windows:需兼容的 gcc(常用 MSYS2 的 UCRT64 工具链),并把 gcc 加入 PATH 后再 go build。
版本提示:duckdb-go 的次版本号常编码所捆绑的 DuckDB 版本(例如 DuckDB 1.5.0 → v2.10500.x)。以 releases 与官方 Overview 为准。
注册驱动
仅使用 database/sql 时,用空白标识符导入以注册驱动名 duckdb:
import (
"database/sql"
_ "github.com/duckdb/duckdb-go/v2"
)
若要调用 Appender、UDF、duckdb.Error 等包内类型,改为具名导入:
import duckdb "github.com/duckdb/duckdb-go/v2"
连接:内存库、文件库与 DSN
package main
import (
"context"
"database/sql"
"log"
_ "github.com/duckdb/duckdb-go/v2"
)
func main() {
// 空 DSN 与 ":memory:" 等价:进程内内存库,退出即丢失
db, err := sql.Open("duckdb", "")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// sql.Open 惰性建连;Ping 可验证能否真正打开
if err := db.PingContext(context.Background()); err != nil {
log.Fatal(err)
}
// 持久化:路径不存在时会创建文件
// db, err = sql.Open("duckdb", "analytics.duckdb")
// 只读 + 线程数:以 URL 查询参数形式追加配置
// db, err = sql.Open("duckdb", "analytics.duckdb?access_mode=read_only&threads=4")
}
| DSN | 含义 |
|---|---|
"" 或 :memory: | 匿名内存库 |
/path/to/foo.db | 持久化库(可创建) |
file.db?access_mode=read_only&threads=4 | 文件库 + Configuration 选项 |
连接后也可用 SQL SET threads = 4 等调整会话配置(见 性能与运维)。
Connector:启动时跑引导 SQL
需要在「每个新连接」上执行引导语句,或为 Appender / Arrow 拿底层连接时,使用 duckdb.NewConnector + sql.OpenDB:
package main
import (
"context"
"database/sql"
"database/sql/driver"
"log"
duckdb "github.com/duckdb/duckdb-go/v2"
)
func main() {
connector, err := duckdb.NewConnector("app.duckdb?threads=4", func(execer driver.ExecerContext) error {
for _, q := range []string{
`SET schema = 'main'`,
`CREATE SCHEMA IF NOT EXISTS analytics`,
} {
if _, err := execer.ExecContext(context.Background(), q, nil); err != nil {
return err
}
}
return nil
})
if err != nil {
log.Fatal(err)
}
defer connector.Close()
db := sql.OpenDB(connector)
defer db.Close()
// 业务查询走 *sql.DB …
_ = db
}
无引导逻辑时第二个参数传 nil 即可。
关闭与资源
DuckDB 嵌在同一进程内:未 Close 会占内存;持久化库未关闭还可能让变更停在 WAL、未刷到主文件。务必对 *sql.DB、*sql.Conn、*sql.Rows、Connector、Appender 等实现了 Close 的对象使用 defer xxx.Close()。
临时表绑定在单条连接上。database/sql 可能把连接放回池而不是真正销毁,临时对象会「活」得比你想象的久。若必须在关闭连接时拆掉临时对象:
db.SetMaxIdleConns(0)
查询、占位符与扫描
下面是一个可直接运行的完整示例(建表、插入、参数查询、扫描):
package main
import (
"context"
"database/sql"
"log"
"time"
_ "github.com/duckdb/duckdb-go/v2"
)
type user struct {
name string
age int
height float32
awesome bool
bday time.Time
}
func main() {
ctx := context.Background()
db, err := sql.Open("duckdb", "")
if err != nil {
log.Fatal(err)
}
defer db.Close()
_, err = db.ExecContext(ctx, `
CREATE TABLE users (
name VARCHAR,
age INTEGER,
height FLOAT,
awesome BOOLEAN,
bday DATE
)`)
if err != nil {
log.Fatal(err)
}
_, err = db.ExecContext(ctx, `
INSERT INTO users VALUES
('marc', 99, 1.91, true, '1970-01-01'),
('macgyver', 70, 1.85, true, '1951-01-23')`)
if err != nil {
log.Fatal(err)
}
// 位置占位符 ? ;也可用 $1、$2
rows, err := db.QueryContext(ctx, `
SELECT name, age, height, awesome, bday
FROM users
WHERE (name = ? OR name = ?) AND age > ? AND awesome = ?`,
"macgyver", "marc", 30, true,
)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var u user
if err := rows.Scan(&u.name, &u.age, &u.height, &u.awesome, &u.bday); err != nil {
log.Fatal(err)
}
log.Printf("%s age=%d height=%.2f bday=%s awesome=%t",
u.name, u.age, u.height, u.bday.Format("2006-01-02"), u.awesome)
}
if err := rows.Err(); err != nil {
log.Fatal(err)
}
}
命名参数可用 sql.Named:
row := db.QueryRowContext(ctx,
`SELECT $age >= 18 AND $name = 'Alice'`,
sql.Named("age", 20),
sql.Named("name", "Alice"),
)
事务
tx, err := db.BeginTx(ctx, nil)
if err != nil {
log.Fatal(err)
}
if _, err := tx.ExecContext(ctx, `INSERT INTO users VALUES ('gru', 25, 1.35, false, '1996-04-03')`); err != nil {
_ = tx.Rollback()
log.Fatal(err)
}
if err := tx.Commit(); err != nil {
log.Fatal(err)
}
预处理语句
循环多次执行同一语句时,PrepareContext 可减少重复解析成本。大批量插入请改用 Appender,不要用预处理 INSERT 灌入海量行。
stmt, err := db.PrepareContext(ctx, `INSERT INTO users VALUES (?, ?, ?, ?, ?)`)
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
_, err = stmt.ExecContext(ctx, "Kevin", 11, 0.55, true, "2013-07-06")
if err != nil {
log.Fatal(err)
}
强制绑定类型
Go 的 time.Time 默认常绑定为 TIMESTAMP_TZ。若列是 TIMESTAMP_NS 等,用 duckdb.Typed 钉死逻辑类型:
start := time.Date(2024, time.April, 5, 0, 0, 0, 0, time.UTC)
row := db.QueryRow(`
SELECT count(*)
FROM (VALUES (TIMESTAMP_NS '2024-04-05 12:00:00.000000001')) events(ts)
WHERE ts >= ?`,
duckdb.Typed(start, duckdb.TYPE_TIMESTAMP_NS),
)
从文件读数据(与 SQL 一致)
Go 里同样可以直接跑 read_csv / read_parquet(路径按你本机准备):
rows, err := db.QueryContext(ctx, `
SELECT *
FROM read_csv('data/flights.csv', header = true)
LIMIT 10`)
把结果物化进库:
_, err = db.ExecContext(ctx, `
CREATE TABLE flights AS
SELECT * FROM read_parquet('data/flights.parquet')`)
更多格式选项见 导入与导出。
Appender:高速批量写入
Appender 面向「在 Go 里生成大量行」的场景,比逐条 INSERT 快得多。它工作在原始连接上,需通过 Connector 获取:
package main
import (
"context"
"database/sql"
"log"
duckdb "github.com/duckdb/duckdb-go/v2"
)
func main() {
c, err := duckdb.NewConnector("", nil)
if err != nil {
log.Fatal(err)
}
defer c.Close()
con, err := c.Connect(context.Background())
if err != nil {
log.Fatal(err)
}
defer con.Close()
db := sql.OpenDB(c)
defer db.Close()
if _, err := db.Exec(`CREATE TABLE users (name VARCHAR, age INTEGER)`); err != nil {
log.Fatal(err)
}
a, err := duckdb.NewAppenderFromConn(con, "", "users") // 空 schema = 默认
if err != nil {
log.Fatal(err)
}
// 参数顺序 = 表列顺序;INTEGER 列用 int32
if err := a.AppendRow("Fred", int32(34)); err != nil {
log.Fatal(err)
}
if err := a.AppendRow("Wilma", int32(31)); err != nil {
log.Fatal(err)
}
// Close 会做最后一次 Flush;必须检查错误
if err := a.Close(); err != nil {
log.Fatal(err)
}
var name string
var age int
if err := db.QueryRow(`SELECT name, age FROM users WHERE name = 'Fred'`).Scan(&name, &age); err != nil {
log.Fatal(err)
}
log.Printf("%s %d", name, age)
}
注意:Appender 缓冲行,约束错误(如 NOT NULL)可能在 Flush / Close 时才出现,而不是在某次 AppendRow 当场返回。依赖数据落盘前请显式 Flush() 并检查错误。
嵌套类型与 JSON:duckdb.Composite
LIST / STRUCT / MAP / JSON 等用泛型包装类型扫描:
package main
import (
"database/sql"
"log"
duckdb "github.com/duckdb/duckdb-go/v2"
)
func main() {
db, err := sql.Open("duckdb", "")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// JSON / LIST → []any
var arr duckdb.Composite[[]any]
if err := db.QueryRow(`SELECT json_array('foo', 'bar')`).Scan(&arr); err != nil {
log.Fatal(err)
}
log.Printf("first=%v", arr.Get()[0])
// JSON 对象 / STRUCT → map
var obj duckdb.Composite[map[string]any]
if err := db.QueryRow(`SELECT '{"family": "anatidae", "coolness": 42.42}'::JSON`).Scan(&obj); err != nil {
log.Fatal(err)
}
log.Printf("family=%v", obj.Get()["family"])
}
v2 起:JSON 不能再直接
Scan进string/[]byte;需要字符串时在 SQL 里::VARCHAR,或继续用Composite。
标量 UDF:在 SQL 里调 Go
在检出的 *sql.Conn 上注册。下面实现 my_length(VARCHAR) → INTEGER:
package main
import (
"context"
"database/sql"
"database/sql/driver"
"log"
duckdb "github.com/duckdb/duckdb-go/v2"
)
type varcharLen struct{}
func (*varcharLen) Config() duckdb.ScalarFuncConfig {
in, err := duckdb.NewTypeInfo(duckdb.TYPE_VARCHAR)
if err != nil {
panic(err)
}
out, err := duckdb.NewTypeInfo(duckdb.TYPE_INTEGER)
if err != nil {
panic(err)
}
return duckdb.ScalarFuncConfig{
InputTypeInfos: []duckdb.TypeInfo{in},
ResultTypeInfo: out,
}
}
func (*varcharLen) Executor() duckdb.ScalarFuncExecutor {
return duckdb.ScalarFuncExecutor{
RowExecutor: func(values []driver.Value) (any, error) {
return int32(len(values[0].(string))), nil
},
}
}
func main() {
db, err := sql.Open("duckdb", "")
if err != nil {
log.Fatal(err)
}
defer db.Close()
conn, err := db.Conn(context.Background())
if err != nil {
log.Fatal(err)
}
defer conn.Close()
if err := duckdb.RegisterScalarUDF(conn, "my_length", &varcharLen{}); err != nil {
log.Fatal(err)
}
var n int32
if err := db.QueryRow(`SELECT my_length('hello world')`).Scan(&n); err != nil {
log.Fatal(err)
}
log.Printf("length=%d", n) // 11
}
多路重载可用 RegisterScalarUDFSet;表函数见官方 Write User Defined Functions 与仓库 examples/table_udf。
错误分类
DuckDB 报错类型为 *duckdb.Error,用 errors.As 按类别处理,避免解析英文消息:
_, err := db.Exec(`SELECT * FROM no_such_table`)
if err != nil {
var de *duckdb.Error
if errors.As(err, &de) {
switch de.Type {
case duckdb.ErrorTypeCatalog:
log.Printf("catalog: %s", de.Msg)
case duckdb.ErrorTypeConstraint:
log.Printf("constraint: %s", de.Msg)
default:
log.Printf("duckdb %v: %s", de.Type, de.Msg)
}
}
}
其它常见类型还包括 ErrorTypeBinder、ErrorTypeParser、ErrorTypeIO、ErrorTypeInvalidInput 等。
Arrow(选修)
Arrow 接口依赖较重,默认不编译进去。需要时:
go build -tags=duckdb_arrow
然后通过 duckdb.NewArrowFromConn 按 RecordBatch 流式读结果,或把外部 Arrow RecordReader 注册成可查询视图。Arrow 句柄非并发安全,且不享受 database/sql 连接池;一连接一句柄,勿跨 goroutine 共享。细节见 Handle Results。
性能剖析(简介)
Profiling 绑在单连接上:
conn, err := db.Conn(ctx)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
_, _ = conn.ExecContext(ctx, `PRAGMA enable_profiling = 'no_output'`)
_, _ = conn.ExecContext(ctx, `PRAGMA profiling_mode = 'detailed'`)
res, err := conn.QueryContext(ctx, `SELECT 42`)
if err != nil {
log.Fatal(err)
}
res.Close()
info, err := duckdb.GetProfilingInfo(conn)
if err != nil {
log.Fatal(err)
}
log.Printf("latency=%v", info.Metrics["LATENCY"])
与 SQL 侧 EXPLAIN ANALYZE 互补,见 性能与运维。
并发模型(简表)
| 对象 | 多 goroutine 安全? |
|---|---|
*sql.DB | 是(连接池) |
*sql.Conn / Appender / Arrow | 否 |
| DuckDB 内部 worker 线程 | 由 threads 配置,跨连接共享 |
常见坑
undefined: conn/ cgo 未启用:缺 C 编译器,或交叉编译时 Go 自动关了 cgo。安装工具链,或CGO_ENABLED=1 CC=<交叉编译器> go build。- 忘记
Close:内存涨、WAL 未刷;长驻服务务必及时关闭Rows/Conn/DB。 - 用 Prepared INSERT 灌百万行:改用 Appender;官方明确不建议用预处理语句做大规模导入。
- 临时表「消失」或「还在」:与连接池复用有关;需要确定语义时设
SetMaxIdleConns(0)或避免依赖临时对象跨调用。 - JSON 扫描到
string失败:v2 行为变更,改用Composite或 SQL 转型。 - 旧 import 路径:
marcboeker/go-duckdb→duckdb/duckdb-go;模块 major 为/v2。 - 类型不匹配:Appender 的
INTEGER传int32;时间戳注意Typed。
延伸阅读
- Go Client Overview · Connect · Run Queries
- Import Data(Appender) · Handle Results(Arrow)
- UDF · Profile · Troubleshoot
- 仓库示例:duckdb-go/examples(
simple、appender、json、scalar_udf) - 系列:Python 客户端(对照另一语言绑定)· 性能与运维