go陷阱

时间:2023-12-30 23:28:44

必看的题目:https://blog.csdn.net/weiyuefei/article/details/77963810

1、关于值传递、引用传递与指针传递

当一个变量或者新值被创建时, 如果没有为其明确指定初始值,go语言会自动初始化其值为此类型对应的零值, 各类型零值如下:
false : bool,
0: integer
0.0: float
"": string
nil : pointer, function, interface, slice, channel, map

对于复合类型, go语言会自动递归地将每一个元素初始化为其类型对应的零值。比如:数组, 结构体。

nil 是专门为go语言的指针类型和引用类型准备的,go语言的数组和结构体可是值类型, 切片、字典或通道为引用类型。所以如果数组作为函数参数时,因为是值类型, 所以要复制 。

nil 可以用作 interface、function、pointer、map、slice 和 channel 的“空值”

在go语言中没有引用传递,只有值传递。

例1:值传递

func main() {
	a := []string{"a", "b"}
	test(a)
	fmt.Println(a)
}

func test(b []string) {
	b = []string{"cc", "cc"}
}  

打印出来后值为:[a b],表明在传递时是复制了一份单独的数组结构。

指针传递也是值传递。

例2:指针传递,也是值传递

func main() {
	a := &[]string{"a", "b"}
	test(a)
	fmt.Println(*a)
}

func test(b *[]string) {
	b = nil
}

打印出来后为:[a b]。传递指针时,能修改指针所指向的值,并不能修改指针本身的值。

传引用

var a Object
modify(a) // 修改a的值
print(a)

如果函数modify修改a的值, 然后print打印出来的也是修改后的值,那么就可以认为modify是通过引用的方式使用了参数a。而如上例子证明了指明传递是值传递。 

例3:引用传递

func main() {
    a := new(int)
    fmt.Println(a)
    func() {
        a = nil
    }()
    fmt.Println(a)
}

打印结果为:

0xc042008220
<nil>

关于指针传递与引用传递参考:https://studygolang.com/articles/4810

2、关于go中的map

go语言中的map不是协程安全的,如果要多个协程对同 一个map进行写操作,则会出错:

fatal error: concurrent map writes

举例:

func main() {
	Map := make(map[int]int)

	for i := 0; i < 100000; i++ {
		go writeMap(Map, i, i)
		go readMap(Map, i)
	}

}

func readMap(Map map[int]int, key int) int {
	return Map[key]
}

func writeMap(Map map[int]int, key int, value int) {
	Map[key] = value
}

因为map为引用类型,所以即使函数传值调用,参数副本依然指向映射m, 所以N个goroutine并发写同一个映射m,共享资源会遭到破坏。

解决办法就是加锁,或者channel排队串行化。

例2:

type S struct {
    I int
}
func main() {
	m := map[string]S{
	    "a": S{1},
	}
       m["a"].I = 5 // cannot assign to struct field m["a"].I in map
}

对于一个struct值的map,你无法更新单个的struct值。因为只有变量才可以赋值,变量的定义里包括了array和slice的下标表达式,但是不包括map的下标表达式。

go中的map输出是无序的,如下:

m := make(map[int]int)
m[0] = 0
m[1] = 1
m[2] = 2
m[3] = 3
m[4] = 4
for k,v := range m{
	fmt.Printf("map[%d = %d\n",k,v)
}

(4)map中的元素不是变量,因此不能寻址!!

参考:https://blog.csdn.net/erlib/article/details/50963152

3、关于初始化make与new

1、make用于内建类型(map、slice 和channel)的内存分配。new用于各种类型的内存分配。

2、new(T)分配了零值填充的T类型的内存空间,并且返回其地址,即一个*T类型的值。用Go的术语说,它返回了一个指针,指向新分配的类型T的零值。有一点非常重要:new返回指针。

3、make只能创建slice、map和channel,并且返回一个有初始值(非零)的T类型(引用类型),而不是*T。本质来讲,导致这三个内建类型有所不同的原因是:引用在使用前必须被初始化。例如,一个slice,是一个包含指向数据(内部array)的指针、长度和容量的三项描述符;在这些项目被初始化之前,slice为nil。对于slice、map和channel来说,make初始化了内部的数据结构,填充适当的值。make返回初始化后的(非零)值。

例1:

type X struct {
	a int
}

type T struct {
	i int;
	f float64;
	next *T
	x X
	y *X
}

则:

(1)new(T) 结果:&{0 0 <nil> {0} <nil>}  

(2)mySlice1 := make([]T, 1)  结果:[{0 0 <nil> {0} <nil>}]

(3)mySlice1 := make([]T, 2,5) 结果:[{0 0 <nil> {0} <nil>} {0 0 <nil> {0} <nil>}]

(4)mySlice1 := make([]*T, 2,5)  结果: [<nil> <nil>]

(5)t := new(*T) 则*t为nil

(6)t := new([]T) 结果:&[]

