gorm 是 go 语言中实现的比较好的 ORM 包,且是国人开发的。项目地址
Scan 是 gorm 提供的一个把数据库结果读取到 struct 的函数。定义如下:
// Scan scan value to a struct
func (s *DB) Scan(dest interface{}) *DB {
return s.NewScope(s.Value).Set("gorm:query_destination", dest).callCallbacks(s.parent.callbacks.queries).db
}
今天同事小张写代码的时候写了一个类似如下的语句:
var account models.Account
database.DB.Table("accounts").Scan(&account) // 这里 DB 是 gorm 的实例
如上查出了表 accounts 里所有的数据但是把结果读入一个 struct 变量 account 中,显然这是错的,但是 gorm 竟然可以正常运行,最诡异的是读出来的 account 信息是乱掉的,通过 account 的 id 查到数据库中的数据的 name 字段和 account 实际返回的不一致。由于小张真实的查询语句比上面的复杂很多,所以当时小张是懵逼了的,而当小张拿给我看的时候我也是懵逼了的。
当我看到 Scan 这里明显应该使用 First 代替的时候,我就想到应该是 Scan 的实现造成的。
callback_query.go
if rows, err := scope.SQLDB().Query(scope.SQL, scope.SQLVars...); scope.Err(err) == nil {
defer rows.Close()
columns, _ := rows.Columns()
for rows.Next() {
scope.db.RowsAffected++
elem := results
if isSlice {
elem = reflect.New(resultType).Elem()
}
scope.scan(rows, columns, scope.New(elem.Addr().Interface()).Fields())
if isSlice {
if isPtr {
results.Set(reflect.Append(results, elem.Addr()))
} else {
results.Set(reflect.Append(results, elem))
}
}
}
if err := rows.Err(); err != nil {
scope.Err(err)
} else if scope.db.RowsAffected == 0 && !isSlice {
scope.Err(ErrRecordNotFound)
}
}
以上代码是读取数据结果并把结果放入目标变量 results (即对应我们上面声明的 account),以上代码是个循环,循环处理数据库返回的多条记录。最核心的代码是如上的 8 - 13 行,这里判断如果 results 是 slice 那么就 new 一个 元素,并在 13 行把当前行数据库数据赋值给新元素,然后在 15 到 20 行时 append slice 后面,如果 results 不是 slice 那么 再 第 13 行处理任何一行数据库数据时都是重复的对 results 赋值。所以 Scan 并不像 gorm 注释中说的给 struct 赋值,其是可以传 slice 和 struct 的。
但是为什么小张传的 account 的信息是乱掉的呢?继续追第 13 行代码如下:
func (scope *Scope) scan(rows *sql.Rows, columns []string, fields []*Field) {
var (
ignored interface{}
values = make([]interface{}, len(columns))
selectFields []*Field
selectedColumnsMap = map[string]int{}
resetFields = map[int]*Field{}
)
for index, column := range columns {
values[index] = &ignored
selectFields = fields
offset := 0
if idx, ok := selectedColumnsMap[column]; ok {
offset = idx + 1
selectFields = selectFields[offset:]
}
for fieldIndex, field := range selectFields {
if field.DBName == column {
if field.Field.Kind() == reflect.Ptr {
values[index] = field.Field.Addr().Interface()
} else {
reflectValue := reflect.New(reflect.PtrTo(field.Struct.Type))
reflectValue.Elem().Set(field.Field.Addr())
values[index] = reflectValue.Interface()
resetFields[index] = field
}
selectedColumnsMap[column] = offset + fieldIndex
if field.IsNormal {
break
}
}
}
}
scope.Err(rows.Scan(values...))
for index, field := range resetFields {
fmt.Println(reflect.ValueOf(values[index]).Elem().Elem().String())
if v := reflect.ValueOf(values[index]).Elem().Elem(); v.IsValid() {
field.Field.Set(v)
}
}
}
第 65 到 93 行是把数据库返回的字段和我们要保存到的 struct 的个字段关系一一对应起来分别保存在 values 和 resetFields 中,第 97 到 102 行是把数据库个字段数据赋值到 struct 对应的字段值。而最重要的是第 99 到 101 这里会遍历每行数据的每个字段时如果该字段不为空(这就是为啥数据会乱掉)就会把该字段值更新到 struct 中。到这我们就可以明白当我们调用 Scan 时传入 struct 而数据库返回多条数据时,如果第一条数据有为空的字段而后面数据行有该字段不为空时就会覆盖掉 struct 中对应字段的值。
Scan 的这个问题应该算是 gorm 的一个 bug,而从 gorm 源码结构上可以看到引入这么奇怪的问题是因为 gorm 为了复用数据库数据赋值到目标变量上引入的。不只是 Scan 存在这个问题,Find 也同样存在同样的问题。但其实按理说我们应该乖乖的使用 First,这也不能算是一个 bug 吧。
非著名程序员,全栈开发工程师,长期专注系统开发与架构设计。
功能待开通!
三个工具介绍 go fmt是用来规范go文件格式,比如格式化单个文件 go fmt xxx.go goimports 用来检查导入包,导入依赖包,删除不依赖的包 gometalinter 集成go语言几乎所有检测工具,静态分析代码,包含功能如下 go vet -工具可以帮我们静态分析我们的源码存在的各种问题,例如多余的代码,提前return的逻辑,struct的tag是否符合标准等。 go tool vet --shadow -用来检查作用域里面设置的局部变量名和全局变量名设置一样导致全局变量设置无效的问题 gotype -类型检测用来检测传递过来的变量和预期变量类型一致 gotype -x
Go 语言的可移植性 Java 平台可移植性是众所都知的,Java 的可移植性依赖于其虚拟机 JVM,Java 实现了对不同平台的 JVM 的支持,那么一份 Java 代码就可以在各个平台上运行。而 Go 语言的可移植性也是依赖于其 runtime,runtime 去对接操作系统层,用户代码在 runtime 中运行,用户代码就不用去关心平台问题。 查看 Go 支持的OS和平台: > $ go version go version go1.11 darwin/amd64 liushuai@liushuaideMacBook-Pro ~/Documents/goProject/src
临时忽略掉struct中空字段 type User struct { Email string `json:"email"` Password string `json:"password"` } 当我们把用户信息返回给前端的时候显然需要忽略调Password 字段,则可以这样做: json.Marshal(struct{ *User Password bool `json:"password,omitempty"` }{ User:user, }) 临时添加额外字段 type User struct { Email string `json:"
golang中字符串格式化输出 package main import ( "fmt" "os" ) type point struct { x, y int } func main() { // Go提供了几种打印格式,用来格式化一般的Go值,例如 // 下面的%v打印了一个point结构体的对象的值 p := point{1, 2} fmt.Printf("%v\n", p) // 如果所格式化的值是一个结构体对象,那么`%+v`的格式化输出 // 将包括结构体的成员名称和值 fmt.Printf("%
Go语言的条件编译是由go/build包支持的,通过条件编译我们可以实现根据不同的参数编译包里不同的文件。 Go是怎么支持条件编译的 Go通过在源代码里添加编译标签(build tag)实现条件编译的。编译标签是以// +build开始,并且出现在代码文件的最开始。构建选项规则如下: 以逗号分割的选项是并的关系 以空格分割的选项是或的关系 条件项的名字用字母+数字表示,!表示否定的意思 构建标签后必须留一行空行 例如如下的例子: // +build linux,386 darwin,!cgo 对应的布尔表达式就是 (linux AND 386) OR (darwin AND (NOT cg