Golang中切片在复制后操作副本时原切片被修改的问题

Golang中切片在复制后操作副本时原切片被修改的问题 我编写了这段代码用于复制切片,它运行正常,作为参数传递给函数的原始切片没有受到影响:

package main

import "fmt"

type Team []Person
type Person struct {
	Name string
	Age  int
}

func main() {
	team := Team{
		Person{"Hasan", 34}, Person{"Karam", 32},
	}
	fmt.Printf("original before clonning: %v\n", team)
	team_cloned := team.Clone()
	fmt.Printf("original after clonning: %v\n", team)
	fmt.Printf("clones slice: %v\n", team_cloned)
}

func (c *Team) Clone() Team {
	var s = make(Team, len(*c))
	copy(s, *c)
	for index, _ := range s {
		s[index].Name = "change name"
	}
	return s
}

但在我的另一段代码中,即使我向函数传递了原始切片的克隆,原始切片仍然被改变了:

type Inventories []Inventory
type Inventory struct { //instead of: map[string]map[string]Pairs
	Warehouse string
	Item      string
	Batches   Lots
}
type Lots []Lot
type Lot struct {
	Date  time.Time
	Key   string
	Value float64
}

func (c *Inventories) Clone() Inventories {
	var s = make(Inventories, len(*c))
	copy(s, *c)
	return s
}

func (outs Inventories) BuildBatchesFrom(ins Inventories) (batchesBalance Inventories, outgoing Inventories) {
	batchesOut := Inventories{}

	for _, in := range batchesBalance {
		for _, out := range outgoing {
			if out.Warehouse == in.Warehouse && out.Item == in.Item {
				batches := Lots{}
			OUTER:
				for {
					oldestBatch := in.Batches.First()
					batchQty := math.Min(in.Batches.First().Value, math.Abs(out.Batches.First().Value))

					batches = append(batches, Lot{out.Batches.First().Date, oldestBatch.Key, batchQty})

					out.Batches[0].Value = out.Batches.First().Value + batchQty
					in.Batches[0].Value = oldestBatch.Value - batchQty

					if in.Batches.First().Value == 0 {
						in.Batches.PopFirst()
					}
					if out.Batches.First().Value == 0 {
						out.Batches.PopFirst()
						if len(out.Batches) == 0 {
							break
						} else {
							continue OUTER
						}
					} else {
						continue OUTER
					}
				}
				batchesOut = append(batchesOut, Inventory{
					Warehouse: out.Warehouse,
					Item:      out.Item,
					Batches:   batches,
				})
			}
		}
		//os.Exit(3)
	}
	return batchesOut, batchesBalance
} 

func main() {
ins := Inventory{
		Warehouse: "DMM",
		Item:      "Gloves",
		Batches: Lots{
			Lot{mustTime(time.Parse(custom, "1/7/2020")), "Jan", 50},
			Lot{mustTime(time.Parse(custom, "2/1/2020")), "Feb", 70},
		},
	}

outs := Inventory{
		Warehouse: "DMM",
		Item:      "Gloves",
		Batches: Lots{
			Lot{mustTime(time.Parse(custom, "1/5/2020")), "", -10},
			Lot{mustTime(time.Parse(custom, "2/9/2020")), "", -30},
		},
	}

        fmt.Printf("\n\n[1] Ins: \n%v", ins)     // This output is different after running outs_clone.BuildBatchesFrom(ins_clone)
	fmt.Printf("\n\n[2] Outs: \n%v", outs)   //  // This output is different after running outs_clone.BuildBatchesFrom(ins_clone)
	
        ins_clone := ins.Clone()
	outs_clone := outs.Clone()

	batchesOut, batchesBalance := outs_clone.BuildBatchesFrom(ins_clone)

	fmt.Printf("\n\n[1] Ins: \n%v", ins)    // This output is different before running outs_clone.BuildBatchesFrom(ins_clone)
	fmt.Printf("\n\n[2] Outs: \n%v", outs)   // This output is different after running outs_clone.BuildBatchesFrom(ins_clone)

	fmt.Printf("\n\n[4] Batches outs: \n%v", batchesOut)
	fmt.Printf("\n\n[5] Batches Balances: \n%v", batchesBalance)
}

在上面的代码中,ins在运行函数后发生了变化,尽管我并没有将它传递给函数。并且ins_clone()在函数运行后也发生了变化,尽管我在函数代码的第一行对其进行了克隆。outsouts_clone()的情况也是如此。


更多关于Golang中切片在复制后操作副本时原切片被修改的问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html

3 回复

感谢您的回复。实际上,我提供的代码并不完整,它只是文件的一部分,目的是为了避免使其变得复杂,但它包含了相关信息。

更多关于Golang中切片在复制后操作副本时原切片被修改的问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在审阅代码后(尽管由于缺少某些函数而存在一些错误),它是否按预期工作?

我并不期望 BuildBatchesFrom 的行为与 Clone 相同,因为其中包含一些逻辑操作(例如,最小值/最大值处理和按日期排序)。至于 Clone,它是对数组切片的一个清晰的线性复制。

