Go 语言入门指南:内存拷贝问题 | 青训营

259 阅读3分钟

浅拷贝与深拷贝

浅拷贝: Go 语言中会进行浅拷贝的基本变量类型有 slice map function chan

  • 仅拷贝地址,不复制值
  • 新老对象的值完全同步
  • 同时被垃圾回收

深拷贝: Go 语言中会进行深拷贝的基本变量类型有 bool int float string array struct

  • 复制时创建一个新的对象
  • 新老对象的值不同步
  • 可各自被垃圾回收

重点需要关注的是 arraystructGo 语言中绝大多数的场景下使用的都是切片,因为 array 大小固定不易拓展且易因为复制导致大量内存拷贝。 struct 在复制时也会将其中的每个字段根据类型进行深拷贝与浅拷贝,如果有 array 在结构体中,又会回到 array 复制的问题。

type MessageBody struct {
    statusCode int
    message    string
}

以下测试以该结构体为示例。

func main() {
    mb := MessageBody{}
    fmt.Printf("Hello! %p %p\n", &mb, &mb.statusCode)
    mb.Hello()
}

func (mb MessageBody) Hello() {
    fmt.Printf("Hello! %p %p\n", &mb, &mb.statusCode)
}
// output:
// Hello! 0xc000092060 0xc000092060
// Hello! 0xc000092078 0xc000092078

甚至要注意以结构体为方法接受对象的情况,因为此时也会复制结构体,其中的字段值也是根据类型进行深拷贝与浅拷贝的。

for 与 for range

Go 语言中,for range 语法功能强大, 可以轻松遍历 channel map slice array 等变量。但该方法也存在着对应问题,就是 for range 会复制键/键值对。 Go 语言中结构体是可以相互赋值的,且赋值时对非指针内容是深拷贝,因此 for range 在某些情况下不仅会造成大量的额外开销,对获取到的值进行修改也可能不尽人意。常见会对值进行修改的就有 slicemap

func forRange() {
    msg1, msg2 := MessageBody{200, "OK"}, MessageBody{500, "Server fail"}
    var slice = make([]MessageBody, 0, 2)
    slice = append(slice, msg1, msg2)
    for _, msg := range slice {
        fmt.Printf("%p\t", &msg)
        msg.message = "empty"
    }
    fmt.Println()
    fmt.Printf("%v\t%v\t%v\t\n", slice, msg1, msg2)
    fmt.Printf("%p\t%p\t%p\t%p\n", &slice[0], &slice[1], &msg1, &msg2)
}
// output:
// 0xc0000040a8    0xc0000040a8
// [{200 OK} {500 Server fail}]    {200 OK}        {500 Server fail}
// 0xc00007c480    0xc00007c498    0xc000004078    0xc000004090
  • 第一行输出是 for range 中对两次循环的 msg 进行取地址,可以看出两次代码指向相同空间,即每次遍历时为结构体重新赋值
  • 第二行可以看出无论是切片中的结构体还是原结构体的值都没有发生改变
  • 第三行检测 append 方法与结构体的适应性,可以看出 append 时是深拷贝了一个新的结构体
func hashMap() {
    msg1, msg2 := MessageBody{200, "OK"}, MessageBody{500, "Server fail"}
    var hash = make(map[int]MessageBody, 2)
    hash[0] = msg1
    hash[1] = msg2
    for k, v := range hash {
        fmt.Printf("%p\t%p\n", &k, &v)
        v.message = "empty"
    }
    fmt.Printf("%v\t%v\t%v\t\n", hash, msg1, msg2)
    msg1.message = "empty"
    fmt.Printf("%v\t%v\t\n", hash[0], msg1)
}
// output:
// 0xc000128058    0xc000114060
// 0xc000128058    0xc000114060
// map[0:{200 OK} 1:{500 Server fail}]     {200 OK}        {500 Server fail}
// {200 OK}        {200 empty}

map 可以得出相同结论,for range 用的临时变量还是使用相同地址,即每次遍历是将值复制给了临时变量,对 map 进行赋值时得到的也是复制体。