Go 中slice, map, chan, strcuct 是值传递么?

简介: Go 中slice, map, chan, strcuct 是值传递么?

Slice 也是值传递么?


看个例子吧:

func TestSliceReference(t *testing.T)  {
 var args =  []int64{1,2,3}
 fmt.Printf("切片args的地址: %p\n",args)
 modifiedNumber3(args)
 fmt.Println(args)
}
func modifiedNumber3(args []int64)  {
 fmt.Printf("形参切片的地址 %p \n",args)
 args[0] = 10
}

运行结果:

=== RUN   TestSliceReference
切片args的地址: 0xc000016120
形参切片的地址 0xc000016120 
[10 2 3]
--- PASS: TestSliceReference (0.00s)
PASS

发现没有,切片的地址和形参的地址为啥一样, 难道也问题?不是值传递?上面可以看到,我们并没有用取址符 & 来进行地址转换,就把 slice 打印出来来,再测试一下:

func TestSliceReference2(t *testing.T)  {
 var args =  []int64{1,2,3}
 fmt.Printf("切片args的地址: %p \n",args)
 fmt.Printf("切片args第一个元素的地址: %p \n",&args[0])
 fmt.Printf("直接对切片args取地址%v \n",&args)
 modifiedNumber4(args)
 fmt.Println(args)
}
func modifiedNumber4(args []int64)  {
 fmt.Printf("形参切片的地址 %p \n",args)
 fmt.Printf("形参切片args第一个元素的地址: %p \n",&args[0])
 fmt.Printf("直接对形参切片args取地址%v \n",&args)
 args[0] = 10
}

运行结果

=== RUN   TestSliceReference2
切片args的地址: 0xc0000ee030 
切片args第一个元素的地址: 0xc0000ee030 
直接对切片args取地址&[1 2 3] 
形参切片的地址 0xc0000ee030 
形参切片args第一个元素的地址: 0xc0000ee030 
直接对形参切片args取地址&[1 2 3] 
[10 2 3]
--- PASS: TestSliceReference2 (0.00s)
PASS

可以看到的是 使用 &  地址符进行取址是无效的,而且使用 %p 取出的地址是和第一个元素地址是一样的。为啥这样?看 fmt.Printf 源码

fmt包,print.go中的printValue这个方法,截取重点部分,因为`slice`也是引用类型,所以会进入这个`case`:
case reflect.Ptr:
  // pointer to array or slice or struct? ok at top level
  // but not embedded (avoid loops)
  if depth == 0 && f.Pointer() != 0 {
   switch a := f.Elem(); a.Kind() {
   case reflect.Array, reflect.Slice, reflect.Struct, reflect.Map:
    p.buf.writeByte('&')
    p.printValue(a, verb, depth+1)
    return
   }
  }
  fallthrough
 case reflect.Chan, reflect.Func, reflect.UnsafePointer:
  p.fmtPointer(f, verb)

可以看到 p.buf.writeByte('&')  这个代码打印地址输出结果带有 & , 这个就是为啥

fmt.Printf("切片args的地址: %p \n",args)

打印出来的是一个地址 0xc0000ee030。

为啥打印出来的切片中还会包含 "[]" 呢?

看下 printValue 源码:

case reflect.Array, reflect.Slice:
//省略部分代码
} else {
   p.buf.writeByte('[')
   for i := 0; i < f.Len(); i++ {
    if i > 0 {
     p.buf.writeByte(' ')
    }
    p.printValue(f.Index(i), verb, depth+1)
   }
   p.buf.writeByte(']')
  }

因为递归调用了 p.printValue(a, verb, depth+1) 进行打印, 这个就是为啥

fmt.Printf("直接对切片args取地址%v \n",&args)

输出直接对切片args取地址

&[1 2 3]

还有个问题, 为啥 %p 输出的内存地址 和 slice 结构里面第一个元素的地址是一样的呢?

