Golang中修改原始map的副本导致原map被改变的问题

Golang中修改原始map的副本导致原map被改变的问题 我提供了一个Go Playground示例:

package main

import "fmt"

type Service struct {
	name      string
	namespace string
	endpoints []string
	mappings  map[string]string
}

func main() {
	svc := Service{
		name:      "nginx",
		namespace: "default",
		endpoints: []string{
			"192.168.0.1",
			"192.168.0.2",
		},
		mappings: map[string]string{
			"pod-0": "192.168.0.1",
			"pod-1": "192.168.0.2",
		},
	}

	nginx := svc
	nginx.namespace = "ingress"
	nginx.endpoints = append(nginx.endpoints, "192.168.0.40")
	nginx.mappings["pod-2"] = "192.168.0.40"
	fmt.Printf("updated: %+v\noriginal: %+v\n", nginx, svc)
}

我创建了一个 svc 的副本 nginx,但当我修改这个副本的 mapping 部分时,我注意到原始项 svc 中的 mapping 也被改变了!

我猜测原因是 nginx 复制了 svc 中使用的 mapping地址,而不是

我需要:

  1. 理解这是如何以及为何发生的
  2. 如何避免这种情况,并确保更改副本/克隆不会影响原始对象

更多关于Golang中修改原始map的副本导致原map被改变的问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html

3 回复

非常感谢,十分感激。

更多关于Golang中修改原始map的副本导致原map被改变的问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


可以将 map 想象成这样:

typeap struct {
    data *mapData
}

type mapData struct {
    // contains filtered or unexported fields
}

当你复制一个 map 时,它确实是一个副本。只是它包含一个指针,就像复制一个切片仍然引用相同的元素一样。

如果你想将 Servicemappings 复制到它自己的 map 中,那么你需要编写自己的 CopyClone 函数。也许下面这样可行:

type Service struct {
	name      string
	namespace string
	endpoints []string
	mappings  map[string]string
}

func (s *Service) Clone() *Service {
	s2 := *s // "future-proofing" in case addl. fields added.
	s2.endpoints = make([]string, len(s.endpoints))
	copy(s2.endpoints, s.endpoints)
	s2.mappings = make(map[string]string, len(s.mappings))
	for k, v := range s.mappings {
		s2.mappings[k] = v
	}
	return &s2

这是一个典型的浅拷贝问题。在Go中,当你将一个结构体赋值给另一个变量时,会进行值拷贝,但对于引用类型字段(如slice、map、指针等),拷贝的是引用而不是底层数据。

问题分析

nginx := svc  // 这是浅拷贝

这行代码执行了以下操作:

  • 基本类型字段(name、namespace)被完整拷贝
  • 切片字段(endpoints)拷贝了切片头(指针、长度、容量),但底层数组共享
  • 映射字段(mappings)拷贝了映射描述符,但底层哈希表共享

解决方案

1. 手动深拷贝(推荐)

func deepCopyService(s Service) Service {
    // 拷贝切片
    endpointsCopy := make([]string, len(s.endpoints))
    copy(endpointsCopy, s.endpoints)
    
    // 拷贝映射
    mappingsCopy := make(map[string]string, len(s.mappings))
    for k, v := range s.mappings {
        mappingsCopy[k] = v
    }
    
    return Service{
        name:      s.name,
        namespace: s.namespace,
        endpoints: endpointsCopy,
        mappings:  mappingsCopy,
    }
}

func main() {
    svc := Service{
        name:      "nginx",
        namespace: "default",
        endpoints: []string{"192.168.0.1", "192.168.0.2"},
        mappings:  map[string]string{"pod-0": "192.168.0.1", "pod-1": "192.168.0.2"},
    }
    
    nginx := deepCopyService(svc)
    nginx.namespace = "ingress"
    nginx.endpoints = append(nginx.endpoints, "192.168.0.40")
    nginx.mappings["pod-2"] = "192.168.0.40"
    
    fmt.Printf("updated: %+v\noriginal: %+v\n", nginx, svc)
}

2. 使用序列化/反序列化

import "encoding/json"

func deepCopyViaJSON(s Service) (Service, error) {
    var copy Service
    data, err := json.Marshal(s)
    if err != nil {
        return Service{}, err
    }
    err = json.Unmarshal(data, &copy)
    return copy, err
}

// 使用示例
nginx, err := deepCopyViaJSON(svc)
if err != nil {
    // 处理错误
}

3. 使用第三方库

import "github.com/jinzhu/copier"

func main() {
    svc := Service{
        name:      "nginx",
        namespace: "default",
        endpoints: []string{"192.168.0.1", "192.168.0.2"},
        mappings:  map[string]string{"pod-0": "192.168.0.1", "pod-1": "192.168.0.2"},
    }
    
    var nginx Service
    // 使用DeepCopy选项
    copier.CopyWithOption(&nginx, &svc, copier.Option{DeepCopy: true})
    
    nginx.namespace = "ingress"
    nginx.endpoints = append(nginx.endpoints, "192.168.0.40")
    nginx.mappings["pod-2"] = "192.168.0.40"
    
    fmt.Printf("updated: %+v\noriginal: %+v\n", nginx, svc)
}

4. 为结构体实现Clone方法

func (s Service) Clone() Service {
    endpoints := make([]string, len(s.endpoints))
    copy(endpoints, s.endpoints)
    
    mappings := make(map[string]string, len(s.mappings))
    for k, v := range s.mappings {
        mappings[k] = v
    }
    
    return Service{
        name:      s.name,
        namespace: s.namespace,
        endpoints: endpoints,
        mappings:  mappings,
    }
}

// 使用示例
nginx := svc.Clone()

关键点

  1. 切片make([]T, len(src)) + copy() 创建新切片
  2. 映射make(map[K]V, len(src)) + 循环拷贝键值对
  3. 性能考虑:对于大型结构体,深拷贝有性能开销
  4. 嵌套结构:如果结构体包含嵌套的引用类型,需要递归处理

在你的具体场景中,手动实现深拷贝是最清晰和高效的方式。

回到顶部