实战系列:(七)坑爹的Go

2019-11-15  本文已影响0人  foundwei

写在前面

       首先声明,本人是一位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

上一篇下一篇

猜你喜欢

热点阅读