能否请您再次阐明需求?

代码示例:

https://play.golang.org/p/OmpXKlvnwsg

这个问题是因为Go语言中切片和结构体的浅拷贝特性导致的。在你的代码中,Clone()方法只复制了切片本身,但没有复制切片中的元素。

问题分析

  1. 第一个示例能正常工作的原因
func (c *Team) Clone() Team {
    var s = make(Team, len(*c))
    copy(s, *c)  // 这里复制的是Person结构体,结构体是值类型
    for index, _ := range s {
        s[index].Name = "change name"  // 修改的是副本
    }
    return s
}
  1. 第二个示例出问题的原因
func (c *Inventories) Clone() Inventories {
    var s = make(Inventories, len(*c))
    copy(s, *c)  // 这里只复制了Inventory结构体,但Inventory包含Lots切片
    return s
}

根本原因

Inventory结构体中的Batches字段是Lots类型([]Lot切片)。当复制Inventory结构体时,Batches字段被浅拷贝,这意味着新旧结构体共享同一个底层数组。

解决方案

你需要实现深拷贝。以下是修复后的代码:

// 为Lot实现Clone方法
func (l Lot) Clone() Lot {
    return Lot{
        Date:  l.Date,
        Key:   l.Key,
        Value: l.Value,
    }
}

// 为Lots实现Clone方法
func (l Lots) Clone() Lots {
    s := make(Lots, len(l))
    for i, lot := range l {
        s[i] = lot.Clone()
    }
    return s
}

// 为Inventory实现Clone方法
func (i Inventory) Clone() Inventory {
    return Inventory{
        Warehouse: i.Warehouse,
        Item:      i.Item,
        Batches:   i.Batches.Clone(),  // 关键:深拷贝Batches
    }
}

// 为Inventories实现Clone方法
func (c Inventories) Clone() Inventories {
    s := make(Inventories, len(c))
    for i, inv := range c {
        s[i] = inv.Clone()  // 深拷贝每个Inventory
    }
    return s
}

完整示例

package main

import (
    "fmt"
    "time"
)

type Inventories []Inventory

type Inventory struct {
    Warehouse string
    Item      string
    Batches   Lots
}

type Lots []Lot

type Lot struct {
    Date  time.Time
    Key   string
    Value float64
}

// Lot的深拷贝
func (l Lot) Clone() Lot {
    return Lot{
        Date:  l.Date,
        Key:   l.Key,
        Value: l.Value,
    }
}

// Lots的深拷贝
func (l Lots) Clone() Lots {
    s := make(Lots, len(l))
    for i, lot := range l {
        s[i] = lot.Clone()
    }
    return s
}

// Inventory的深拷贝
func (i Inventory) Clone() Inventory {
    return Inventory{
        Warehouse: i.Warehouse,
        Item:      i.Item,
        Batches:   i.Batches.Clone(),
    }
}

// Inventories的深拷贝
func (c Inventories) Clone() Inventories {
    s := make(Inventories, len(c))
    for i, inv := range c {
        s[i] = inv.Clone()
    }
    return s
}

func main() {
    custom := "1/2/2006"
    
    ins := Inventory{
        Warehouse: "DMM",
        Item:      "Gloves",
        Batches: Lots{
            Lot{mustTime(time.Parse(custom, "1/7/2020")), "Jan", 50},
            Lot{mustTime(time.Parse(custom, "2/1/2020")), "Feb", 70},
        },
    }

    outs := Inventory{
        Warehouse: "DMM",
        Item:      "Gloves",
        Batches: Lots{
            Lot{mustTime(time.Parse(custom, "1/5/2020")), "", -10},
            Lot{mustTime(time.Parse(custom, "2/9/2020")), "", -30},
        },
    }

    fmt.Printf("原始ins: %+v\n", ins)
    fmt.Printf("原始outs: %+v\n", outs)
    
    // 创建Inventories切片
    inventories := Inventories{ins, outs}
    
    // 深拷贝
    cloned := inventories.Clone()
    
    // 修改克隆后的数据
    cloned[0].Batches[0].Value = 100
    
    fmt.Printf("\n修改后原始ins: %+v\n", inventories[0])  // Value仍然是50
    fmt.Printf("修改后克隆ins: %+v\n", cloned[0])         // Value变为100
}

func mustTime(t time.Time, err error) time.Time {
    if err != nil {
        panic(err)
    }
    return t
}

关键点

  1. 值类型 vs 引用类型

    • 结构体是值类型,复制时会创建新实例
    • 切片是引用类型,复制时只复制切片头,底层数组共享
  2. 深拷贝原则

    • 对于包含引用类型字段的结构体,需要递归实现深拷贝
    • 每个包含切片或指针的层级都需要实现自己的Clone方法
  3. 性能考虑

    • 深拷贝比浅拷贝消耗更多内存和时间
    • 根据实际需求选择适当的拷贝策略
回到顶部