- 开发工具
- 代码生成
- 数据库
【免费下载链接】sqlc
Generate type-safe code from SQL
sqlc 的核心能力是将 SQL 查询与数据库模式直接编译为类型安全的 Go 代码,而这其中最关键的一环,就是数据库内部类型到 Go 类型的映射。本文以官方文档 docs/reference/datatypes.md 为主体,结合源码实现,系统讲解 sqlc 对数组、时间、枚举、空值、UUID、JSON、TEXT、几何类型等复杂类型的默认映射规则,并给出通过overrides配置覆盖默认映射的完整实战方案。读完本文,你将能准确预测任意 SQL 列会生成什么样的 Go 类型,并能熟练地为特殊类型定制自己的映射。
总览:默认映射的决策逻辑
sqlc的默认映射遵循一个基本原则:对内部数据库类型到 Go 类型做出合理默认选择,复杂类型的选择规则在文档中逐一说明。如果你对默认结果不满意,随时可以通过sqlc配置文件中的 overrides 列表 覆盖任意类型。
从源码结构看,这套映射逻辑按数据库引擎拆分为三个独立实现,全部位于 internal/codegen/golang 目录下:
- postgresql_type.go:PostgreSQL 类型映射,入口为
postgresType()函数; - mysql_type.go:MySQL 类型映射,入口为
mysqlType()函数; - sqlite_type.go:SQLite 类型映射,入口为
sqliteType()函数。
每个入口函数都接收两个核心信息:col.NotNull(列是否非空)和col.IsArray(列是否为数组),并用notNull := col.NotNull || col.IsArray合并判断。数组中每个元素必然是"非空"的,因此数组列也会被当作非空列处理——这是理解后续所有映射规则的关键前提(见 postgresql_type.go)。
映射还会根据sql_package选项(即database/sql、pgx/v5、pgx/v4、lib/pq等驱动)产生不同分支。总体规律是:使用database/sql时,可空类型映射为sql.NullXXX;使用pgx/v5时,可空类型映射为pgtype.XXX。
Arrays:PostgreSQL 数组映射为 Go 切片
PostgreSQL 数组 类型会被 sqlc 物化为Go 切片(slice)。
以下面这张表为例:
CREATE TABLE places ( name text not null, tags text[] );sqlc 会生成如下结构体:
package db type Place struct { Name string Tags []string }即text[]数组列被映射为[]string。由于数组列在映射逻辑中被视为非空列(notNull := col.NotNull || col.IsArray),生成的切片字段不会套用sql.NullString之类的可空包装类型。仓库中的端到端测试用例(如 internal/endtoend/testdata/array_text、internal/endtoend/testdata/array_in)都验证了这一行为:PostgreSQL 的TEXT[]生成[]string、INT[]生成[]int32。
Dates and times:日期时间统一映射为 time.Time
所有日期和时间类型默认返回time.Time结构体。对于可空的时间或日期值,database/sql驱动下使用其NullTime类型;而使用pgx/v5时,则使用对应的 pgx 类型(如pgtype.Timestamp、pgtype.Timestamptz、pgtype.Date、pgtype.Time)。
MySQL 用户如果依赖github.com/go-sql-driver/mysql驱动,必须在数据库连接串中添加parseTime=true,否则时间值无法正确解析为time.Time。
示例表:
CREATE TABLE authors ( id SERIAL PRIMARY KEY, created_at timestamp NOT NULL DEFAULT NOW(), updated_at timestamp );生成结果:
package db import ( "database/sql" "time" ) type Author struct { ID int CreatedAt time.Time UpdatedAt sql.NullTime }从 postgresql_type.go 的源码可以看到这一映射的完整分支逻辑:以timestamp/timestamptz为例,pgx/v5驱动返回pgtype.Timestamp/pgtype.Timestamptz;非空列返回time.Time;可空列在emit_pointers_for_null_types开启时返回*time.Time,否则返回sql.NullTime。date、time、timetz类型的处理与此类似。MySQL 侧的 mysql_type.go 则将date、timestamp、datetime、time统一映射为非空time.Time/ 可空sql.NullTime。
Enums:PostgreSQL 枚举映射为别名 string 类型
PostgreSQL 枚举类型 会被 sqlc 映射为基于 string 的别名类型,并为每个枚举值生成类型化常量。
CREATE TYPE status AS ENUM ( 'open', 'closed' ); CREATE TABLE stores ( name text PRIMARY KEY, status status NOT NULL );生成结果:
package db type Status string const ( StatusOpen Status = "open" StatusClosed Status = "closed" ) type Store struct { Name string Status Status }从 postgresql_type.go 的默认分支可以看到枚举识别的实现细节:当列类型不在内置类型表中时,sqlc 会在目录(catalog)的所有 schema 中查找同名枚举;找到后,非空列返回StructName(enum.Name)(即枚举的 Go 名称,如Status),可空列返回"Null" + StructName(enum.Name)(即NullStatus)。枚举常量名的生成逻辑在 enum.go 中:先剔除非法字符,再把蛇形命名(snake_case)转成驼峰命名(camelCase),例如'open'生成常量名StatusOpen。
MySQL 的枚举(enum列类型)目前在 mysql_type.go 中仍统一映射为string(源码中标注了TODO: Proper Enum support);而 schema 中显式CREATE TYPE ... AS ENUM的 MySQL 枚举则会像 PostgreSQL 一样生成别名类型与常量。
Null:可空值使用 database/sql 或 pgx 的类型
对于结构体字段,null 值使用database/sql或pgx包中对应的类型来表示。
CREATE TABLE authors ( id SERIAL PRIMARY KEY, name text NOT NULL, bio text );生成结果:
package db import ( "database/sql" ) type Author struct { ID int Name string Bio sql.NullString }在database/sql驱动下,sqlc 的可空类型映射形成了完整的sql.NullXXX家族:sql.NullInt16/Int32/Int64、sql.NullFloat64、sql.NullBool、sql.NullString、sql.NullTime。例如 PostgreSQL 的smallint、integer、bigint可空列分别映射为sql.NullInt16、sql.NullInt32、sql.NullInt64(见 postgresql_type.go);MySQL 的tinyint可空列因为标准库没有sql.NullInt8,会退而使用最小的sql.NullInt16(见 mysql_type.go)。
如果你希望可空列直接映射为 Go 指针类型(如*string),可以使用emit_pointers_for_null_types选项,详见下文 TEXT 一节。
UUIDs:默认使用 github.com/google/uuid
默认情况下,sqlc 使用github.com/google/uuid包来处理 UUID 类型;使用pgx/v5时则使用pgtype.UUID。
CREATE TABLE records ( id uuid PRIMARY KEY );生成结果:
package db import ( "github.com/google/uuid" ) type Author struct { ID uuid.UUID }从 postgresql_type.go 可以看到完整的分支:pgx/v5驱动返回pgtype.UUID;非空列返回uuid.UUID;可空列在开启emit_pointers_for_null_types时返回*uuid.UUID,否则返回uuid.NullUUID。仓库的端到端测试 types_uuid 展示了database/sql驱动下的实际生成结果:可空列生成uuid.NullUUID,非空列生成uuid.UUID。
使用标准库 uuid 包
Go 1.27 在标准库中新增了uuid)。
标准库没有NullUUID类型,所以需要两条覆盖规则:一条用于非空列,一条用于可空列——可空列映射为*uuid.UUID指针。
version: "2" sql: - engine: "postgresql" schema: "schema.sql" queries: "query.sql" gen: go: package: "db" out: "db" sql_package: "pgx/v5" overrides: - db_type: "uuid" go_type: "uuid.UUID" - db_type: "uuid" nullable: true go_type: import: "uuid" type: "UUID" pointer: true为什么标准库的 uuid 需要两个 override?从 override.go 的匹配逻辑可以看出:db_type覆盖通过o.Nullable != notNull来区分可空与非空列,一条覆盖规则只会命中"可空"或"非空"中的一种情况,因此想让同一个 Go 类型同时覆盖可空与非空列,必须配置两条规则。
采用上述配置后,对于同时包含可空与非空uuid列的表:
CREATE TABLE records ( id uuid PRIMARY KEY, external_id uuid );会生成:
package db import ( "uuid" ) type Record struct { ID uuid.UUID ExternalID *uuid.UUID }这两条覆盖规则同时适用于pgx/v5和database/sql两种 sql package:pgx 支持任何底层类型为[16]byte的类型;从 Go 1.27 起,database/sql也能双向转换uuid.UUID——参数通过driver.DefaultParameterConverter绑定,结果可以扫描进uuid.UUID与*uuid.UUID目标,nil表示NULL。
MySQL 中的 UUID 处理
MySQL 没有原生的uuid数据类型。当使用UUID_TO_BIN存储UUID()时,底层字段类型是BINARY(16),默认会被 sqlc 映射为sql.NullString。要让 sqlc 自动把这些字段转换为uuid.UUID类型,需要对存储 uuid 的列使用 column 覆盖(详见 Overriding types):
{ "overrides": [ { "column": "*.uuid", "go_type": "github.com/google/uuid.UUID" } ] }注意这里的column字段支持*.uuid这样的通配符模式。从 override.go 的解析逻辑可以看到,column支持table.column、schema.table.column、catalog.schema.table.column三档形式,每一段都可以是通配符表达式。
JSON:默认 []byte,pgx/v5 可映射为结构体
默认情况下,sqlc 为 JSON 列生成[]byte、pgtype.JSON或json.RawMessage,具体取决于驱动:
pgx/v5:[]byte(json 与 jsonb 均是);pgx/v4:pgtype.JSON/pgtype.JSONB;lib/pq:非空列json.RawMessage,可空列pqtype.NullRawMessage。
对应实现见 postgresql_type.go。SQLite 侧(sqlite_type.go)的json/jsonb则固定映射为json.RawMessage。
但如果你使用pgx/v5sql package,可以通过 overrides 指定一个结构体替代默认类型(详见 Overriding types),pgx 实现会自动对结构体执行 marshal/unmarshal(序列化/反序列化)。例如定义 DTO 结构体:
package dto type BookData struct { Genres []string `json:"genres"` Title string `json:"title"` Published bool `json:"published"` }数据库表:
CREATE TABLE books ( data jsonb );配置覆盖规则:
{ "overrides": [ { "column": "books.data", "go_type": { "import":"example.com/db", "package": "dto", "type":"BookData", "pointer": true } } ] }生成结果:
package db import ( "example.com/db/dto" ) type Book struct { Data *dto.BookData }这里的go_type使用了映射(map)形式而非字符串形式。从 go_type.go 的解析逻辑可以看出映射各字段的作用:import指定包导入路径、package指定包名(当导入路径末尾与包名不一致时使用)、type指定类型名、pointer: true生成指针类型*dto.BookData。go_type也支持字符串形式(如"time.Time")与pointer/slice组合,字符串形式下要求是 Go 基本类型或package.type格式(见 go_type.go)。
TEXT:非空映射 string,可空映射 pgtype.Text
在 PostgreSQL 中,非空的TEXT列默认映射为Gostring;但使用pgx/v5驱动时,可空的TEXT列会被映射为pgtype.Text。这一区别对于在 Go 应用中正确处理空值至关重要。
对应的 postgresql_type.go 分支逻辑为:非空列返回string;可空列在开启emit_pointers_for_null_types时返回*string;pgx/v5返回pgtype.Text;否则返回sql.NullString。该分支同时覆盖text、varchar、bpchar、citext、name等字符类型。
如果你希望可空字符串映射为 Go 的*string指针,有两种方式:
- 在 sqlc 配置中使用
emit_pointers_for_null_types选项,让所有可空 SQL 列都以指针类型表示,清晰区分 null 与非 null 值:
version: "2" sql: - engine: "postgresql" schema: "schema.sql" queries: "query.sql" gen: go: package: "db" out: "db" sql_package: "pgx/v5" emit_pointers_for_null_types: true- 在覆盖
TEXT数据类型时传入pointer: true(详见 Overriding types):
overrides: - db_type: "text" nullable: true go_type: type: "string" pointer: true从 postgresql_type.go 可以看到,emit_pointers_for_null_types仅在pgx系列驱动下生效(emitPointersForNull := driver.IsPGX() && options.EmitPointersForNullTypes),且枚举类型还可通过emit_pointers_for_null_enum_types单独控制(未设置时继承前者)。该选项对应的字段定义见 options.go。
Geometry:PostGIS 几何类型接入方案
sqlc 支持为 PostGIS 几何类型配置第三方 Go 包,文档给出了两套方案,均需配合 Overriding types 使用。
方案一:使用github.com/twpayne/go-geos(仅限 pgx/v5)
配置 sqlc 使用*github.com/twpayne/go-geos.Geom处理几何类型,共三个步骤:
- 在配置中为 geometry 类型设置 override(详见 Overriding types);
- 在每个
*github.com/jackc/pgx/v5.Conn上调用github.com/twpayne/pgx-geos.Register; - 如有必要,在 SQL 中标注
::geometry类型转换(typecast)。
示例 SQL(英国国家网格 EPSG:27700 坐标系下的多面体):
-- Multipolygons in British National Grid (epsg:27700) create table shapes( id serial, name varchar, geom geometry(Multipolygon, 27700) ); -- name: GetCentroids :many SELECT id, name, ST_Centroid(geom)::geometry FROM shapes;配置:
{ "version": 2, "gen": { "go": { "overrides": [ { "db_type": "geometry", "go_type": { "import": "github.com/twpayne/go-geos", "package": "geos", "pointer": true, "type": "Geom" }, "nullable": true } ] } } }运行时注册:
import ( "github.com/twpayne/go-geos" pgxgeos "github.com/twpayne/pgx-geos" ) // ... config.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error { if err := pgxgeos.Register(ctx, conn, geos.NewContext()); err != nil { return err } return nil }方案二:使用github.com/twpayne/go-geom
同样通过 overrides 配置实现(详见 Overriding types):
-- Multipolygons in British National Grid (epsg:27700) create table shapes( id serial, name varchar, geom geometry(Multipolygon, 27700) ); -- name: GetShapes :many SELECT * FROM shapes;{ "version": "1", "packages": [ { "path": "db", "engine": "postgresql", "schema": "query.sql", "queries": "query.sql" } ], "overrides": [ { "db_type": "geometry", "go_type": "github.com/twpayne/go-geom.MultiPolygon" }, { "db_type": "geometry", "go_type": "github.com/twpayne/go-geom.MultiPolygon", "nullable": true } ] }注意此方案同样需要两条 override(非空一条、可空一条),这与前面 UUID 标准库覆盖的规则一致——db_type覆盖只能命中可空或非空列中的一种。这两套方案都依赖 overrides 机制,其完整字段说明(db_type、column、go_type、nullable、unsigned、go_struct_tag等)可查阅 Overriding types。
更多类型与进阶覆盖技巧
其他值得关注的默认映射
从 postgresql_type.go 的完整分支中,还可以看到文档未展开但值得了解的类型映射:
- 网络类型:
inet在 pgx/v5 下映射为netip.Addr(可空为*netip.Addr),cidr映射为netip.Prefix,macaddr/macaddr8映射为net.HardwareAddr(见 postgresql_type.go); - 数值类型:
numeric/money在 pgx 下为pgtype.Numeric,在database/sql下因标准库缺少 decimal 类型而映射为string/sql.NullString(见 postgresql_type.go); - 区间类型:
daterange、tsrange、int4range、numrange等在 pgx/v5 下映射为pgtype.Range[pgtype.XXX]泛型类型,多区间(multirange)映射为pgtype.Multirange[...](见 postgresql_type.go); - 二进制与系统类型:
bytea固定映射为[]byte;oid、cid、xid在 pgx/v5 下映射为pgtype.Uint32(见 postgresql_type.go); - 未知类型:所有引擎对无法识别的类型都会在
debug.Active时输出日志并最终回退为any(见 postgresql_type.go、mysql_type.go)。
overrides 的匹配与优先级规则
理解以下规则可以让你更精准地使用类型覆盖(全部来自 override.go 与 docs/howto/overrides.md):
column与db_type互斥:一条 override 必须且只能指定其中之一,同时指定会报错(见 override.go);nullable只对db_type生效,对column覆盖无效(见 docs/howto/overrides.md);column覆盖优先于db_type覆盖;db_type一条规则只覆盖可空或非空中的一种,如需两者统一需配置两条;- 全局覆盖:可以在配置顶层
overrides段配置跨包生效的覆盖,并可结合engine字段区分不同数据库引擎(见 docs/howto/overrides.md); - 从 options.go 的解析逻辑看,全局 overrides 会追加到各包的局部 overrides 之前,即局部配置优先级更高。
配置结构与解析入口
sqlc 的 Go 代码生成配置通过 internal/codegen/golang/gen.go 中的Generate()入口串联:先由 options.go 的Parse()解析插件选项(含 overrides 解析与校验),再由buildEnums、buildStructs、buildQueries构建枚举、结构体与查询,最后通过text/template模板渲染并经过go/format格式化输出(见 gen.go)。结构体名称的生成规则(蛇形转驼峰、首字母数字前加下划线、id等首字母缩写词大写)在 struct.go 中实现。
总结
sqlc 的数据类型映射设计围绕"合理的默认值 + 可覆盖"展开:
- 数组→ Go 切片(元素视为非空);
- 日期时间→
time.Time(可空时sql.NullTime或pgtype.XXX,MySQL 需parseTime=true); - 枚举→ 别名 string 类型 + 类型化常量;
- 可空值→
database/sql或pgx的 Null 类型; - UUID→
github.com/google/uuid(可空uuid.NullUUID),标准库uuid可通过两条 overrides 启用; - JSON→
[]byte/pgtype.JSON/json.RawMessage,pgx/v5 下可覆盖为自定义结构体; - TEXT→ 非空
string,pgx/v5 可空为pgtype.Text,可配置指针类型; - PostGIS 几何→ 通过 overrides 接入
go-geos或go-geom。
无论默认映射是否满足需求,overrides机制都提供了灵活且类型安全的补救手段。建议在调整映射前先阅读 config.md 了解完整配置项,并参考 internal/codegen/golang/postgresql_type.go、mysql_type.go、sqlite_type.go 三个源码文件确认你所用引擎的完整类型清单,同时可查看 internal/endtoend/testdata 下的端到端测试用例验证实际生成效果。
- 开发工具
- 代码生成
- 数据库
【免费下载链接】sqlc
Generate type-safe code from SQL
相关推荐
node-redis RESP 协议类型映射完全指南:RESP2 / RESP3 到 JavaScript 类型的默认规则与自定义配置
node redis RESP 协议类型映射完全指南:RESP2 / RESP3 到 JavaScript 类型的默认规则与自定义配置 导读 RESP(Redi
后端数据库客户端缓存告别数据类型混乱:DBeaver自定义类型映射规则完全指南
告别数据类型混乱:DBeaver自定义类型映射规则完全指南 你是否曾在使用DBeaver处理不同数据库时,遇到过数据类型显示异常的问题?比如MySQL的VARC
数据库客户端桌面应用数据库3分钟跑起开源思维导图:从空白画布到导出成图
3分钟跑起开源思维导图:从空白画布到导出成图 每次做复盘,都要用表格拼模块关系,分享时截图还总糊成一片。开源思维导图 SimpleMindMap 在本地画出结构
前端UI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考