(7)t := new([]*T) 结果为:&[]

make在初始化一个切片时,如:

a := make([]int,  5, 10) // 注意这样是初始化了一个切片类型

则第一个参数为len,第二个为cap。长度是指已经被赋过值的最大下标+1,可通过内置函数len()获得。容量是指切片目前可容纳的最多元素个数,可通过内置函数cap()获得  

4、数组Array与切片Slice

数组举例如下:

var arr [5]int
var arr [5]int{1,2,3,4,5}
var arr [...]int{1,2,3,4,5} 

数组只是连续的内存块,如果你去阅读 Go 运行时的源码(src/runtime/malloc.go),你会发现创建一个数组本质上就是分配了一块指定大小的内存。数组元素总是会初始化为指定类型的 零值

切片举例如下:

var foo []int 

切片的数据结构包括 3 个部分 - 指向数组的指针、切片的长度和切片的容量:  

type slice struct {
        array unsafe.Pointer
        len   int
        cap   int
} 

当创建一个新的切片时,Go 运行时会在内存里创建这样一个包含 3 块区域的对象,并且会把数组指针初始化为 nillen 和 cap 初始化为 0。  

可以用 make 来初始化一个指定大小的切片:  

foo = make([]int, 5) 

这段代码会创建一个切片,包含了一个 5 个元素的数组,每个元素的初值为 0,len 和 cap 的初值则为 5。

Cap 是指切片大小可以达到的上限,以便为未来可能的增长留出空间。可以用 make([]int, len, cap) 语法来指定容量。

关于切片与数组需要注意:

(1)如果你要更改切片中某些元素的值,实际上是在改变切片指向的数组元素的值。

举例:

func main() {
	foo := make([]int, 5)
	foo[3] = 42
	foo[4] = 100
	fmt.Println(foo)

	bar  := foo[1:4]
	bar[1] = 99

	fmt.Println(foo)
	fmt.Println(bar)
}

打印结果如下:

[0 0 0 42 100]

[0 0 99 42 100]

[0 99 42]

(2)切片函数append 注意是追加

例2:

func main() {
	s := make([]int, 3)
	s = append(s, 1, 2, 3)
	fmt.Println(s)
}

结果如下:[0 0 0 1 2 3] 

(3) 数组扩容

当切片的容量不够用时,会开辟一个新的数组,然后将原来数组中的值复制到新数组中,这样就改变了切片指向的底层数组:

a := make([]int, 16)
b1 := a[1:8] // 在a[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]上进行切片操作
a = append(a, 1) // 追加元素导致底层的数组变为了新的,而旧的仍然为16个0
a[2] = 42

fmt.Println(a) // [0 0 42 0 0 0 0 0 0 0 0 0 0 0 0 0 1]
b2 := a[1:8] // 在新的上进行切片操作,此时的a为[0 0 42 0 0 0 0 0 0 0 0 0 0 0 0 0 1]
fmt.Println(b1) // [0 0 0 0 0 0 0]
fmt.Println(b2) // [0 42 0 0 0 0 0]

由于切片由于底层是指向数组,所以无法知道这个切片到底耗费了多少内存。

5、for range

转载地址: https://www.cnblogs.com/hetonghai/p/6718250.html

go只提供了一种循环方式,即for循环,在使用时可以像c那样使用,也可以通过for range方式遍历容器类型如数组、切片和映射。但是在使用for range时,如果使用不当,就会出现一些问题,导致程序运行行为不如预期。比如,下面的示例程序将遍历一个切片,并将切片的值当成映射的键和值存入,切片类型是一个int型,映射的类型是键为int型,值为*int,即值是一个地址。

package main

import "fmt"

func main() {
    slice := []int{0, 1, 2, 3}
    myMap := make(map[int]*int)

    for index, value := range slice {
        myMap[index] = &value
    }
    fmt.Println("=====new map=====")
    prtMap(myMap)
}

func prtMap(myMap map[int]*int) {
    for key, value := range myMap {
        fmt.Printf("map[%v]=%v\n", key, *value)
    }
}

运行程序输出如下:  

=====new map=====

map[3]=3
map[0]=3
map[1]=3
map[2]=3
由输出可以知道,不是我们预期的输出,正确输出应该如下:

=====new map=====

map[0]=0
map[1]=1
map[2]=2
map[3]=3

但是由输出可以知道,映射的值都相同且都是3。其实可以猜测映射的值都是同一个地址,遍历到切片的最后一个元素3时,将3写入了该地址,所以导致映射所有值都相同。其实真实原因也是如此,因为for range创建了每个元素的副本,而不是直接返回每个元素的引用,如果使用该值变量的地址作为指向每个元素的指针,就会导致错误,在迭代时,返回的变量是一个迭代过程中根据切片依次赋值的新变量,所以值的地址总是相同的,导致结果不如预期。

package main

import "fmt"

