实战系列:(七)坑爹的Go
写在前面
首先声明,本人是一位Go的初学者,经验并不丰富,出于项目需要开始使用Go语言。该文旨在记录本人在Go语言的开发过程中踩过的坑,以图分享给大家,避免重蹈覆辙。该文将不断补充内容,因为本人日前仍在也许将来一段时间都会使用Go语言开发,所以将不断补充、记录在使用Go的过程中遇到的各种问题。
修改历史
序号 | 修改内容 | 版本 | 修改时间 |
---|---|---|---|
1 | 条件判断语句折行问题 | 1.1 | 2019年11月21日 |
2 | xorm、gorm中update问题 | 1.0 | 2019年11月15日 |
3 | xorm英文文档问题 | 1.0 | 2019年11月15日 |
4 | 数据库事务挂起问题 | 1.0 | 2019年11月15日 |
5 | 不同数字类型的变量之间不能直接运算 | 1.0 | 2019年11月23日 |
6 | 变量作用域的坑 | 1.0 | 2019年11月28日 |
7 | interface的数据类型问题 | 1.0 | 2019年11月28日 |
8 | interface类型转换的问题 | 1.0 | 2019年11月29日 |
以此表格来记录该文的修改历史。
正文
坑1:条件判断语句折行问题
在Java代码,如果单行的判断语句有多个判断条件,会导致改行占用的宽度很大,所以Java代码中可以使用折行的方式缩小行宽,以便于阅读。
Java代码示例:
if(bean.getTradeCode() == null || bean.getTradeCode().equalsIgnoreCase("") ||
bean.getOrderTime() == null || bean.getOrderTime().equalsIgnoreCase("") ||
bean.getPayTime() == null || bean.getPayTime().equalsIgnoreCase("") ||
bean.getMemo() == null || bean.getMemo().equalsIgnoreCase("") ||
bean.getReceiver() == null || bean.getReceiver().equalsIgnoreCase("") ||
bean.getPhoneNum() == null || bean.getPhoneNum().equalsIgnoreCase("") ||
bean.getAddress() == null || bean.getAddress().equalsIgnoreCase("") ||
bean.getGoodsName() == null || bean.getGoodsName().equalsIgnoreCase("") ||
bean.getGoodsNum() == null || bean.getGoodsNum().equalsIgnoreCase("")) {
return "上传文件的数据不正确!";
}
Java中通过折行来减小行宽,便于阅读,貌似坑爹的Go中却不能如此。
比如下面的代码示例中,if语句要判断的条件有很多个,并且又使用了常量和Map,导致该行过宽(超出屏幕范围),实在难于阅读。
Go代码示例:
if countMap[global.STOCK_APPLY_REJECT] == applysLen && countMap[global.STOCK_APPLY_INIT] > 0 && countMap[global.STOCK_APPLY_CANCEL] != applysLen && countMap[global.STOCK_APPLY_CANCEL] > 0 {
// all of the applies are rejected, then group will be rejected.
status = global.STOCK_APPLY_GROUP_REJECTED
}
想尝试像Java代码那样通过折行的方式来减小宽度,发现并不可行,报以下错误。
条件语句折行错误【更新于2019年11月21日】经高人指点,原来Go中的条件判断语句也是可以折行的,但是必须要在条件运算符号后面折行,类似于下面这样:
if countMap[global.STOCK_APPLY_REJECT] == applysLen &&
countMap[global.STOCK_APPLY_INIT] > 0 &&
countMap[global.STOCK_APPLY_CANCEL] != applysLen &&
countMap[global.STOCK_APPLY_CANCEL] > 0 {
// all of the applies are rejected, then group will be rejected.
status = global.STOCK_APPLY_GROUP_REJECTED
}
再补充一句,Java的语法就比较随意了,在条件运算符的前后都可以。
坑2:xorm、gorm中update问题
前几天遇到一个bug,使用xorm中的update方法更新数据库表中一个字段(is_leader)失败。该字段为tinyint类型,取值为0或1,代表了一个状态而已。当从0修改为1时,成功;但从1修改为0时,却不成功!
之前的代码是这样进行更新操作的,使用结构体(struct)作为参数,相关代码如下:
import (
"github.com/go-xorm/xorm"
)
var X *xorm.Engine
type Activity struct {
Id int `json:"id" xorm:"autoincr"`
ActivityStartTime time.Time `json:"activity_start_time"`
ActivityEndTime time.Time `json:"activity_end_time"`
Sort int `json:"sort"`
OpeningLevel string `json:"opening_level"`
BuyCount int `json:"buy_count"`
BuyingQuota int `json:"buying_quota"`
CreateTime time.Time `json:"create_time" xorm:"created"`
UpdateTime time.Time `json:"update_time" xorm:"updated"`
Updator string `json:"updator"`
Df int `json:"df"`
IsLeaderFree int `json:"is_leader_free"`
//其他省略
}
func UpdateActivity(activity *Activity) (num int64, err error) {
i, err := X.Table("activity").Where("id=?", activity.Id).Update(activity)
return i, err
}
调用的过程如下:
activity := &Activity {
Id: 1111,
BuyCount: 22,
Updator: "System",
IsLeaderFree: 0,
// 其他省略
}
n, err := UpdateActivity(activity)
if err != nil {
return err
}
对于一个Go经验尚浅的初学者来说,遇到这个问题一脸的懵逼,一时不知所措!翻来覆去看了好几遍,也没看出代码有什么问题,测试一下就是没有更新成功(如果结构体中is_leader字段的值为零时,确实没能更新成功)。
内事不决问百度,外事不决问谷歌!百度了一番,确实找到了问题的根源:在xorm中,结构体会自动忽略空字段(或者说默认值,比如int的0 ,string的"")。因为Go的结构体中的字段是有默认值的,在这种情况下,Go就无法识别是默认值(没有人为设置),还是设置的值,于是乎就忽略了该字段的更新。
对于一个具有探索精神的码农来讲,一定要看个究竟才安心,把xorm的源代码挖出来看看。
xorm源代码session_update.go:
func (session *Session) Update(bean interface{}, condiBean ...interface{}) (int64, error) {
......(省略)
if isStruct {
if err := session.statement.setRefBean(bean); err != nil {
return 0, err
}
if len(session.statement.TableName()) <= 0 {
return 0, ErrTableNotFound
}
if session.statement.ColumnStr == "" {
colNames, args = session.statement.buildUpdates(bean, false, false,
false, false, true)
} else {
colNames, args, err = session.genUpdateColumns(bean)
if err != nil {
return 0, err
}
}
} else if isMap {
colNames = make([]string, 0)
args = make([]interface{}, 0)
bValue := reflect.Indirect(reflect.ValueOf(bean))
for _, v := range bValue.MapKeys() {
colNames = append(colNames, session.engine.Quote(v.String())+" = ?")
args = append(args, bValue.MapIndex(v).Interface())
}
} else {
return 0, ErrParamsType
}
......(省略)
}
可以看到如果是传入的参数是struct类型的话,并且ColumnStr为空,调用了session.statement.buildUpdates(...)方法来生成要更新的列。继续挖掘buildUpdates的源码。
这里顺便先提一下,如果传入的参数类型是Map的话,就直接遍历Map的key并放到需要更新的列数组中,这是解决更新失败问题的方法之一,后面还会提到。
xorm源代码statement.go中buildUpdates方法大约340多行到380行左右到的位置:
// Auto generating update columnes and values according a struct
func (statement *Statement) buildUpdates(bean interface{},
includeVersion, includeUpdated, includeNil,
includeAutoIncr, update bool) ([]string, []interface{}){
switch fieldType.Kind() {
case reflect.Bool:
if allUseBool || requiredField {
val = fieldValue.Interface()
} else {
// if a bool in a struct, it will not be as a condition because it default is false,
// please use Where() instead
continue
}
case reflect.String:
if !requiredField && fieldValue.String() == "" {
continue
}
// for MyString, should convert to string or panic
if fieldType.String() != reflect.String.String() {
val = fieldValue.String()
} else {
val = fieldValue.Interface()
}
case reflect.Int8, reflect.Int16, reflect.Int, reflect.Int32, reflect.Int64:
if !requiredField && fieldValue.Int() == 0 {
continue
}
val = fieldValue.Interface()
case reflect.Float32, reflect.Float64:
if !requiredField && fieldValue.Float() == 0.0 {
continue
}
val = fieldValue.Interface()
case reflect.Uint8, reflect.Uint16, reflect.Uint, reflect.Uint32, reflect.Uint64:
if !requiredField && fieldValue.Uint() == 0 {
continue
}
t := int64(fieldValue.Uint())
val = reflect.ValueOf(&t).Interface()
......(省略)
APPEND:
args = append(args, val)
if col.IsPrimaryKey && engine.dialect.DBType() == "ql" {
continue
}
colNames = append(colNames, fmt.Sprintf("%v = ?", engine.Quote(col.Name)))
}
return colNames, args
}
从以上源代码可以看出,确实忽略了结构体中为默认值的字段(continue了),并没有放到需要更新的列数组中。实际上,另外一个orm工具gorm同样存在这个问题,这是由Go语言的特点造成的。
其实xorm也好,gorm也罢,在他们的使用文档中已经说明了。
xorm的文档说明:
// Update records, bean's non-empty fields are updated contents,
// condiBean' non-empty filds are conditions
// CAUTION:
// 1.bool will defaultly be updated content nor conditions
// You should call UseBool if you have bool to use.
// 2.float32 & float64 may be not inexact as conditions
func (session *Session) Update(bean interface{}, condiBean ...interface{}) (int64, error)
gorm的文档说明:
// WARNING when update with struct, GORM will only update those fields that with non blank value
// For below Update, nothing will be updated as "", 0, false are blank values of their types
db.Model(&user).Updates(User{Name: "", Age: 0, Actived: false})
总结一下就是,当用结构体更新的时候,当结构体的值是""或者0,false等的字段(更准确的说,结构体中的字段值为该类型的默认取值时),是不会更新的。
既然知道了问题的原因,那么接下来就看一下怎么能够克服这个弊端。如果要更新的字段值是0,"",false等,可以使用以下方法:
① 使用Map作为参数
其实这个方法在上面的源码分析过程过程中已经提到过了,具体实现方法如下:
m := map[string]interface{}{
"is_leader_free": 0,
// others
}
func UpdateActivity(id, int, m map[string]interface{}) (num int64, err error) {
i, err := X.Table("activity").Where("id=?", id).Update(m)
return i, err
}
② 使用更新选中字段的方法
可以调用Cols方法指定需要更新的列,如下:
i, err = X.Table("activity").Where("id=?", activity.Id).Cols("is_leader").Update(activity)
③ 使用sql语句方法
这种方法就不再赘述了,自己来生成sql语句。
坑3:xorm英文文档问题
这个坑和上面那个坑是有关联的,是在分析上面那个update的坑的时候发现的。
这是xorm关于update方法的文档说明,官网上也是一样的:
// Update records, bean's non-empty fields are updated contents,
// condiBean' non-empty filds are conditions
// CAUTION:
// 1.bool will defaultly be updated content nor conditions
// You should call UseBool if you have bool to use.
// 2.float32 & float64 may be not inexact as conditions
func (session *Session) Update(bean interface{}, condiBean ...interface{}) (int64, error)
第一次看到上面的文档说明时,我真的是感到欲哭无泪。这他妈中国人看不明白,外国人看不懂,什么狗屁英文!居然还有非常低级的拼写错误,一点认真的态度都没有。
xorm英文文档拼写错误 而且这段注释怎么看怎么别扭,英文中有defaultly这个单词吗?恕我才疏学浅,只能猜测写这个文档的人的母语肯定不是英语。查看了一下xorm的github主页,项目发起者和代码贡献做多的两个人原来是中国人,一位来自上海,一位来自杭州。在写出不错的开源项目这一点上,我为国人自豪!既然官方的文档都是英文写的,那么作者肯定是希望该项目能够在全世界范围内普及的。但是,我奉劝一句,能不能专业一点,敬业一点,连英文文档都写不好,还谈什么国际化,中国人看不懂,外国人看不明白。我当然希望国人的开源项目能够在世界舞台上走的更远,有更大的影响力,但是这样的英文水平着实成为拖累。
且抛开技术方面不谈,说实话我没资格,因为xorm我也只是应用一下而已,深层次的东西不太了解。既然想国际化,扩大影响力,那就得专业一点,好好的把英文文档更新一下吧!说实话英文真的很low!我不是故意找茬儿,只是建议国人学习一下老外一丝不苟的工作态度!
坑4:数据库事务挂起问题
请参看我的另外一篇文章。实战系列:(六)Hang issue of DB transaction in Go
坑5:不同数字类型的变量之间不能直接运算
Java语言中,不同数字类型的变量是可以直接做数学运算的,比如:一个int类型的变量与一个float类型的变量相乘时,会自动把int类型的变量转换为float类型。但是Go语言中却不是这样,不同数字类型的变量之间不能直接运算(例如:加减乘除),必须先强制类型转换为相同的数据类型之后才能够进行运算。比如下面这个例子是会报错的:
var b float32
b = 22.22
var c float64
c = 33.33
a := b + c
不同数字类型的变量相加
必须强制转换为完全相同的数据类型才能进行运算,如下所示:
var b float32
b = 22.22
var c float64
c = 33.33
a := float64(b) + c //必须强制转换为完全相同的数据类型才能进行运算
int类型之间也是一样的道理,int8、int16、int、int32、int64、uint8、uint16、uint、uint32、uint64。
但是,数值之间或者数值与变量之间是可以直接做数学运算的,如下面的几个例子都是OK的:
a := 11 + 23.11
a := 11 * 23.11
var b int32
b = 22
a := 11 + b // a的类型为int32
var b int
b = 22
a := 11 + b // a的类型为int
这个应该是语言特性,也不能算是Go语言的坑,只是不太方便而已。
坑6:变量作用域的坑
总觉得Go怪怪的,很有个性,有点特立独行的意味!比如下面这个变量作用域的问题。
在Go中,局部变量的有效范围只是在自己的作用域范围之内,即使作用域外部有相同名称的变量也不能覆盖。用下面这个例子来说明,GetItemPageList方法的一开始先声明一个变量items,类型为[]*models.Item。在方法体中有一个if语句块中同样有一个items的变量,通过调用FindItemByStoreNum方法返回,类型也是[]*models.Item。实际上,if语句块中的items变量与一开始声明的items变量是两个独立的变量,if语句块中的items并没有像我们认为的那样覆盖掉外面的items。所以当方法执行到for语句块的时候items是nil,这个items是一开始声明的那个items,默认值为nil,并没有被if语句块中的items变量覆盖掉。
func GetItemPageList(queryMap map[string]string) (pageInfo PageInfo, err error) {
var items []*models.Item
// ......
if queryMap["type"] == "1" {
items, _, err := admin.FindItemByStoreNum(queryMap["supplierId"])
if err != nil {
return pageInfo, err
}
}
// ......
for _, item := range items {
// 此处的items为nil
}
}
其中这个方法FindItemByStoreNum的返回值类型为[]*models.Item。
解决办法如下:
func GetItemPageList(queryMap map[string]string) (pageInfo PageInfo, err error) {
var items1 []*models.Item
if queryMap["type"] == "1" {
items, _, err := admin.FindItemByStoreNum(queryMap["supplierId"])
if err != nil {
return pageInfo, err
}
items1 = item // 赋值给外部的变量
}
// ......
for _, item := range items1 {
// items1是ok的
}
}
使用两个不同名的变量,在if语句块中给外面的变量赋值。
坑7:interface的数据类型问题
interface是Go中的接口,此外它还可以作为函数参数,因为interface的变量可以持有任意实现该interface类型的对象,我们可以通过定义interface参数,让函数接受各种类型的参数。这本来应该是一个非常不错的语言特性,但时不时会让人掉入坑中。
直接用例子来说明:
qMap := make(map[string]interface{})
qMap["value"] = 1
if qMap["value"] == 1 {
print("equal")
} else {
print("not equal")
}
输出结果为:equal
使用debug工具查看qMap["value"]的类型和值:
debug查看类型及值
再来看一个例子:
if int32(1) == 1 {
print("equal")
} else {
print("not equal")
}
输出为:equal,看似很正常。
奇怪的是下面这个例子:
qMap := make(map[string]interface{})
qMap["value"] = int32(1)
if qMap["value"] == 1 {
print("equal")
} else {
print("not equal")
}
输出结果为:not equal
看看debug工具的类型和值:
debug查看类型及值
qMap["value"]居然不等于1,要想得到期望的逻辑,必须得这样才行:
qMap := make(map[string]interface{})
qMap["value"] = int32(1)
if qMap["value"].(int32) == 1 {
print("equal")
} else {
print("not equal")
}
本人目前还没有搞清楚到底是什么原因?如果哪位高人知道,请不吝赐教!暂时抛开具体原因不讲,这样很容易造成逻辑错误。
坑8:interface类型转换的问题
这个还是跟interface有关,interface使用起来非常灵活,但也很容易产生误解,造成潜在的问题。Go中的interface是一个神奇的存在!作者不是批评interface的功能,不过过于灵活的功能还是很难以驾驭的。
先来看一个例子:
var b interface{}
b = int32(0)
b = 1.1
b = "hello"
print(b)
这样是可以的,变量b的类型一直在改变,从int32变为float64,再变为string。
interface变量的类型变化
但是下面这个例子是有问题的:
var b interface{}
b = int32(0)
a := int32(1)
a = a + b
报以下错误:
类型不匹配
必须指明b的类型才可以:
var b interface{}
b = int32(0)
a := int32(1)
a = a + b.(int32) // 修改点
最坑爹是这个:
var b interface{}
b = int32(0)
b = b.(int) + 1
没有语法错误和编译错误,但运行时抛出错误“interface conversion: interface {} is int32, not int”。这种问题还是尽早发现的好,等到运行时再去检查发现这种问题有点晚喽!
未完待续......
2019年11月15日星期五 于团结湖瑞辰国际中心
2019年11月21日 更新坑1
2019年11月23日 添加坑5
2019年11月28日 添加坑6
2019年11月28日 添加坑7
2019年11月29日 添加坑8