gopher常见坑位

在我看来,golnag有许多反直观的设计,而且这些设计通常不能自圆其说,导致gohper一而再再而三的调入陷阱。

网上也有很多gohper总结了一些笔记,我再提炼精简一下,挂在脑图树上便于记忆。

值类型包括:所有integer、所有float、bool、string、数组和structure

引用类型包括:指针、slice、map、chan、interface


1. map需要先先初始化,才能使用

issue: 定义、不初始化使用,运行时panic;
solution:make关键字初始化或者使用map字面量

m := make(map[string]float64, 5)
m["pi"] = 3.14

mm := map[string]float64{"pi": 3.14, "pi1": 4.14}  // map字面量

slice不需要初始化就可以被append.

var ss []int
ss = append(ss, 1, 2, 3)
fmt.Println(ss) // [1,2,3]

2. map中取得不存在的键,会返回零值,

issue:

func main() {  
   x := map[string]string{"one":"a","three":"c"}
   if v := x["two"]; v == "" { // 不存在该键值对,也能返回零值“”
       fmt.Println("true")
   }
}

solution: 使用map取值的参数2 bool值来判断

func main() {  
    x := map[string]string{"one":"a","two":"","three":"c"}
    if _,ok := x["two"]; !ok {
        fmt.Println("no entry")
    }
}

3. array作为函数实参, array内容不受函数内影响

issue: golang array 是值类型,形参是实参的内存拷贝

package main

import "fmt"

func changeFunc(arr [3]int) {
	arr[0] = 222
}
func main() {
	var arr [3]int = [3]int{1, 2, 3}
	changeFunc(arr)
	for i, item := range arr {
		fmt.Printf("index : %d, item: %d \n\r", i, item)
	}
}

solution: 实参从array改成引用类型的 slice。

slice切片的实质是SliceHeader 结构体,值传递slice时,正好将底层数组指针拷贝。

type SliceHeader struct {
  Data uintptr  // 底层数组的指针
  Len  int
  Cap  int
}

4. 变量遮蔽

issue: 块内声明的变量会遮蔽上层的同名变量n

func main() {
	n := 0
	if true {
		n := 1
		n++
	}

	fmt.Println(n)  // 0
}

solution: 不要使用同名遮蔽变量,使用 n=1

5. 不可修改的字符串

issue:同大多数语言一样,golang的string是不可变的

func main() {
	s := "hello"
	s[0] = 'H'
}

# command-line-arguments
.\main.go:8:2: cannot assign to s[0] (value of type byte)

solution: 尝试通过byte/rune 中转

	s := "hello"

	buf := []rune(s)
	buf[0] = 'H'
	ss := string(buf)
	fmt.Println(ss)   // Hello

6. strings.TrimRight 并非剔除右侧后缀

issue: strings.TrimRight实际是将cutset字符串拆成字符,然后原字符串从右向左,直到遇到没有在cutset中国出现的字符。

fmt.Println(strings.TrimRight("ABBA", "BA"))  //  ""
fmt.Println(strings.TrimRight("ABBAABABCABAB", "BA")) //  "ABBAABABC"

solution: 常规的移除后缀使用 strings.TrimSuffix

7. copy函数:最小化拷贝

issue: 内置函数Copyreturns the number of elements copied, which will be the minimum of len(src) and len(dst).

	src := []int{1, 2, 3}
	var dst []int      // 此时len(dst) =0
	copy(dst, src)
	fmt.Println(dst)   //  []

solution: 初始化足够空间的dst

src := []int{1, 2, 3}
	var dst []int = make([]int, 3)
	copy(dst, src)
	fmt.Println(dst) //  [1,2,3]

或者使用append方法

	src := []int{1, 2, 3}

	var dst1 []int
	dst1 = append(dst1, src...)
	fmt.Println(dst1) // [1,2,3]

append方法既可以加元素,也可以加切片,真是活见久。
// slice = append(slice, elem1, elem2)
// slice = append(slice, anotherSlice...)

golang ... 有三种用法:
① 不定长参数 func test1( test ...string) // 可以接收任意个string参数
② 将slice打散
sli []string= []string {"A","B","C"}
test1(sli...)

8. range slice不是随机,range map是随机的

issue:

func main() {
	ss := []int{11, 4, 5, 2, 7}

	for i, item := range ss {
		fmt.Printf(" %d : %d \r\n", i, item)
	}

	mm := map[string]int{"11": 11, "4": 4, "5": 5, "2": 2, "7": 7}
	for i, m := range mm {
		fmt.Printf(" %s : %d \r\n", i, m)
	}
}
输出:
 0 : 11 
 1 : 4 
 2 : 5
 3 : 2
 4 : 7     // slice
 4 : 4     // map
 5 : 5
 2 : 2
 7 : 7
 11 : 11

9. 让人困惑的for-range 循环

golang中除了经典的三段式for循环外,还有帮助快速遍历 slice array map channel的 for range循环。

issue1:for range中操作迭代变量,原切片竟然没影响。

func main() {
	ss := []int{1, 1, 1}

	for _, x := range ss {
		x = x + 1
	}
	fmt.Println(ss)  // [1,1,1]
}

solution:操作索引值

func main() {
	ss := []int{1, 1, 1}

	for i,_ := range ss {
		ss[i] += 1
	}
	fmt.Println(ss)  // [2,2,2]
}

issue2: 这也是一个有意思的case, 迭代体内对于[修改array元素值]无意识, 对于[修改slice元素值]有意识, 活见久。

func main() {
	aa := [2]int{0, 0}
	for _, x := range aa {
		fmt.Println(x) //  print  0,0
		aa[1] = 8
	}
	fmt.Println(aa) // print  [0,8]
}

solution: 将array换成slice

	ss := []int{0, 0}
	for _, x := range ss {
		fmt.Println(x) //  print  0,8
		ss[1] = 8
	}
	fmt.Println(ss) // print  [0,8]

以上问题的关键是:

所有的 range 循环,Go 语言都会在编译期将原切片或者数组赋值给一个新变量 ha,在赋值的过程中就发生了拷贝,而我们又通过 len 关键字预先获取了切片的长度,所以在循环中追加新的元素也不会改变循环执行的次数,这也就解释了上面提到的现象。

而遇到这种同时遍历索引和元素的 range 循环时,Go 语言会额外创建一个新的 v2 变量存储切片中的元素,循环中使用的这个变量 v2 会在每一次迭代被重新赋值而覆盖,赋值时也会触发拷贝

ha := a
hv1 := 0
hn := len(ha)
v1 := hv1
v2 := nil
for ; hv1 < hn; hv1++ {
    tmp := ha[hv1]
    v1, v2 = hv1, tmp
    ...
}

C#中没有这么多诡异的情况。
C#数组是定长数组,一旦被创建,数组大小就无法改变;
span 带有底层数组指针和长度,但是长度也是只读,是类型安全、内存安全的滑块。

10. nil值比较

issue: golang中:一个接口等于另一个接口,前提是它们的类型和动态值相同。这同样适用于nil值。

func Foo() error {
	var err *os.PathError = nil
	return err
}
func main() {
	err := Foo()
	fmt.Println(err)         // print: <nil>
	fmt.Println(err == nil)  // print: false
}

solution: 强转为同一类型

 fmt.Println(err == (*os.PathError)(nil))  // print: true

或者显式返回nil error

func returnsError() error {
  if bad() {
      return ErrBad
  }
  return nil
}

在底层,接口被实现为两个元素,一个类型T和一个值V,V是一个具体的值,比如int、结构体或指针,而不是接口本身,它的类型是T, 上面的错误示例中: err 具备了T=*MyError, V=nil 的实现,故与nil不等。

只要记住,如果接口中存储了任何具体的值,该接口将不会为nil.


最后再提供几张图,供大家参考,也许上面的坑位能柳暗花明。
(1)

[4]int在内存中的表示形式只是按顺序排列的四个整数值:

(2)

s = make([]byte,5)

切片是数组片段的描述符,它由指向数组的指针、片段的长度和它的容量(片段的最大长度)组成。

当我们对s进一步切片: s =s[2,4]

C# span是指向一段连续内存的类型安全的、内存安全的视图,也有数组指针和长度length,不过他的length是只读定长的,也不会有扩容的动作。

(3)

m := make(map[string]string)

m是指向Map Header数据结构的指针,Map Header包含了关于map的所有元信息:
  • map中当前的条目数
  • map中的桶数总是等于2的幂,因此存储log(桶)以保持较小的值
  • 指向连续内存位置的桶数组的指针
  • 创建不同的map的哈希种子是随机