func (p *pp) fmtPointer(value reflect.Value, verb rune) {
 var u uintptr
 switch value.Kind() {
 case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.Slice, reflect.UnsafePointer:
  u = value.Pointer()
 default:
  p.badVerb(verb)
  return
 }
...... 省略部分代码
// If v's Kind is Slice, the returned pointer is to the first
// element of the slice. If the slice is nil the returned value
// is 0.  If the slice is empty but non-nil the return value is non-zero.
 func (v Value) Pointer() uintptr {
 // TODO: deprecate
 k := v.kind()
 switch k {
 case Chan, Map, Ptr, UnsafePointer:
  return uintptr(v.pointer())
 case Func:
  if v.flag&flagMethod != 0 {
 ....... 省略部分代码

上面有一句话,If v’s Kind is Slice, the returned pointer is to the first。意思是对于 slice 类型,返回的是元素第一个元素的地址,这里正好解释上面为什么 fmt.Printf("切片args的地址:%p \n",args)和fmt.Printf("形参切片的地址 %p \n",args)打印出来的地址是一样的,因为args是引用类型,所以他们都返回slice这个结构里的第一个元素的地址。

Slice 的结构

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

slice 是一个结构体, 第一个元素是指针类型,这个指针指向的是底层数组的第一个指针。所以当 slice 类型的时候, fmt.Printlnf() d打印的是第一个元素的地址。其实也是指针处理,只不过指针的存放的内容是第一个元素的内存地址,但是这个指针的在传递过程中是会拷贝的。

slice 其实也是值传递,为啥引用类型传递可以修改原内容的数据, 因为底层默认传递的指向第一个元素地址的指针。容易混淆的是 fmt.printf() 打印的是第一个这个传递指针对应的内容,而不是存储指针的地址,会给人一种错觉,以为是引用传递。


map 是值传递么?


map 没有明显的指针

func TestMapReference(t *testing.T)  {
 persons:=make(map[string]int)
 persons["asong"]=8
 addr:=&persons
 fmt.Printf("原始map的内存地址是:%p\n",addr)
 modifiedAge(persons)
 fmt.Println("map值被修改了,新值为:",persons)
}
func modifiedAge(person map[string]int)  {
 fmt.Printf("函数里接收到map的内存地址是:%p\n",&person)
 person["asong"]=9
}

运行结果:

=== RUN   TestMapReference
原始map的内存地址是:0xc00000e038
函数里接收到map的内存地址是:0xc00000e040
map值被修改了,新值为: map[asong:9]
--- PASS: TestMapReference (0.00s)
PASS

接着看一下 map 源码

//src/runtime/map.go
// makemap implements Go map creation for make(map[k]v, hint).
// If the compiler has determined that the map or the first bucket
// can be created on the stack, h and/or bucket may be non-nil.
// If h != nil, the map can be created directly in h.
// If h.buckets != nil, bucket pointed to can be used as the first bucket.
func makemap(t *maptype, hint int, h *hmap) *hmap {
 mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
 if overflow || mem > maxAlloc {
  hint = 0
 }
 // initialize Hmap
 if h == nil {
  h = new(hmap)
 }
 h.hash0 = fastrand()

map 使用 make 函数返回的是一个 hmap 类型的指针,func modifiedAge(persons map[string]int) 其实和 func modifiedAge(person *hmap) 意思是一样的,这样实际上传递也是使用了指针的副本进行传递, 属于值传递,  map 也是引用类型,但是传递的类型不是引用,也是值传递,传递的是指针的拷贝。


chan 是值传递么


func TestChanReferencr(t *testing.T) {
 p := make(chan bool)
 fmt.Printf("原始chan的内存地址是:%p\n", &p)
 go func(p chan bool) {
  fmt.Printf("函数里接收到chan的内存地址是:%p\n", &p)
  //模拟耗时
  time.Sleep(2 * time.Second)
  p <- true
 }(p)
 select {
 case l := <-p:
  fmt.Println(l)
 }
}

运行结果

=== RUN   TestChanReferencr
原始chan的内存地址是:0xc000120028
函数里接收到chan的内存地址是:0xc000120030
true
--- PASS: TestChanReferencr (2.00s)
PASS

看到实参和形参地址返回的是不一样的,chan 的底层实现

// src/runtime/chan.go
func makechan(t *chantype, size int) *hchan {
 elem := t.elem
 // compiler checks this but be safe.
 if elem.size >= 1<<16 {
  throw("makechan: invalid channel element type")
 }
 if hchanSize%maxAlign != 0 || elem.align > maxAlign {
  throw("makechan: bad alignment")
 }
 mem, overflow := math.MulUintptr(elem.size, uintptr(size))
 if overflow || mem > maxAlloc-hchanSize || size < 0 {
  panic(plainError("makechan: size out of range"))
 }

其实可以看到 和 map 有点类型,通过 mak 函数,返回的也是一个 hchan 类型的指针,实际上在操作中,传递的是指针的副本。属于值传递。


struct 是值传递么?


看个例子

type Persons struct {
 Name string
 Age  int64
}
func TestStructRerence(t *testing.T) {
 per := Persons{
  Name: "asong",
  Age:  int64(8),
 }
 fmt.Printf("原始struct地址是:%p\n", &per)
 modifiedAge2(per)
 fmt.Println(per)
}
func modifiedAge2(per Persons) {
 fmt.Printf("函数里接收到struct的内存地址是:%p\n", &per)
 per.Age = 10
}

运行结果:

=== RUN   TestStructRerence
原始struct地址是:0xc00000c048
函数里接收到struct的内存地址是:0xc00000c060
{asong 8}
--- PASS: TestStructRerence (0.00s)
PASS

可以看到 struct 就是值传递, 没有指针发现没?当你修改指为10 的时候,发现没有修改成功,原来 struct 中 Age 值,还是 8


总结


Go 语言中的参数传递都是值传递,虽然 Go 语言中都是值传递,但我们还是可以修改原参数中的内容的,因此传递的参数是引用类型。

相关文章
|
12月前
|
Go
go语言中遍历映射(map)
go语言中遍历映射(map)
291 8
|
4月前
|
存储 人工智能 安全
深入理解 go sync.Map - 基本原理
本文介绍了 Go 语言中 `map` 在并发使用时的常见问题及其解决方案,重点对比了 `sync.Mutex`、`sync.RWMutex` 和 `sync.Map` 的性能差异及适用场景。文章指出,普通 `map` 不支持并发读写,容易引发错误;而 `sync.Map` 通过原子操作和优化设计,在某些场景下能显著提升性能。同时详细讲解了 `sync.Map` 的基本用法及其适合的应用环境,如读多写少或不同 goroutine 操作不同键的场景。
204 1
|
6月前
|
存储 安全 Go
Map的遍历与判断键是否存在-《Go语言实战指南》
本文介绍了 Go 语言中对 `map` 的常见操作,包括遍历所有项和判断键是否存在。通过 `for range` 可以遍历 `map` 的键值对、仅键或仅值(需忽略键)。注意,`map` 遍历顺序是随机的。判断键是否存在时,使用双赋值语法 `value, ok := map[key]`,其中 `ok` 表示键是否存在。直接访问不存在的键会返回类型的零值,可能导致逻辑错误。掌握这些机制可更安全高效地处理键值对数据。
|
6月前
|
人工智能 Go
[go]Slice 切片原理
本文详细介绍了Go语言中的切片(slice)数据结构,包括其定义、创建方式、扩容机制及常见操作。切片是一种动态数组,依托底层数组实现,具有灵活的扩容和传递特性。文章解析了切片的内部结构(包含指向底层数组的指针、长度和容量),并探讨了通过`make`创建切片、基于数组生成切片以及切片扩容的规则。此外,还分析了`append`函数的工作原理及其可能引发的扩容问题,以及切片拷贝时需要注意的细节。最后,通过典型面试题深入讲解了切片在函数间传递时的行为特点,帮助读者更好地理解和使用Go语言中的切片。
187 0
|
9月前
|
存储 缓存 安全
Go 语言中的 Sync.Map 详解:并发安全的 Map 实现
`sync.Map` 是 Go 语言中用于并发安全操作的 Map 实现,适用于读多写少的场景。它通过两个底层 Map(`read` 和 `dirty`)实现读写分离,提供高效的读性能。主要方法包括 `Store`、`Load`、`Delete` 等。在大量写入时性能可能下降,需谨慎选择使用场景。
|
10月前
|
存储 安全 Go
Go语言中的map数据结构是如何实现的?
Go 语言中的 `map` 是基于哈希表实现的键值对数据结构,支持快速查找、插入和删除操作。其原理涉及哈希函数、桶(Bucket)、动态扩容和哈希冲突处理等关键机制,平均时间复杂度为 O(1)。为了确保线程安全,Go 提供了 `sync.Map` 类型,通过分段锁实现并发访问的安全性。示例代码展示了如何使用自定义结构体和切片模拟 `map` 功能,以及如何使用 `sync.Map` 进行线程安全的操作。
286 9
|
11月前
|
Go
go语言for遍历映射(map)
go语言for遍历映射(map)
408 12
|
12月前
|
存储 Go
go语言 遍历映射(map)
go语言 遍历映射(map)
341 2
|
Go 定位技术 索引
Go 语言Map(集合) | 19
Go 语言Map(集合) | 19
|
Go
Go语言切片深入解析:按值传递的奥秘
【10月更文挑战第27天】
160 0

热门文章

最新文章