Go 的数据库访问一直在两个极端之间摇摆:database/sql 太原始(手写 SQL + 手动 Scan),GORM 太魔法(运行时反射、隐式行为多)。sqlc 走了一条不同的路:你写 SQL,它生成类型安全的 Go 代码。SQL 优先,编译时校验,零反射。
sqlc vs 其他方案
| 维度 | database/sql | sqlx | GORM | sqlc |
|---|---|---|---|---|
| SQL 控制力 | 完全 | 完全 | 弱 | 完全 |
| 类型安全 | 无 | 部分 | 部分 | 完全 |
| 代码生成 | 无 | 无 | 无 | 有 |
| 学习成本 | 低 | 低 | 中 | 低 |
| 运行时反射 | 多 | 少 | 多 | 无 |
| 编译时校验 | 无 | 无 | 无 | 有 |
我用 GORM 遇到过的坑:Where 条件拼错字段名运行时才报错、Preload 嵌套查询生成的 SQL 效率差、复杂查询最后还是得写原生 SQL。sqlx 好一些,但字段映射还是靠 struct tag,拼错了编译不报错。
sqlc 的思路是反过来:SQL 是一等公民,Go 代码是生成的。你写的 SQL 在生成阶段就被解析和校验,字段名写错直接报错。
安装与项目配置
# 安装 sqlc
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
# 或用 brew
brew install sqlc
项目结构:
myproject/
├── sqlc.yaml # sqlc 配置
├── sql/
│ ├── schema.sql # 表结构
│ └── query.sql # 查询定义
├── db/ # 生成的 Go 代码(自动)
│ ├── db.go
│ ├── models.go
│ └── query.sql.go
├── main.go
└── go.mod
sqlc.yaml 配置
version: "2"
sql:
- engine: "postgresql" # 支持 postgresql / mysql / sqlite
queries: "sql/query.sql"
schema: "sql/schema.sql"
gen:
go:
package: "db"
out: "db"
sql_package: "pgx/v5" # 使用 pgx 驱动
emit_json_tags: true # 生成 JSON tag
emit_empty_slices: true # 空结果返回 [] 而非 nil
关键配置项:
engine:数据库类型,决定了 SQL 语法校验规则sql_package:底层驱动,PostgreSQL 推荐pgx/v5,MySQL 用database/sqlemit_json_tags:生成的 struct 自带jsontag,方便直接序列化为 API 响应
Schema 定义
-- sql/schema.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
avatar_url TEXT,
role VARCHAR(20) NOT NULL DEFAULT 'user',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE posts (
id BIGSERIAL PRIMARY KEY,
author_id BIGINT NOT NULL REFERENCES users(id),
title VARCHAR(200) NOT NULL,
content TEXT NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'draft',
view_count INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_posts_author_id ON posts(author_id);
CREATE INDEX idx_posts_status ON posts(status);
查询定义
sqlc 用注释中的特殊标记来指定查询类型:
:one— 返回一行:many— 返回多行:exec— 不返回数据(INSERT/UPDATE/DELETE):execresult— 返回受影响行数:execrows— 返回受影响行数(整数)
-- sql/query.sql
-- name: GetUser :one
SELECT id, username, email, avatar_url, role, created_at, updated_at
FROM users
WHERE id = $1;
-- name: GetUserByUsername :one
SELECT id, username, email, avatar_url, role, created_at, updated_at
FROM users
WHERE username = $1;
-- name: ListUsers :many
SELECT id, username, email, role, created_at
FROM users
ORDER BY created_at DESC
LIMIT $1 OFFSET $2;
-- name: CreateUser :one
INSERT INTO users (username, email, password, role)
VALUES ($1, $2, $3, $4)
RETURNING id, username, email, role, created_at;
-- name: UpdateUser :exec
UPDATE users
SET username = $2, email = $3, avatar_url = $4, updated_at = NOW()
WHERE id = $1;
-- name: DeleteUser :exec
DELETE FROM users WHERE id = $1;
-- name: GetPost :one
SELECT p.id, p.title, p.content, p.status, p.view_count,
p.created_at, p.updated_at,
u.username as author_name
FROM posts p
JOIN users u ON u.id = p.author_id
WHERE p.id = $1;
-- name: ListPostsByAuthor :many
SELECT id, title, status, view_count, created_at
FROM posts
WHERE author_id = $1
ORDER BY created_at DESC
LIMIT $2 OFFSET $3;
-- name: CreatePost :one
INSERT INTO posts (author_id, title, content, status)
VALUES ($1, $2, $3, $4)
RETURNING id, title, status, created_at;
-- name: UpdatePostStatus :exec
UPDATE posts SET status = $2, updated_at = NOW()
WHERE id = $1;
-- name: IncrementViewCount :exec
UPDATE posts SET view_count = view_count + 1
WHERE id = $1;
-- name: CountPostsByStatus :many
SELECT status, COUNT(*) as count
FROM posts
WHERE author_id = $1
GROUP BY status;
生成代码
sqlc generate
看看生成了什么:
// db/models.go — 自动生成的 struct
type User struct {
ID int64 `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"password"`
AvatarUrl *string `json:"avatar_url"`
Role string `json:"role"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type Post struct {
ID int64 `json:"id"`
AuthorID int64 `json:"author_id"`
Title string `json:"title"`
Content string `json:"content"`
Status string `json:"status"`
ViewCount int32 `json:"view_count"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
// db/query.sql.go — 自动生成的查询方法(部分)
// GetUser 的参数和返回值都是强类型
func (q *Queries) GetUser(ctx context.Context, id int64) (User, error) {
// ...
}
// CreateUser 的参数是生成的 struct
type CreateUserParams struct {
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"password"`
Role string `json:"role"`
}
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateUserRow, error) {
// ...
}
// JOIN 查询的返回值也是单独生成的 struct
type GetPostRow struct {
ID int64 `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
Status string `json:"status"`
ViewCount int32 `json:"view_count"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
AuthorName string `json:"author_name"` // JOIN 的字段
}
注意 GetPostRow:因为 GetPost 查询 JOIN 了 users 表并取了 author_name,sqlc 自动生成了一个包含这个字段的专用 struct。字段类型、可空性(*string)都是从 schema 推导的。
在项目中使用
// main.go
package main
import (
"context"
"fmt"
"log"
"myproject/db"
"github.com/jackc/pgx/v5/pgxpool"
)
func main() {
ctx := context.Background()
pool, err := pgxpool.New(ctx, "postgres://user:pass@localhost:5432/mydb")
if err != nil {
log.Fatal(err)
}
defer pool.Close()
queries := db.New(pool)
// 创建用户 — 参数和返回值都是强类型
user, err := queries.CreateUser(ctx, db.CreateUserParams{
Username: "zhangsan",
Email: "zhangsan@example.com",
Password: "$2a$10$hashed_password_here",
Role: "user",
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Created user: %d %s\n", user.ID, user.Username)
// 查询用户 — 返回强类型 struct,不需要手动 Scan
u, err := queries.GetUser(ctx, user.ID)
if err != nil {
log.Fatal(err)
}
fmt.Printf("User: %s (%s)\n", u.Username, u.Email)
// 创建文章
post, err := queries.CreatePost(ctx, db.CreatePostParams{
AuthorID: user.ID,
Title: "Hello sqlc",
Content: "This is my first post.",
Status: "published",
})
if err != nil {
log.Fatal(err)
}
// JOIN 查询 — 返回的 GetPostRow 包含 AuthorName 字段
p, err := queries.GetPost(ctx, post.ID)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Post: %s by %s\n", p.Title, p.AuthorName)
// 列表查询 — 分页
posts, err := queries.ListPostsByAuthor(ctx, db.ListPostsByAuthorParams{
AuthorID: user.ID,
Limit: 10,
Offset: 0,
})
if err != nil {
log.Fatal(err)
}
for _, p := range posts {
fmt.Printf(" - %s (%s)\n", p.Title, p.Status)
}
}
与现有项目集成
如果项目已经在用 GORM 或 sqlx,不需要全盘替换。sqlc 可以和它们共存:
- 新查询用 sqlc,旧代码保留:逐步迁移,不影响现有功能
- 复杂查询用 sqlc,简单 CRUD 用 GORM:取两者之长
- 共享数据库连接:sqlc 生成的
Queries可以用*sql.DB初始化,和 GORM 用同一个连接池
// sqlc 和 GORM 共存
import (
"database/sql"
"gorm.io/gorm"
"myproject/db"
)
sqlDB, _ := sql.Open("postgres", dsn)
gormDB, _ := gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}))
queries := db.New(sqlDB)
// 简单 CRUD 用 GORM
gormDB.Create(&SomeModel{...})
// 复杂查询用 sqlc
stats, _ := queries.CountPostsByStatus(ctx, userID)
sqlc 的局限
- 动态查询不方便:条件不固定的筛选查询(比如搜索页面,用户可能填也可能不填某个条件),sqlc 处理起来比较笨,每种组合可能需要单独写一个查询
- 数据库方言绑定:PostgreSQL 的 sqlc 支持最好,MySQL 支持在改善中但还有些 edge case
- schema 变更需要重新生成:改了表结构要跑一次
sqlc generate,需要集成到开发流程中 - 不支持 ORM 的关联加载:没有 Preload 这类自动加载关联的功能,JOIN 查询自己写
这些局限决定了 sqlc 不适合完全替代 ORM。但对于性能敏感、查询逻辑固定、需要精确控制 SQL 的场景,它是目前 Go 生态里最好的选择之一。