https://phati-sawant.medium.com/internals-of-map-in-golang-33db6e25b3f8

golang 有关panic()、recover()、 defer 异常、异常恢复相关的内容 https://go.dev/blog/defer-panic-and-recover

  1. A defer statement pushes a function call onto a list. The list of saved calls is executed after the surrounding function returns. Defer is commonly used to simplify functions that perform various clean-up actions.
    defer 语法将函数压栈,这些函数在包围的函数返回之后开始出栈(记住,是包围的函数,不是代码块),defer 通常用于简化执行清理操作的函数

  2. panic is a built-in function that stops the ordinary flow of control and begins panicking. When the function F calls panic, execution of F stops, any deferred functions in F are executed normally, and then F returns to its caller. To the caller, F then behaves like a call to panic. The process continues up the stack until all functions in the current goroutine have returned, at which point the program crashes. Panics can be initiated by invoking panic directly. They can also be caused by runtime errors, such as out-of-bounds array accesses
    panic 是内建函数, 终止原始控制并开始panic,当F函数调用panic时, F函数执行终止,包围F函数的defer函数正常执行,之后执行流返回给上层调用者,对于上层调用者而言,F函数现在就像是panic()。 以上过程持续向上冒泡,知goroutine中所有函数都能返回,这个时候程序崩溃

可以直接调用panic来主动引发panic, 也可能来自运行时的错误引起。

所以原生关键字go虽然很容易起goroutine,但是我们还是要注意goroutine的健壮性,因为单goroutine的panic 会导致整个程序崩溃。

一个比较好的实践是:打印错误和堆栈

func testDefer() *int64 {
	defer func() {
		if err := recover(); err != nil {
			fmt.Println("stacktrace from panic:" + string(debug.Stack()))     // 打印错误堆栈
		}
	}()
	var a int64 = 1
	panic("panic....")          // 这里panic之后, 执行流立即回到defer recover(), return &a 不会得到执行, 那么定义的临时返回变量将是指针的零值, 未被赋值。

	return &a
}

func main() {

	var ss *int64 = testDefer()
	fmt.Printf("ss=%+v\n", ss)
}

注意: testDefert函数内panic之后, 执行流立即回到defer recover(), return &a 不会得到执行, 那么定义的临时返回变量将保持指针的零值, 未被赋值。
output:

stacktrace from panic:goroutine 1 [running]:
runtime/debug.Stack()
        /usr/local/go/src/runtime/debug/stack.go:24 +0x65
main.testDefer.func1()
        /Users/admin/test/test_sync_one/main.go:37 +0x2f
panic({0x10987c0, 0x10c9680})
        /usr/local/go/src/runtime/panic.go:884 +0x213
main.testDefer()
        /Users/admin/test/test_sync_one/main.go:41 +0x65
main.main()
        /Users/admin/test/test_sync_one/main.go:48 +0x2b

ss=<nil>
  1. recover is a built-in function that regains control of a panicking goroutine. Recover is only useful inside deferred functions. During normal execution, a call to recover will return nil and have no other effect. If the current goroutine is panicking, a call to recover will capture the value given to panic and resume normal execution.
    recover 也是一个内建函数,用于(正在panic的goroutine)重新获得控制,recover 只能用在defer函数(这意味着,在常规代码块中调用recover,会返回nil并没有任何效果), 当前goroutine正在panic,调用recover会获得panic时候的参数,并重获执行流。

绝大部分panic都可以通过recover捕获并恢复控制,但runtime 中调用 throw 函数抛出的异常是无法在业务代码中通过 recover()函数 捕获的,这点最为致命(会有print打印到标注输出,这提供了一点蛛丝马迹)。

致命fatal场景,无法捕获并恢复控制的的panic: https://eddycjy.com/posts/go/throw/

  1. 并发读写map: 对于并发读写 map 的地方,应该对 map 加锁。

if one goroutine is writing to a map, no other goroutine should be reading or writing the map concurrently.

  1. 堆栈内存耗尽
  2. 将nil作为goroutine启动
  3. goroutine死锁
  4. 线程限制耗尽, GOLANG 官方定义线程数1w , https://blog.csdn.net/slphahaha/article/details/120984884
  5. 超出可用内存
posted @ 2023-03-12 16:51  博客猿马甲哥  阅读(81)  评论(0编辑  收藏  举报