Go语言细节摘录

  1. defer并不延迟函数的参数本身的调用
    func trace(s string) string { fmt.Println("entering:", s) return s }func un(s string) { fmt.Println("leaving:", s) }func a() { defer un(trace("a")) fmt.Println("in a") }func b() { defer un(trace("b")) fmt.Println("in b") a() }func main() { b() }

    返回结果为
    entering: b in b entering: a in a leaving: a leaving: b

    这一特性可以用来设计系统状态转移机制。
  2. new和make以及声明的关系。
    首先要明晰值类型和引用类型的区别。go中值类型包括基本数据类型,数组以及结构体;引用类型包括指针,slice,map以及channel几种。值类型变量存储在栈上,变量的内容就是值;引用类型变量存储在堆上,变量的内容为堆区内存的首地址。在利用var声明变量时,值类型会默认分配对应的内存空间以及默认值,引用类型并没有在堆中分配内存,因此变量内容为0x0,此时需要用new或者make分配空间。下面的分析都只针对引用类型,因为值类型的new与声明相比几乎没什么区别。new和它在其他语言如C++中最大的区别是go的new不初始化内存,也就是说,对于new(T),为T类型实例分配了默认值的内存(引用类型默认值都是nil),并返回了T实例的首地址,所以new(T)返回的是*T类型的值,是个指向T实例的指针。make只适用于slice,map和channel,返回的是类型本身。看个例子吧,new大多数情况下没什么用处。
    var p *[]int = new([]int)// allocates slice structure; *p == nil; rarely useful var v[]int = make([]int, 100) // the slice v now refers to a new array of 100 ints// Unnecessarily complex: var p *[]int = new([]int) *p = make([]int, 100, 100)// Idiomatic: v := make([]int, 100)

    所以,对于值类型与引用类型,请分别使用普通定义方式和make方式,new不推荐。
  3. go中没有引用传递
    go的函数传参都是值传递,如果想达到引用传递的效果需要传递指向变量的指针,但这也是值传递。参数传递时,会把参数值复制一份,由于引用类型的值是指向堆内存的地址,因此复制引用类型变量后,对该复制体的修改仍会影响到堆内存中的实际值,这其实就达到了引用传递的效果。下面是一个简单的测试,结构体属于值类型,但结构体中包含的slice属于引用类型,因此结构体作为参数会复制结构体,对复制结构体的slice修改还是能对原结构体的slice产生影响。
    示例: type SV struct { s []int v int } func testModify(target SV) { fmt.Printf("target:%#v, &target:%p, target.s:%p, target.s:%#v\n", target, &target, target.s, target.s) target.s[1] = 2 fmt.Printf("target:%#v, &target:%p, target.s:%p, target.s:%#v\n", target, &target, target.s, target.s) target.v = 10 } func main() { ts := make([]int, 2) ts[0] = 1 target := SV{s: ts} fmt.Printf("target:%#v, &target:%p, target.s:%p, target.s:%#v\n", target, &target, target.s, target.s) testModify(target) fmt.Printf("target:%#v, &target:%p, target.s:%p, target.s:%#v\n", target, &target, target.s, target.s) }

    结果: target:main.SV{s:[]int{1, 0}, v:0}, &target:0xc00007e020, target.s:0xc000088010, target.s:[]int{1, 0} target:main.SV{s:[]int{1, 0}, v:0}, &target:0xc00007e0a0, target.s:0xc000088010, target.s:[]int{1, 0} target:main.SV{s:[]int{1, 2}, v:0}, &target:0xc00007e0a0, target.s:0xc000088010, target.s:[]int{1, 2} target:main.SV{s:[]int{1, 2}, v:0}, &target:0xc00007e020, target.s:0xc000088010, target.s:[]int{1, 2}

    但是这里也有两个坑。首先,如果源结构体的slice长度设成0,容量设为2,然后在函数中对其进行append操作,如下所示:
    type SV struct { s []int v int } func testModify(target SV) { fmt.Printf("target:%#v, &target:%p, target.s:%p, target.s:%#v\n", target, &target, target.s, target.s) target.s = append(target.s, 3) fmt.Printf("target:%#v, &target:%p, target.s:%p, target.s:%#v\n", target, &target, target.s, target.s) target.v = 10 } func main() { ts := make([]int, 0, 2) target := SV{s: ts} fmt.Printf("target:%#v, &target:%p, target.s:%p, target.s:%#v\n", target, &target, target.s, target.s) testModify(target) fmt.Printf("target:%#v, &target:%p, target.s:%p, target.s:%#v\n", target, &target, target.s, target.s) }

    target:main.SV{s:[]int{}, v:0}, &target:0xc00000c080, target.s:0xc00001c110, target.s:[]int{} target:main.SV{s:[]int{}, v:0}, &target:0xc00000c100, target.s:0xc00001c110, target.s:[]int{} target:main.SV{s:[]int{3}, v:0}, &target:0xc00000c100, target.s:0xc00001c110, target.s:[]int{3} target:main.SV{s:[]int{}, v:0}, &target:0xc00000c080, target.s:0xc00001c110, target.s:[]int{}

    可以看到这个结果最后原结构体显示并没有append成功,明明ts的容量是足够append的,这里推测是原来的slice的长度属性没有被改变?但是看地址显示复制的结构体中的slice都是一个地址,如果函数中改变了长度应该会影响原来的结果才对,暂时比较迷惑这个点,希望有大神解答。第二个坑就比较常见了,append超出原slice的容量,导致slice新开内存扩容然后复制原slice的值。
    type SV struct { s []int v int } func testModify(target SV) { fmt.Printf("target:%#v, &target:%p, target.s:%p, target.s:%#v\n", target, &target, target.s, target.s) target.s = append(target.s, 3) fmt.Printf("target:%#v, &target:%p, target.s:%p, target.s:%#v\n", target, &target, target.s, target.s) target.v = 10 } func main() { ts := make([]int, 2) target := SV{s: ts} fmt.Printf("target:%#v, &target:%p, target.s:%p, target.s:%#v\n", target, &target, target.s, target.s) testModify(target) fmt.Printf("target:%#v, &target:%p, target.s:%p, target.s:%#v\n", target, &target, target.s, target.s) }

    target:main.SV{s:[]int{0, 0}, v:0}, &target:0xc00000c080, target.s:0xc00001c110, target.s:[]int{0, 0} target:main.SV{s:[]int{0, 0}, v:0}, &target:0xc00000c100, target.s:0xc00001c110, target.s:[]int{0, 0} target:main.SV{s:[]int{0, 0, 3}, v:0}, &target:0xc00000c100, target.s:0xc000016440, target.s:[]int{0, 0, 3} target:main.SV{s:[]int{0, 0}, v:0}, &target:0xc00000c080, target.s:0xc00001c110, target.s:[]int{0, 0}

    这里可以看到append后的slice内存地址已经变了,所以必然不会影响原slice。
  4. GO中的数据格式转换,hex string to int
    go的strconv包提供了足够的格式转换功能,但是对于hex string类型的负数值,直接使用ParseInt是会出问题的,见下面代码:
    func main() { hexStr := "80000000" num1, err := strconv.ParseInt(hexStr, 16, 32) fmt.Printf("Original hexString:%s; Direct parseInt:%d; err:%s\n", hexStr, num1, err) num2, err := strconv.ParseUint(hexStr, 16, 32) realNum := int32(num2) fmt.Printf("Original hexString:%s; ParseUint then transform:%d; err:%s\n", hexStr, realNum, err) decStr := "-2147483648" num3, err := strconv.ParseInt(decStr, 10, 32) fmt.Printf("Original decString:%s; Direct parseInt:%d; err:%s\n", decStr, num3, err) }

    Original hexString:80000000; Direct parseInt:2147483647; err:strconv.ParseInt: parsing "80000000": value out of range Original hexString:80000000; ParseUint then transform:-2147483648; err:%!s() Original decString:-2147483649; Direct parseInt:-2147483648; err:%!s()

    这里hexStr是负数,可以看到直接用ParseInt转是会溢出的,所以要先用ParseUint转uint32,然后显示转换成int32;而如果字符串是十进制形式的,就没这个问题,可以直接转。所以需要研究下ParseInt的源码,如下:
    func ParseInt(s string, base int, bitSize int) (i int64, err error) { const fnParseInt = "ParseInt" // Empty string bad. if len(s) == 0 { return 0, syntaxError(fnParseInt, s) } // Pick off leading sign. s0 := s neg := false if s[0] == '+' { s = s[1:] } else if s[0] == '-' { neg = true s = s[1:] } // Convert unsigned and check range. var un uint64 un, err = ParseUint(s, base, bitSize) if err != nil && err.(*NumError).Err != ErrRange { err.(*NumError).Func = fnParseInt err.(*NumError).Num = s0 return 0, err } if bitSize == 0 { bitSize = int(IntSize) } cutoff := uint64(1 << uint(bitSize-1)) if !neg && un >= cutoff { return int64(cutoff - 1), rangeError(fnParseInt, s0) } if neg && un > cutoff { return -int64(cutoff), rangeError(fnParseInt, s0) } n := int64(un) if neg { n = -n } return n, nil }

    可以看到,ParseInt在处理正负数时,判断标准仅仅是前面有没有带负号,而对于其他包括二进制、八进制、十六进制,首位最高位为1代表负数的形式,是不支持的。判断完符号,就调用ParseUint进行处理,得到结果会根据符号和bitSize来判断是否溢出,如果溢出就返回能表示的最大值以及错误信息。
  5. 格式化输出时,%#v的坑
    %#v是完整按照golang的语法打印值,但是转义字符的问题,对于如下表示
    type T struct { a int b float64 c string } t := &T{ 7, -2.35, "abc\tdef" } fmt.Printf("%v\n", t) fmt.Printf("%+v\n", t) fmt.Printf("%#v\n", t)

    &{7 -2.35 abcdef} &{a:7 b:-2.35 c:abcdef} &main.T{a:7, b:-2.35, c:"abc\tdef"}

    可以看到转义字符不会生效。
【Go语言细节摘录】

    推荐阅读