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 特有能力:

能力入口典型场景
Appenderduckdb.NewAppenderFromConnGo 侧生成大量行时的高速写入
Arrow(可选)-tags=duckdb_arrow列式批次进出、与 Arrow 生态互操作
UDFRegisterScalarUDF / RegisterTableUDF在 SQL 中调用 Go 函数
ProfilingGetProfilingInfo读取算子级耗时

模块路径:自 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 配置,跨连接共享

常见坑

  1. undefined: conn / cgo 未启用:缺 C 编译器,或交叉编译时 Go 自动关了 cgo。安装工具链,或 CGO_ENABLED=1 CC=<交叉编译器> go build。
  2. 忘记 Close:内存涨、WAL 未刷;长驻服务务必及时关闭 Rows / Conn / DB。
  3. 用 Prepared INSERT 灌百万行:改用 Appender;官方明确不建议用预处理语句做大规模导入。
  4. 临时表「消失」或「还在」:与连接池复用有关;需要确定语义时设 SetMaxIdleConns(0) 或避免依赖临时对象跨调用。
  5. JSON 扫描到 string 失败:v2 行为变更,改用 Composite 或 SQL 转型。
  6. 旧 import 路径:marcboeker/go-duckdb → duckdb/duckdb-go;模块 major 为 /v2。
  7. 类型不匹配:Appender 的 INTEGER 传 int32;时间戳注意 Typed。

延伸阅读


上一篇:性能与运维 · 系列首页:DuckDB 教程