Go语言当中反人性的基础坑位

从其他语言转到Go语言,往往会有路径依赖;Go语言核心数据结构比较随意的设计风格,初学者往往会陷入很多坑位。

https://go.dev/doc/effective_go golang官方出的快速指南,值得深度。

1. 核心数据结构

  • 值类型包括:所有integer、所有float、bool、string(+rune)、数组和 struct

  • 指针类型

Go是类C语言,只有值类型和指针类型,没有明确的引用类型的概念, 只不过利用strut类型和指针,能做到C#引用类型的效果。

2. 切片slice是结构体

slice是一个结构体,切片s执行unsafe.Sizeof(s) = 24

type slice struct {
	array unsafe.Pointer         // 底层数组指针
	len   int                     // 内容长度
	cap   int                    // 底层数组容量
}


s = make([]byte,5)
当我们对s进一步切片: s =s[2,4]

2.1 append(slice) 不需要显式初始化

slice需要通过make初始化完才可以使用,有一个例外,不初始化就可以使用:append

2.2 sort.Slice([] s) 是原地排序

    var person = []RouteInfo { RouteInfo{},  RouteInfo{} }
	sort.Slice(person, func(i, j int) bool { // 原地降序
		// return person[i].Id > person[j].Id && person[i].Prefix > person[j].Prefix // 这个是做不到的先按照id,再按照Prefix排序的
		if person[i].Id == person[j].Id {
			return person[i].Prefix > person[j].Prefix
		}
		return person[i].Id > person[j].Id
	})

3. map是指针

map 是指向一个hmap结构体的指针,对字典m执行unsafe.Sizeof(m) =8

Like slices, maps hold references to an underlying data structure. If you pass a map to a function that changes the contents of the map, the changes will be visible in the caller.

// A header for a Go map.
type hmap struct {
	// Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/reflect.go.
	// Make sure this stays in sync with the compiler's definition.
	count     int // # live cells == size of map.  Must be first (used by len() builtin)
	flags     uint8
	B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
	noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
	hash0     uint32 // hash seed

	buckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
	oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
	nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)

	extra *mapextra // optional fields
}

3.1 map需要先初始化,才能使用, 否则会panic

可采用字面量或者make关键字初始化

mm := map[string]string{"k1": "v1", "k2": "v2"}  // map字面量
m := make(map[string]float64, 5)
m["pi"] = 3.14

3.2 map中取得不存在的键,会返回零值, 会误导对于该值存在性的判断

func main() {  
   x := map[string]string{"k1":"v1","k2":"v2"}
   if v := x["k3"]; v == "" {         // 不存在该键值对,会返回值类型的零值
       fmt.Println("①字典该key对应值为""  ②字典不存在该key")
   }
}

使用map取值的返回参数2 bool值来判断

func main() {  
    x := map[string]string{"k1":"v1","k2":"v2","k3":""}
    if _,ok := x["k3"]; !ok {
        fmt.Println("字典不存在该key")
    }
}

3.3 对map做for-range输出是随机的, 但是json.Marshal、fmt.Printf是固定排序

说一个题外话: JSON字符串的值可以是null, 当json.Unmarshal反序列化时,null被反序列化为指定字段的零值。

4. channel 是指向hchan结构体的指针

type hchan struct {
	qcount   uint           // 队列中已有的缓存元素的长度
	dataqsiz uint           // 环形队列的长度
	buf      unsafe.Pointer // 环形队列的地址
	elemsize uint16
	closed   uint32
	elemtype *_type // 元素类型
	sendx    uint   // 待发送的元素索引
	recvx    uint   // 待接受元素索引
	recvq    waitq  // 阻塞等待的goroutine
	sendq    waitq  // 阻塞等待的gotoutine
 
	// lock protects all fields in hchan, as well as several
	// fields in sudogs blocked on this channel.
	//
	// Do not change another G's status while holding this lock
	// (in particular, do not ready a G), as this can deadlock
	// with stack shrinking.
	lock mutex
}

5. 数组array 是值类型

将数组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)    // 输出 1,2,3
	}
}

6. 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 带有底层数组指针和长度,但是长度也是只读,是类型安全、内存安全的滑块。

6. struct{} 类型占用空位为0

变量不一定都占用空间,struct{} 类型指向看了一个固定地址, 不开辟空间。

7. string 是不可修改的结构体

在golang中,字符串是utf-8编码的字节序列, 对字符串执行unsafe.Sizeof(s) =16

  type  stringStruct  struct {
          str unsafe.Pointer   // 指向底层字符数组的指针
          len  int                  //  底层数组长度
  }

7.1 for-range 字符串,默认情况下是迭代rune

uft-8 是一种变长编码方式,一个字符可能由一个或多个字节组成。
rune 表示一个unicode字符, 可以方便的处理 多字节字符, 故在for-range 字符串时默认是迭代rune而不是字节

同大多数语言一样,golang的string是不可变的, 可尝试通过byte/rune 中转:

[]byte /[]rune 显式转化为string,最常见和安全的方式,适用于大多数场景:在转换时会创建一个新的字符串,并将字节数组的内容复制到新的字符串中。

7.2 都说string()显式转换[]byte性能比不上 指针方式,那什么时候使用不安全指针方式?

指针方式利用unsafe包来避免数据复制,从而提高性能, 适用于对性能有极高要求且能够接受使用不安全代码的场景。
带来的问题:

1> 内存安全性:

unsafe包的使用会破坏Go语言的内存安全保证。如果[]byte在转换后的string仍在使用时被修改,可能导致数据不一致或者未定义的行为,因为string在Go中应该是不可变的。

2> 数据生命周期:

如果[]byte的底层数组在string仍在使用时被垃圾回收或者被其他数据覆盖,可能导致string指向无效内存区域。这会引发程序崩溃或产生不可预测的行为。

import (
    "reflect"
    "unsafe"
)

func UnsafePointer() {
	var b = []byte{'h', 'e', 'l', 'l', 'o'}
	var s string = *(*string)(unsafe.Pointer(&b))

	fmt.Printf("%s\n", s)    // 这里演示了通过unsafe.pointer 将[]byte 转换为string的效果
	b[0] = 'H'
	fmt.Printf("%s\n", s)

	s = "我们都有一个家" // string 是不可修改的,直接对string赋值实际是新申请空间去存储
	for _, c := range s {  //  这里编译器会提示迭代的元素类型是 rune
		fmt.Printf("%c", c)
	}
	for _, c := range b {
		fmt.Printf("%c", c)
	}
}

-- output
hello
Hello
我们都有一个家Hello

8. 变量

8.1 变量遮蔽

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

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

	fmt.Println(n)     // 块内:=声明的变量, 遮蔽了外层变量,块内的操作对外层无影响  。   0 
}

8.2 可为空类型

golang 没有可为空类型这样的说法, 但是实际业务中我们要体现 null true false 这样的可为空值, 这在序列化时 null值会被忽略KV。

golang 可以使用指针类型达到C#可为空类型的效果:

定义 ControlByJean        *bool    `json:"controlByJean,omitempty" bson:"controlByJean,omitempty" cfg:"controlByJean" web:"健康检查规则由jean控制"`

具体赋值时: 
  t = false
  ControlByJean  = &t  
  ControlByJean  = nil   

8.3 变量类型转换

9. 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.

10.


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  博客猿马甲哥  阅读(102)  评论(1编辑  收藏  举报