美文网首页
实战系列:(七)坑爹的Go

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

作者: foundwei | 来源:发表于2019-11-15 17:10 被阅读0次

    写在前面

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

    相关文章

      网友评论

          本文标题:实战系列:(七)坑爹的Go

          本文链接:https://www.haomeiwen.com/subject/rsxlictx.html