func main() {
    slice := []int{0, 1, 2, 3}
    myMap := make(map[int]*int)

    for index, value := range slice {
        num := value
        myMap[index] = &num
    }
    fmt.Println("=====new map=====")
    prtMap(myMap)
}

func prtMap(myMap map[int]*int) {
    for key, value := range myMap {
        fmt.Printf("map[%v]=%v\n", key, *value)
    }
}

运行程序输出如下:  

=====new map=====

map[2]=2
map[3]=3
map[0]=0
map[1]=1
记住是创建每个元素的副本,所以对于值类型,修改其副本并不能影响原有的值,如下:
type Job struct {
    dsp_id string
    out  chan Job
}
func main()  {

    jobs := []Job{}
    num := 2
    for i:= 0;i<num;i++{
        job := Job{  dsp_id:"i"  }
        jobs = append(jobs,job)
    }

    jobChannel := make(chan Job,num)

    for _,j := range jobs{
        j.out = jobChannel
    }

    for _,j := range jobs{
        fmt.Println(j) // 打印结果没有out值
    }

}  

输出结果为:

{i <nil>}
{i <nil>}

6、defer、return和返回值之间的执行顺序

func main() {
	fmt.Println("return:", a()) // 打印结果为 return: 0
	fmt.Println("return:", b()) // 打印结果为 return: 2
	fmt.Println("c return:", *(c())) // 打印结果为 c return: 2
}

func a() int {
	var i int
	defer func() {
		i++
		fmt.Println("defer2:", i) // 打印结果为 defer: 2
	}()
	defer func() {
		i++
		fmt.Println("defer1:", i) // 打印结果为 defer: 1
	}()
	// 函数的返回值没有被提前声明,其值来自于其他变量的赋值,
	// 而defer中修改的也是其他变量,而非返回值本身,因此函数
	// 退出时返回值并没有被改变。
	return i
}

func b() (i int) {
	defer func() {
		i++
		fmt.Println("defer2:", i) // 打印结果为 defer: 2
	}()
	defer func() {
		i++
		fmt.Println("defer1:", i) // 打印结果为 defer: 1
	}()
	// 函数的返回值被提前声明,也就意味着defer中是可以调用到真实
	// 返回值的,因此defer在return赋值返回值 i 之后,再一次地修改
	// 了 i 的值,最终函数退出后的返回值才会是defer修改过的值。
	return i // 或者直接 return 效果相同
}

func c() *int {
	var i int
	defer func() {
		i++
		fmt.Println("c defer2:", i) // 打印结果为 c defer: 2
	}()
	defer func() {
		i++
		fmt.Println("c defer1:", i) // 打印结果为 c defer: 1
	}()
	// 虽然 c()*int 的返回值没有被提前声明,但是由于 c()*int 的
	// 返回值是指针变量,那么在return将变量 i 的地址赋给返回值后,
	// defer再次修改了 i 在内存中的实际值,因此函数退出时返回值
	// 虽然依旧是原来的指针地址,但是其指向的内存实际值已经被成功修改了。
	return &i
}

需要掌握2个要点:

  • 多个defer的执行顺序为“后进先出”;
  • defer、return、返回值三者的执行逻辑应该是:return最先执行,return负责将结果写入返回值中;接着defer开始执行一些收尾工作;最后函数携带当前返回值退出。

7、一定要理解go的接口与实现类

type People interface {
   Speak(string) string
}

type Stduent struct{}

func (stu *Stduent) Speak(think string) (talk string) {
	if think == "bitch" {
		talk = "You are a good boy"
	} else {
		talk = "hi"
	}
	return
}

func main() {
	var peo People = &Stduent{} // 注意这里必须Student的对象的指针类型
	think := "bitch"
	fmt.Println(peo.Speak(think))
}

  

8、比较结构体类型

// 进行结构体比较时候,只有相同类型的结构体才可以比较,
// 结构体是否相同不但与属性类型个数有关,还与属性顺序相关
func main() {

	sn1 := struct {
		age  int
		name string
	}{age: 11, name: "qq"}
	sn2 := struct {
		age  int
		name string
	}{age: 11, name: "qq"}

	if sn1 == sn2 {
		fmt.Println("sn1 == sn2") // 打印
	}

	sm1 := struct {
		age int
		m   map[string]string
	}{age: 11, m: map[string]string{"a": "1"}}

	sm2 := struct {
		age int
		m   map[string]string
	}{age: 11, m: map[string]string{"a": "1"}}

	// 报错,结构体中有不可比较的类型,如引用类型map与slice
	//if sm1 == sm2 {
	//	fmt.Println("sm1 == sm2")
	//}

	if reflect.DeepEqual(sm1, sm2) {
		fmt.Println("sm1 == sm2") // 打印
	} else {
		fmt.Println("sm1 != sm2")
	}
}

  

9、go字符串的遍历输出

https://blog.csdn.net/benben_2015/article/details/78904860