Go语言 函数传参时的不同

Posted by kzdgt on Monday, May 15, 2023

slice 和 map 分别作为函数参数时有什么区别?

slice 的结构体定义:

// runtime/slice.go
type slice struct {
    array unsafe.Pointer // 元素指针
    len   int // 长度 
    cap   int // 容量
}

结构体内部包含底层的数据指针

创建 slice 实际是调用 makeslice 函数返回的是 Slice 结构体:

func makeslice(et *_type, len, cap int) slice{
    ...
}

在源码中,表示 map 的结构体是 hmap,它是 hashmap 的“缩写”:

// A header for a Go map.
type hmap struct {
    // 元素个数,调用 len(map) 时,直接返回此值
	count     int
	flags     uint8
	// buckets 的对数 log_2
	B         uint8
	// overflow 的 bucket 近似数
	noverflow uint16
	// 计算 key 的哈希的时候会传入哈希函数
	hash0     uint32
    // 指向 buckets 数组,大小为 2^B
    // 如果元素个数为0,就为 nil
	buckets    unsafe.Pointer
	// 等量扩容的时候,buckets 长度和 oldbuckets 相等
	// 双倍扩容的时候,buckets 长度会是 oldbuckets 的两倍
	oldbuckets unsafe.Pointer
	// 指示扩容进度,小于此地址的 buckets 迁移完成
	nevacuate  uintptr
	extra *mapextra // optional fields
}

创建 map 实际是调用makemap 函数,主要做的工作就是初始化 hmap 结构体的各种字段,例如计算 B 的大小,设置哈希种子 hash0 等等。注意,这个函数返回的结果:*hmap,它是一个指针

func makemap(t *maptype, hint int64, h *hmap, bucket unsafe.Pointer) *hmap {
    ...
}

makemap 和 makeslice 的区别,带来一个不同点:当 map 和 slice 作为函数参数时,在函数参数内部对 map 的操作会影响 map 自身;而对 slice 却不会(之前讲 slice 的文章里有讲过)。

主要原因:一个是指针(*hmap),一个是结构体(slice)。Go 语言中的函数传参都是值传递,在函数内部,参数会被 copy 到本地。*hmap指针 copy 完之后,仍然指向同一个 map,因此函数内部对 map 的操作会影响实参。而 slice 被 copy 后,会成为一个新的 slice,对它进行的操作不会影响到实参。但值得注意的是,不管传的是 slice 还是 slice 指针,如果改变了 slice 底层数组的数据,会反应到实参 slice 的底层数据。为什么能改变底层数组的数据?很好理解:底层数据在 slice 结构体里是一个指针,尽管 slice 结构体自身不会被改变,也就是说底层数据地址不会被改变。 但是通过指向底层数据的指针,可以改变切片的底层数据,没有问题

package main

import (
	"fmt"
)

type Cat struct {
	Name string
	Age  int
}

func main() {

	slice := []int{1, 2, 3, 4, 5, 6}
	fmt.Println(slice) //[1 2 3 4 5 6]
	ChangeSlice(slice)
	fmt.Println(slice) //[100 2 3 4 5 6]

	m := make(map[string]string)
	m["aaa"] = "bbb"
	fmt.Println(m) //map[aaa:bbb]
	ChangeMap(m)
	fmt.Println(m) //map[aaa:ccc]

	c1 := Cat{
		Name: "pussy",
		Age:  1,
	}
	fmt.Println(c1) //{pussy 1}
	ChangeStruct(c1)
	fmt.Println(c1) //{pussy 1}

	c2 := &Cat{
		Name: "pussy2",
		Age:  1,
	}
	fmt.Println(c2) //&{pussy2 1}
	ChangePoint(c2)
	fmt.Println(c2) //&{pussy2 2}
}

func ChangeSlice(s []int) {
	s[0] = 100
}

func ChangeMap(m map[string]string) {
	m["aaa"] = "ccc"
}

func ChangeStruct(c Cat) {
	c.Age = 2
}

func ChangePoint(c *Cat) {
	c.Age = 2
}

可能遇到的坑:因为传的是结构体,所以主函数的数组指针不会变化,如果在函数中进行append扩容,那使用的指针已经不是原指针

package main

import "fmt"

func main() {
	a := []int{55, 56, 57, 58, 59}
	ChangeSlice(a)
	// [1 56 57 58 59]  len:5  cap:5  ptr:0xc0000be060
	fmt.Printf("%v  len:%d  cap:%d  ptr:%p\n", a, len(a), cap(a), a)
	fmt.Println("------------------------")
	ChangeSliceP(&a)
	// [2 56 57 58 59 100 101 102 103 104 105 106]  len:12  cap:12  ptr:0xc0000164e0
	fmt.Printf("%v  len:%d  cap:%d  ptr:%p\n", a, len(a), cap(a), a)
}

func ChangeSlice(a []int) {
	a[0] = 1
	// [1 56 57 58 59]  len:5  cap:5  ptr:0xc0000be060
	fmt.Printf("%v  len:%d  cap:%d  ptr:%p\n", a, len(a), cap(a), a)
	a = append(a, 100, 101, 102, 103, 104, 105, 106)
	a[0] = 2
	// [2 56 57 58 59 100 101 102 103 104 105 106]  len:12  cap:12  ptr:0xc0000860c0  append进行扩容,底层数组已经不是ptr:0xc0000be060
	fmt.Printf("%v  len:%d  cap:%d  ptr:%p\n", a, len(a), cap(a), a)
}

func ChangeSliceP(a *[]int) {
	(*a)[0] = 1
	// &[1 56 57 58 59]  len:5  cap:5  ptr:0xc000010450
	fmt.Printf("%v  len:%d  cap:%d  ptr:%p\n", a, len(*a), cap(*a), *a)
	*a = append(*a, 100, 101, 102, 103, 104, 105, 106)
	(*a)[0] = 2
	// &[2 56 57 58 59 100 101 102 103 104 105 106]  len:12  cap:12  ptr:0xc0000164e0
	fmt.Printf("%v  len:%d  cap:%d  ptr:%p\n", a, len(*a), cap(*a), *a)
}

「真诚赞赏,手留余香」

kzdgt Blog

真诚赞赏,手留余香

使用微信扫描二维码完成支付