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 abc   def}
    &{a:7 b:-2.35 c:abc     def}
    &main.T{a:7, b:-2.35, c:"abc\tdef"}

    可以看到转义字符不会生效。

  6.  

 

你可能感兴趣的:(GO)