Go 数据库操作
# Go 数据库操作
# 导入 driver
import (
"database/sql"
_ "github.com/mattn/go-sqlite3"
)
2
3
4
导入
database/sql
包导入数据库驱动
使用数据库时,除了
database/sql
包本身,还需要引入想使用的特定数据库驱动。我们这里使用sqlite
数据,所以导入了"github.com/mattn/go-sqlite3"
,但由于没有直接用到该包,所以我们使用_
别名来匿名导入驱动。导入时,驱动的初始化函数会调用sql.Register
将自己注册在database/sql
包的全局变量sql.drivers
中,以便以后通过sql.Open
访问。var driverName = "sqlite3" func init() { if driverName != "" { sql.Register(driverName, &SQLiteDriver{}) } }
1
2
3
4
5
6
7
# 初始化 DB
Open:使用
Open
函数初始化DB
:func Open(driverName, dataSourceName string) (*DB, error)
1参数:
driverName
:驱动的名字,例如mysql
、sqlite3
dataSourceName
:简单理解就是数据库链接信息
常见错误:忘记匿名引入
driver
包。OpenDB
:一般用于接入一些自定义的驱动,例如将分库分表做成一个驱动。func OpenDB(c driver.Connector) *DB
1
执行sql.Open()
并未实际建立起到数据库的连接,也不会验证驱动参数。第一个实际的连接会惰性求值,延迟到第一次需要时建立。用户应该通过db.Ping()
来检查数据库是否实际可用。
func main() {
db, err := NewDB()
if err != nil {
log.Fatalln(err)
}
// 记得 Close DB
defer func(db *sql.DB) {
err = db.Close()
if err != nil {
log.Fatalln(err)
}
}(db)
}
func NewDB() (*sql.DB, error) {
db, err := sql.Open("sqlite3", "file:test.db?cache=shared&mode=memory")
if err != nil {
return nil, err
}
// 验证数据库是否可用
if err = db.Ping(); err != nil {
return nil, err
}
fmt.Println("数据库连接成功")
return db, err
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 增删改查
# 增删改
Exec
或ExecContext
func (db *DB) ExecContext(ctx context.Context, query string, args ...any) (Result, error) func (db *DB) Exec(query string, args ...any) (Result, error) { return db.ExecContext(context.Background(), query, args...) }
1
2
3
4
5可以用
ExecContext
来控制超时ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) res, err := db.ExecContext(ctx, sql)
1
2同时检查
error
和sql.Result
注意:注意参数传递,一般的 SQL 都是使用?
作为参数占位符。不要把参数拼接进去 SQL 本身,容易引起注入。
增删改的使用:
创建一个表
func main() { db, err := NewDB() //... CreateTable(db) } func CreateTable(db *sql.DB) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) // 除了 SELECT 语句,都是使用 ExecContext _, err := db.ExecContext(ctx, ` CREATE TABLE IF NOT EXISTS user( id INTEGER PRIMARY KEY, first_name TEXT NOT NULL, age INTEGER, last_name TEXT NOT NULL ) `) if err != nil { log.Fatalf("建表失败: %v", err) } log.Println("建表成功") cancel() }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26执行结果:
2022/12/12 21:14:04 建表成功
1插入数据
func main() { db, err := NewDB() //... CreateTable(db) InsertValue(db) } func InsertValue(db *sql.DB) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) // 使用 ? 作为查询的参数的占位符,防止 SQL 注入 res, err := db.ExecContext(ctx, "INSERT INTO `user`(`id`, `first_name`, `age`, `last_name`) VALUES (?, ?, ?, ?)", 1, "Tom", 18, "Jerry") if err != nil { log.Fatalf("插入数据失败:%v", err) } affected, err := res.RowsAffected() if err != nil { log.Fatalf("获取 受影响行数 失败:%v", err) } log.Println("受影响行数", affected) lastId, err := res.LastInsertId() if err != nil { log.Fatalf("获取 最后插入的ID 失败:%v", err) } log.Println("最后插入的ID", lastId) cancel() }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28执行结果:
2022/12/12 21:14:04 受影响行数 1 2022/12/12 21:14:04 最后插入的ID 1
1
2删除数据
func main() { //... CreateTable(db) InsertValue(db) DeleteValue(db) } func DeleteValue(db *sql.DB) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) res, err := db.ExecContext(ctx, "DELETE FROM `user` WHERE `id` = ?", 1) if err != nil { log.Fatalf("删除数据失败:%v", err) } affected, err := res.RowsAffected() if err != nil { log.Fatalf("获取 受影响行数 失败:%v", err) } log.Println("受影响行数", affected) cancel() }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21执行结果:
2022/12/12 21:16:52 受影响行数 1
1修改数据
func main() { //... CreateTable(db) InsertValue(db) UpdateValue(db) } func UpdateValue(db *sql.DB) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) res, err := db.ExecContext(ctx, "UPDATE `user` SET first_name = ? WHERE `id` = ?", "Smith", 1) if err != nil { log.Fatalf("更新数据失败:%v", err) } affected, err := res.RowsAffected() if err != nil { log.Fatalf("获取 受影响行数 失败:%v", err) } log.Println("受影响行数", affected) cancel() }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22执行结果:
2022/12/12 23:01:38 受影响行数 1
1
# 查
QueryRow
和QueryRowContext
:查询单行数据。预期只有一行,如果没有数据就会报错,如果多于一行则只取第一行。func (db *DB) QueryRowContext(ctx context.Context, query string, args ...any) *Row func (db *DB) QueryRow(query string, args ...any) *Row { return db.QueryRowContext(context.Background(), query, args...) }
1
2
3
4
5Query
和QueryContext
:查询多行数据。没有数据不报错。func (db *DB) QueryContext(ctx context.Context, query string, args ...any) (*Rows, error) func (db *DB) Query(query string, args ...any) (*Rows, error) { return db.QueryContext(context.Background(), query, args...) }
1
2
3
4
5
Row 和 Rows
QueryRow
和QueryRowContext
返回*Row
Row
:可以理解为只有一行的Rows
,而且是必须要有一行。没有的话,在调用Row
的Scan
的时候会返回sql.ErrNoRow
。- 如果查询发生错误,错误会延迟到调用
Scan()
时统一返回,减少了一次错误处理判断。同时QueryRow
也避免了手动操作结果集的麻烦。 - 通过
row.Scan
获取结果集。
Query
和QueryContext
返回*Rows
和error
。Rows
:迭代器设计,需要在使用前调用Next
方法。error
:每个驱动返回的error
都不一样,用错误字符串来判断错误类型并不是明智的做法,更好的方法是对抽象的错误做Type Assertion
,利用驱动提供的更具体的信息来处理错误。
下面使用上面创建的user
表进行测试:
创建一个
User
结构体type User struct { ID int64 FirstName string Age int8 LastName *sql.NullString } func (u User) String() string { return fmt.Sprintf("ID: %d FirstName: %s Age: %d LastName: %s", u.ID, u.FirstName, u.Age, u.LastName.String) }
1
2
3
4
5
6
7
8
9
10
11使用
QueryRowContext
查询单行结果func main() { //... CreateTable(db) InsertValue(db) QueryRow(db) } func QueryRow(db *sql.DB) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*1) // 查询一行数据(预期只有一行) row := db.QueryRowContext(ctx, "SELECT `id`, `first_name`, `age`, `last_name` FROM `user` WHERE `id` = ?", 1) if row.Err() != nil { log.Fatalf("查询一行数据失败:%v", row.Err()) } u := User{} // 通过 Scan 方法从结果集中获取一行结果 // 查询不到,会在 Scan 时返回 sql.ErrNoRows err := row.Scan(&u.ID, &u.FirstName, &u.Age, &u.LastName) if err != nil { log.Fatalf("获取结果集失败:%v", err) } log.Println("结果:", u.String()) cancel() }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26执行结果:
2022/12/12 21:43:28 结果: ID: 1 FirstName: Tom Age: 18 LastName: Jerry
1尝试查询不存在的数据,将查询条件的
ID
改为 10row := db.QueryRowContext(ctx, "SELECT `id`, `first_name`, `age`, `last_name` FROM `user` WHERE `id` = ?", 10)
1
2执行结果:
2022/12/12 21:45:30 获取结果集失败:sql: no rows in result set
1尝试向不存在的表查询数据
row := db.QueryRowContext(ctx, "SELECT `id`, `first_name`, `age`, `last_name` FROM `user_not_exists` WHERE `id` = ?", 1)
1
2执行结果:
2022/12/12 21:47:04 查询一行数据失败:no such table: user_not_exists
1使用
QueryContext
进行批量查询func QueryRows(db *sql.DB) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*1) // 批量查询 rows, err := db.QueryContext(ctx, "SELECT `id`, `first_name`, `age`, `last_name` FROM `user` WHERE `id` = ?", 1) if err != nil { log.Fatalf("批量查询数据失败:%v", err) } users := make([]User, 0) for rows.Next() { // 标准迭代器设计 u := User{} // 这里没有数据不会返回 sql.ErrNoRows // Scan 支持传入的类型: // *string // *[]byte // *int, *int8, *int16, *int32, *int64 // *uint, *uint8, *uint16, *uint32, *uint64 // *bool // *float32, *float64 // *interface{} // *RawBytes // *Rows (cursor value) // any type implementing Scanner (see Scanner docs) if err = rows.Scan(&u.ID, &u.FirstName, &u.Age, &u.LastName); err != nil { log.Fatalf("获取结果集失败:%v", err) } users = append(users, u) log.Println("结果:", u.String()) } log.Println("最终结果:", users, "长度:", len(users)) cancel() }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32执行结果:
2022/12/12 22:00:05 结果: ID: 1 FirstName: Tom Age: 18 LastName: Jerry 2022/12/12 22:00:05 最终结果: [ID: 1 FirstName: Tom Age: 18 LastName: Jerry] 长度: 1
1
2查询结果集为空的数据
rows, err := db.QueryContext(ctx, "SELECT `id`, `first_name`, `age`, `last_name` FROM `user` WHERE `id` = ?", 10)
1
2执行结果:
2022/12/12 22:01:26 最终结果: [] 长度: 0
1
# 事务 API
Begin
和BeginTx
开始事务TxOptions
设置Isolation
字段。大多数时候都不需要设置这个,需要确认自己使用的数据库支持该级别,并且弄清楚效果。type TxOptions struct { // Isolation is the transaction isolation level. // If zero, the driver or database's default level is used. Isolation IsolationLevel ReadOnly bool }
1
2
3
4
5
6
func (db *DB) BeginTx(ctx context.Context, opts *TxOptions) (*Tx, error) func (db *DB) Begin() (*Tx, error) { return db.BeginTx(context.Background(), nil) }
1
2
3
4
5Commit
提交事务Rollback
回滚事务
已插入数据为例:
func main() {
//...
CreateTable(db)
Tx(db)
}
func Tx(db *sql.DB) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
// 开始一个事务
tx, err := db.BeginTx(ctx, &sql.TxOptions{})
if err != nil {
log.Fatalf("事务开始失败:%v", err)
}
// 使用 ? 作为查询的参数的占位符,防止 SQL 注入
res, err := tx.ExecContext(ctx, "INSERT INTO `user`(`id`, `first_name`, `age`, `last_name`) VALUES (?, ?, ?, ?)",
1, "Tom", 18, "Jerry")
if err != nil {
log.Println("事务中插入数据失败,开始回滚")
// 回滚
err = tx.Rollback()
if err != nil {
log.Printf("事务回滚失败:%v", err)
}
cancel()
return
}
affected, err := res.RowsAffected()
if err != nil {
log.Fatalf("获取 受影响行数 失败:%v", err)
}
log.Println("受影响行数", affected)
lastId, err := res.LastInsertId()
if err != nil {
log.Fatalf("获取 最后插入的ID 失败:%v", err)
}
log.Println("最后插入的ID", lastId)
// 提交事务
err = tx.Commit()
if err != nil {
log.Fatalf("提交事务失败:%v", err)
}
cancel()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
执行结果:
2022/12/12 22:14:18 受影响行数 1
2022/12/12 22:14:18 最后插入的ID 1
2
# 事务隔离级别
// IsolationLevel is the transaction isolation level used in TxOptions.
type IsolationLevel int
// Various isolation levels that drivers may support in BeginTx.
// If a driver does not support a given isolation level an error may be returned.
//
// See https://en.wikipedia.org/wiki/Isolation_(database_systems)#Isolation_levels.
const (
LevelDefault IsolationLevel = iota
LevelReadUncommitted
LevelReadCommitted
LevelWriteCommitted
LevelRepeatableRead
LevelSnapshot
LevelSerializable
LevelLinearizable
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
MySQL 的事务隔离级别
序列化(SERIALIZABLE)
- 上一个事务与下一个事务是严格顺序执行的(最严格)。
可重复读(REPEATABLE READ)
- A 事务无法看到 B 事务的修改。即 A 事务内同一个 SELECT 语句执行的结果总是相同的。
已提交读(READ COMMITTED)
- A 事务无法看到 B 事务未提交的修改,但是可以看到已提交的修改
未提交读(READ UNCOMMITTED)
- A 事务能看到 B 事务未提交的修改。
MySQL 默认的级别是可重复读。
事务的问题:
- 脏读:A 事务能看到 B 事务未提交的修改。
- 隔离级别:未提交读
- 不可重复读:A 事务内同一个 SQL 读到了不同的数据。
- 隔离级别:未提交读和已提交读
- 幻读:A 事务内读到了 B 事务新插入的数据。
- 隔离级别:未提交读、已提交读和可重复读(理论上)。注意 InnoDB 引擎的可重复读并不会引起幻读。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
未提交读 | Yes | Yes | Yes |
已提交读 | No | Yes | Yes |
可重复读 | No | No | Yes |
序列化 | No | No | No |
# Prepare Statement
Prepare Statement 表示准备一个需要多次使用的语句,供后续执行用。Prepare Statement 的生命周期和整个应用的生命周期一致。
在查询前进行准备是Go语言中的惯用法,多次使用的查询语句应当进行准备(Prepare
)。准备查询的结果是一个准备好的语句(prepared statement),语句中可以包含执行时所需参数的占位符(即绑定值)。准备查询比拼字符串的方式好很多,它可以转义参数,避免SQL注入。同时,准备查询对于一些数据库也省去了解析和生成执行计划的开销,有利于性能。
func main() {
//...
CreateTable(db)
InsertValue(db)
PrepareStat(db)
}
func PrepareStat(db *sql.DB) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
// 提前准备好 SQL 语句和占位符
stmt, err := db.PrepareContext(ctx, "SELECT `id`, `first_name`, `age`, `last_name` FROM `user` WHERE `id`=?")
if err != nil {
log.Fatalf("Prepare error: %v", err)
}
// 不用 stmt 时需要关闭
defer func(stmt *sql.Stmt) {
err = stmt.Close()
if err != nil {
log.Fatalln(err)
}
}(stmt)
// 执行查询语句,id = 1
rows, err := stmt.QueryContext(ctx, 1)
if err != nil {
log.Fatalf("查询失败:%v", err)
}
for rows.Next() { // 标准迭代器设计
u := User{}
if err = rows.Scan(&u.ID, &u.FirstName, &u.Age, &u.LastName); err != nil {
log.Fatalf("获取结果集失败:%v", err)
}
log.Println("结果:", u.String())
}
cancel()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
执行结果:
2022/12/12 22:33:28 结果: ID: 1 FirstName: Tom Age: 18 LastName: Jerry
# driver.Valuer
和sql.Scanner
接口
场景:SQL 默认支持的类型就是基础类型。如果我们使用自定义类型,比如支持json
类型,应该怎么处理?
driver.Valuer
:读取,实现该接口的类型可以作为查询参数使用(Go
类型到数据库类型)sql.Scanner
:写入,实现该接口的类型可以作为接收器用于Scan
方法(数据库类型到Go
类型)
自定义类型一般是实现这两个接口。
以自定义json
类型数据为例。
创建一个
json
列(数据库的列类型为json
)json.go
type JsonColumn[T any] struct { Val T // NULL 的问题 (sql.NullString) Valid bool }
1
2
3
4
5
6可参考
sql.NullString
的定义:type NullString struct { String string Valid bool // Valid is true if String is not NULL } // Scan implements the Scanner interface. func (ns *NullString) Scan(value any) error { if value == nil { ns.String, ns.Valid = "", false return nil } ns.Valid = true return convertAssign(&ns.String, value) } // Value implements the driver Valuer interface. func (ns NullString) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } return ns.String, nil }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22实现
driver.Value
和sql.Scan
json.go
// Value 实现 driver.Valuer // Go 类型到数据库类型(json) func (j JsonColumn[T]) Value() (driver.Value, error) { // NULL if !j.Valid { return nil, nil } return json.Marshal(j.Val) } // Scan 实现 sql.Scanner // 数据库类型(json)到 Go 类型 func (j *JsonColumn[T]) Scan(src any) error { // Scan 默认支持的类型 // int64 // float64 // bool // []byte // string // time.Time // nil - for NULL values var bs []byte switch data := src.(type) { case string: // 可以考虑额外处理空字符串 bs = []byte(data) case []byte: // 可以考虑额外处理 []byte{} bs = data case nil: // 说明数据库存的就是 NULL return nil default: return errors.New("不支持类型") } err := json.Unmarshal(bs, &j.Val) if err == nil { j.Valid = true } return err }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43创建
JsonColumn
的测试json_test.go
package sql_column import ( "context" "database/sql" "fmt" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "log" "testing" "time" ) // 测试 Value。Go 类型到数据库类型 func TestJsonColumn_Value(t *testing.T) { js := JsonColumn[User]{Valid: true, Val: User{Name: "Tom"}} value, err := js.Value() assert.Nil(t, err) assert.Equal(t, []byte(`{"Name":"Tom"}`), value) js = JsonColumn[User]{} value, err = js.Value() assert.Nil(t, err) assert.Nil(t, value) } // 测试 Scan。数据库类型到 Go 类型 func TestJsonColumn_Scan(t *testing.T) { testCases := []struct { name string src any wantErr error wantVal User valid bool }{ { name: "nil", }, { name: "string", src: `{"Name":"Tom"}`, wantVal: User{Name: "Tom"}, valid: true, }, { name: "bytes", src: []byte(`{"Name":"Tom"}`), wantVal: User{Name: "Tom"}, valid: true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { js := &JsonColumn[User]{} err := js.Scan(tc.src) assert.Equal(t, tc.wantErr, err) if err != nil { return } assert.Equal(t, tc.wantVal, js.Val) assert.Equal(t, tc.valid, js.Valid) }) } } // 测试 Scan。测试可以转为 JSON 的 Go 类型 func TestJsonColumn_ScanTypes(t *testing.T) { jsSlice := JsonColumn[[]string]{} err := jsSlice.Scan(`["a", "b", "c"]`) assert.Nil(t, err) assert.Equal(t, []string{"a", "b", "c"}, jsSlice.Val) val, err := jsSlice.Value() assert.Nil(t, err) assert.Equal(t, []byte(`["a","b","c"]`), val) jsMap := JsonColumn[map[string]string]{} err = jsMap.Scan(`{"a":"a value"}`) assert.Nil(t, err) val, err = jsMap.Value() assert.Nil(t, err) assert.Equal(t, []byte(`{"a":"a value"}`), val) } type User struct { Name string } // JsonColumn Value 方法的例子 func ExampleJsonColumn_Value() { js := JsonColumn[User]{Valid: true, Val: User{Name: "Tom"}} value, err := js.Value() if err != nil { fmt.Println(err) } fmt.Print(string(value.([]byte))) // Output: // {"Name":"Tom"} } // JsonColumn Scan 方法的例子 func ExampleJsonColumn_Scan() { js := JsonColumn[User]{} err := js.Scan(`{"Name":"Tom"}`) if err != nil { fmt.Println(err) } fmt.Print(js.Val) // Output: // {Tom} } type UserJson struct { ID int Name string } // 测试 JsonColumn 到真实数据库的 CRUD func TestJsonColumn_CRUD(t *testing.T) { db, err := sql.Open("sqlite3", "file:test.db?cache=shared&mode=memory") require.NoError(t, err) defer db.Close() db.Ping() ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) // 创建一个表,其中 name 字段的类型为 json _, err = db.ExecContext(ctx, ` CREATE TABLE IF NOT EXISTS user_json( id INTEGER PRIMARY KEY, name JSON ) `) // 完成了建表 require.NoError(t, err) js := JsonColumn[UserJson]{Valid: true, Val: UserJson{ID: 1, Name: "Tom"}} // 将 JsonColumn 插入数据库 res, err := db.ExecContext(ctx, "INSERT INTO `user_json`(`id`, `name`) VALUES (?, ?)", js.Val.ID, js) require.NoError(t, err) affected, err := res.RowsAffected() require.NoError(t, err) log.Println("受影响行数", affected) lastId, err := res.LastInsertId() log.Println(affected) log.Println("最后插入的ID", lastId) // 查询一行数据(预期只有一行) row := db.QueryRowContext(ctx, "SELECT `name` FROM `user_json` WHERE `id` = ?", 1) require.NoError(t, row.Err()) js2 := JsonColumn[UserJson]{} // 主要要用指针 var data string err = row.Scan(&data) require.NoError(t, err) err = js2.Scan(data) require.NoError(t, err) assert.Equal(t, `{"ID":1,"Name":"Tom"}`, data) log.Println(data) // {"Name":"Tom"} assert.Equal(t, UserJson{ID: 1, Name: "Tom"}, js2.Val) log.Println(js2.Val) cancel() }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163执行结果:
# go test -v --run="Json" === RUN TestJsonColumn_Value --- PASS: TestJsonColumn_Value (0.00s) === RUN TestJsonColumn_Scan === RUN TestJsonColumn_Scan/nil === RUN TestJsonColumn_Scan/string === RUN TestJsonColumn_Scan/bytes --- PASS: TestJsonColumn_ScanTypes (0.00s) === RUN TestJsonColumn_CRUD 2022/12/12 23:20:03 受影响行数 1 2022/12/12 23:20:03 1 2022/12/12 23:20:03 最后插入的ID 1 2022/12/12 23:20:03 {"ID":1,"Name":"Tom"} 2022/12/12 23:20:03 {1 Tom} --- PASS: TestJsonColumn_CRUD (0.00s) === RUN ExampleJsonColumn_Value --- PASS: ExampleJsonColumn_Value (0.00s) === RUN ExampleJsonColumn_Scan --- PASS: ExampleJsonColumn_Scan (0.00s) PASS ok leanring-go/orm/sql_demo 0.003s
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21