# Go 里的 copy
在 Go 程序里,从变量赋值,到函数传参,对象的复制随处可见,用过其他语言的话,可能会由于惯性思维踩到 Go 对象复制的坑,例如:
-
# 变量赋值
在 Java 里类型分为基本类型和引用类型,对于基础类型的变量在赋值给另一变量时,实际是对该变量做了一次 copy,点在 Golang 中也不例外。然而对于非基础类型的变量,在赋值给另一变量时,在 Java 中实际上是 copy 了对象的引用,对新的变量的任何修改都会反映到原变量上,例如:
public class Test { | |
private static class Person { | |
public String name; | |
public int age; | |
public Person(String name, int age) { | |
this.name = name; | |
this.age = age; | |
} | |
} | |
public static void main(String[] args) { | |
Person a = new Person("one", 1); | |
Person b = a; | |
b.age = 10; | |
System.out.println("person a's age is: " + a.age); | |
} | |
} |
❯ java ./Test.java
person a's age is: 10
但是在 Go 里,当把一个复杂对象变量赋值给另一个变量时,实际上是做了一次浅拷贝,所以当修改新变量的非指针和引用类型字段时,原变量是不会被影响的,例如:
package main
import (
"fmt"
)
type House struct {
Address string
}
type Person struct {
Name string
Age int
House House
}
func main() {
a := Person{Name: "one", Age: 1, House: House{Address: "here"}}
b := a
b.Age = 10
fmt.Printf("person a's age is %d\n", a.Age)
b.House.Address = "there"
fmt.Printf("person a's address is '%s'", a.House.Address)
}
❯ go run .\main.go
person a's age is 1
person a's address is 'here'
注意到修改变量 b 的 House 字段并没有影响变量 a 的 House,这是因为 House 字段是值类型。在 Go 里除了指针、slice、map, channel、interface 之外的类型其实都是值类型,所以 House 字段和其他所有基础类型一样,拷贝时是将整个对象进行拷贝 (即值拷贝),而不像 Java 那样仅仅拷贝对象的引用。
-
# 函数传参
在 Go 中函数传参实际都会发生变量的复制,即使是引用类型也不例外,并没有传引用的概念,这点要和 Java 区分开。所以在 Go 函数里修改非引用类型参数并不会改变原始参数的值,但是对于引用类型,实际是隐式传入了变量的指针,所以对于这类参数的的修改,就像 Java 那样会改变原始参数的值。Go 函数参数传值的规范更准确的说是只针对数据结构中固定的部分传值,例如传递 slice 时,仅仅是对 slice 结构体做了浅拷贝,其间接指向的内存并不会被复制,因此如果通过一些 hack 的方式修改传入函数的 slice 结构的 len 字段时,并不会改变原始实参 slice 的 len, 这一点得特别注意,如果将 slice 传入函数,在函数里对其扩容,原 slice 并不会改变,这也是为什么 append 函数会返回一个新的 slice。
package main
import (
"fmt"
"unsafe"
)
type slice struct {
array unsafe.Pointer
len int
cap int
}
func changeSliceLength(buf []int) {
fmt.Printf("length of buf: %d\n", len(buf))
ptr := (*slice)(unsafe.Pointer(&buf))
ptr.len = 100
fmt.Printf("length of buf: %d\n", len(buf))
buf[0] = 1
}
func main() {
a := make([]int, 5, 10)
fmt.Printf("a[0]: %d, length of a: %d\n", a[0], len(a))
changeSliceLength(a)
fmt.Printf("a[0]: %d, length of a: %d\n", a[0], len(a))
}
❯ go run .\main.go
a[0]: 0, length of a: 5
length of buf: 5
length of buf: 100
a[0]: 1, length of a: 5
# Go 里如何 deepcopy
在写 Go 程序时,难免面遇到需要深拷贝的场景,对于没有指针或者引用类型的结构,我们仅仅需要将该对象赋值给另一个变量就能轻易实现,但实际项目中,复杂结构免不了有指针类型或者引用类型字段,而 Go 里又没有像 Java 里的 Cloneable,这个时候如何实现深拷贝?
-
# 基于序列化 / 反序列化实现深拷贝
简单地说,我们可以先把对象序列化为字符串,然后再从字符串反序列化为对象,这就完成了一次深拷贝。序列化 / 反序列化可以选择 json、thrift、protobuf 等库,这里以 json 为例:
package main
import (
"fmt"
"encoding/json"
)
type House struct {
Address string `json:"address"`
}
type Person struct {
Name string `json:"name"`
House *House `json:"house"`
}
func main() {
a := &Person{Name: "one", House: &House{Address: "one's house"}}
as, _ := json.Marshal(a)
var b Person
json.Unmarshal(as, &b)
b.House.Address = "two's house"
fmt.Printf("a's address: %s, b's address: %s\n", a.House.Address, b.House.Address)
}
❯ go run .\main.go
a's address: one's house, b's address: two's house
采用这种方式进行深拷贝需要注意,由于会先序列化为字符串,因此会额外占用内存,当对象比较大时,需要格外留意内存占用情况,即使是 thrift 和 protobuf 这样的压缩比很高的库,也是存在风险的。另外在整个序列化和反序列化过程中,有可能由于频繁创建对象、申请内存,导致 cpu 占用也跟着升高!!!
-
# 使用 reflect 进行深拷贝
事实上,json 和 protobuf 在序列化和反序列化时,使用了 reflect 库获取对象的字段和类型,那么我们也可以直接使用 reflect 来递归的读取对象的字段,并进行 copy,这样就减少了序列化带来的内存占用,以及频繁字节数组扩容带来的 cpu 占用上升问题,并且可以完全去除反序列化的过程。不过这种方式的问题是,Go 的 reflect.Value 对象不能复用,即使是相同类型的两个不同变量,也需要重新创建 reflect.Value 对象,这也造成 reflect 性能很差。
package main
import (
"fmt"
"reflect"
)
type House struct {
Address string `json:"address"`
}
type Person struct {
Name string `json:"name"`
House *House `json:"house"`
}
func shallowCopyValue(v reflect.Value) reflect.Value {
switch v.Type().Kind() {
case reflect.Ptr:
return shallowCopyValue(v.Elem()).Addr()
case reflect.Interface:
return shallowCopyValue(v.Elem())
case reflect.Array, reflect.Map, reflect.Func, reflect.Chan:
return v
default:
return reflect.New(v.Type()).Elem()
}
}
func deepcopyValue(v reflect.Value) reflect.Value {
var value reflect.Value
if v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface {
value = v.Elem()
} else {
value = v
}
newIns := shallowCopyValue(v)
newValue := newIns
if v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface {
newValue = newIns.Elem()
}
for i := 0; value.Kind() == reflect.Struct && i < value.Type().NumField(); i++ {
field := value.Field(i)
if !field.IsValid() {
continue
}
newValue.Field(i).Set(deepcopyValue(field))
}
return newIns
}
func Deepcopy(ins interface{}) interface{} {
if nil == ins {
return nil
}
return deepcopyValue(reflect.ValueOf(ins)).Interface()
}
func main() {
a := &Person{Name: "one", House: &House{Address: "one's house"}}
b := Deepcopy(a).(*Person)
b.House.Address = "two's house"
fmt.Printf("a's address: %s, b's address: %s\n", a.House.Address, b.House.Address)
}
❯ go run main.go
a's address: one's house, b's address: two's house
-
# 在编译期根据对象结构生成相应的复制代码
一般的,我们深拷贝一个简单对象,可以直接 new 一个新对象,然后对每个字段单独赋值来达到深拷贝的目的,这种方式相比前两种方式资源占用会更少,但是这种方式对于字段很多、层次很复杂的结构来说,要写的语句很多,而且实际开发中,任何结构都会不断变化,这种方式不能应对时刻变化的需求,如果能做到根据结构的定义,动态生成每个字段的复制代码,那不就是理想的深拷贝方法吗。幸运的是 Go 的标准库提供了对 go 文件语法解析的库 go/ast
,借助这个库我们可以在编译期递归解析代码里的结构体,生成每个字段的复制语句.
# go/ast 包生成抽象语法树
先来看一个读取并解析文件的例子
// main.go
package main
import (
"go/token"
"go/parser"
"go/ast"
"log"
)
func main() {
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "person.go", nil, parser.SpuriousErrors)
if nil != err {
log.Fatal(err)
}
ast.Print(fset, f)
}
我们在这里引入的 go/token
包定义了 Go 语言的词法标记和其对应的基础操作,而 go/parser
包是 Go 源码的解析器。这里我们使用 ParseFile 方法解析 person.go 文件,其返回一个 AST 对象。随后我们使用 ast 包提供的 Print 方法打印整个抽象语法树。
// person.go
package main
type Home struct {
Address string
}
type Person struct {
Name string
Age int
Home *Home
}
❯ go run ./main.go
0 *ast.File {
1 . Package: person.go:1:1
2 . Name: *ast.Ident {
3 . . NamePos: person.go:1:9
4 . . Name: "main"
5 . }
6 . Decls: []ast.Decl (len = 2) {
7 . . 0: *ast.GenDecl {
8 . . . TokPos: person.go:3:1
9 . . . Tok: type
10 . . . Lparen: -
11 . . . Specs: []ast.Spec (len = 1) {
12 . . . . 0: *ast.TypeSpec {
13 . . . . . Name: *ast.Ident {
14 . . . . . . NamePos: person.go:3:6
15 . . . . . . Name: "Home"
16 . . . . . . Obj: *ast.Object {
17 . . . . . . . Kind: type
18 . . . . . . . Name: "Home"
19 . . . . . . . Decl: *(obj @ 12)
20 . . . . . . }
21 . . . . . }
22 . . . . . Assign: -
23 . . . . . Type: *ast.StructType {
24 . . . . . . Struct: person.go:3:11
25 . . . . . . Fields: *ast.FieldList {
26 . . . . . . . Opening: person.go:3:18
27 . . . . . . . List: []*ast.Field (len = 1) {
28 . . . . . . . . 0: *ast.Field {
29 . . . . . . . . . Names: []*ast.Ident (len = 1) {
30 . . . . . . . . . . 0: *ast.Ident {
31 . . . . . . . . . . . NamePos: person.go:4:2
32 . . . . . . . . . . . Name: "Address"
33 . . . . . . . . . . . Obj: *ast.Object {
34 . . . . . . . . . . . . Kind: var
35 . . . . . . . . . . . . Name: "Address"
36 . . . . . . . . . . . . Decl: *(obj @ 28)
37 . . . . . . . . . . . }
38 . . . . . . . . . . }
39 . . . . . . . . . }
40 . . . . . . . . . Type: *ast.Ident {
41 . . . . . . . . . . NamePos: person.go:4:10
42 . . . . . . . . . . Name: "string"
43 . . . . . . . . . }
44 . . . . . . . . }
45 . . . . . . . }
46 . . . . . . . Closing: person.go:5:1
47 . . . . . . }
48 . . . . . . Incomplete: false
49 . . . . . }
50 . . . . }
51 . . . }
52 . . . Rparen: -
53 . . }
54 . . 1: *ast.GenDecl {
55 . . . TokPos: person.go:7:1
56 . . . Tok: type
57 . . . Lparen: -
58 . . . Specs: []ast.Spec (len = 1) {
59 . . . . 0: *ast.TypeSpec {
60 . . . . . Name: *ast.Ident {
61 . . . . . . NamePos: person.go:7:6
62 . . . . . . Name: "Person"
63 . . . . . . Obj: *ast.Object {
64 . . . . . . . Kind: type
65 . . . . . . . Name: "Person"
66 . . . . . . . Decl: *(obj @ 59)
67 . . . . . . }
68 . . . . . }
69 . . . . . Assign: -
70 . . . . . Type: *ast.StructType {
71 . . . . . . Struct: person.go:7:13
72 . . . . . . Fields: *ast.FieldList {
73 . . . . . . . Opening: person.go:7:20
74 . . . . . . . List: []*ast.Field (len = 3) {
75 . . . . . . . . 0: *ast.Field {
76 . . . . . . . . . Names: []*ast.Ident (len = 1) {
77 . . . . . . . . . . 0: *ast.Ident {
78 . . . . . . . . . . . NamePos: person.go:8:2
79 . . . . . . . . . . . Name: "Name"
80 . . . . . . . . . . . Obj: *ast.Object {
81 . . . . . . . . . . . . Kind: var
82 . . . . . . . . . . . . Name: "Name"
83 . . . . . . . . . . . . Decl: *(obj @ 75)
84 . . . . . . . . . . . }
85 . . . . . . . . . . }
86 . . . . . . . . . }
87 . . . . . . . . . Type: *ast.Ident {
88 . . . . . . . . . . NamePos: person.go:8:7
89 . . . . . . . . . . Name: "string"
90 . . . . . . . . . }
91 . . . . . . . . }
92 . . . . . . . . 1: *ast.Field {
93 . . . . . . . . . Names: []*ast.Ident (len = 1) {
94 . . . . . . . . . . 0: *ast.Ident {
95 . . . . . . . . . . . NamePos: person.go:9:2
96 . . . . . . . . . . . Name: "Age"
97 . . . . . . . . . . . Obj: *ast.Object {
98 . . . . . . . . . . . . Kind: var
99 . . . . . . . . . . . . Name: "Age"
100 . . . . . . . . . . . . Decl: *(obj @ 92)
101 . . . . . . . . . . . }
102 . . . . . . . . . . }
103 . . . . . . . . . }
104 . . . . . . . . . Type: *ast.Ident {
105 . . . . . . . . . . NamePos: person.go:9:7
106 . . . . . . . . . . Name: "int"
107 . . . . . . . . . }
108 . . . . . . . . }
109 . . . . . . . . 2: *ast.Field {
110 . . . . . . . . . Names: []*ast.Ident (len = 1) {
111 . . . . . . . . . . 0: *ast.Ident {
112 . . . . . . . . . . . NamePos: person.go:10:2
113 . . . . . . . . . . . Name: "Home"
114 . . . . . . . . . . . Obj: *ast.Object {
115 . . . . . . . . . . . . Kind: var
116 . . . . . . . . . . . . Name: "Home"
117 . . . . . . . . . . . . Decl: *(obj @ 109)
118 . . . . . . . . . . . }
119 . . . . . . . . . . }
120 . . . . . . . . . }
121 . . . . . . . . . Type: *ast.StarExpr {
122 . . . . . . . . . . Star: person.go:10:7
123 . . . . . . . . . . X: *ast.Ident {
124 . . . . . . . . . . . NamePos: person.go:10:8
125 . . . . . . . . . . . Name: "Home"
126 . . . . . . . . . . . Obj: *(obj @ 16)
127 . . . . . . . . . . }
128 . . . . . . . . . }
129 . . . . . . . . }
130 . . . . . . . }
131 . . . . . . . Closing: person.go:11:1
132 . . . . . . }
133 . . . . . . Incomplete: false
134 . . . . . }
135 . . . . }
136 . . . }
137 . . . Rparen: -
138 . . }
139 . }
140 . Scope: *ast.Scope {
141 . . Objects: map[string]*ast.Object (len = 2) {
142 . . . "Person": *(obj @ 63)
143 . . . "Home": *(obj @ 16)
144 . . }
145 . }
146 . Unresolved: []*ast.Ident (len = 3) {
147 . . 0: *(obj @ 40)
148 . . 1: *(obj @ 87)
149 . . 2: *(obj @ 104)
150 . }
151 }
通过与 person.go 源码比对,Package 字段即源文件中的 package 关键字,Name 字段即包名,而 Decls 字段则存放了源码中所有的结构体定义。Decls 是一个 ast.Decl 类型的数组,go AST 中所有声明节点都实现了 Decl 接口,因此我们在解析源码中所有结构体或类型定义时,只需要遍历 Decls 字段即可。
注意到 Decl 结构的 Specs 字段正是类型定义,他有两个关键字段:Name 表示类型的名称,Type 表示实际类型,在这里,Type 是 * ast.StructType 类型,这与我们声明中的 struct 对应,事实上 Type 字段也可能是其他类型,如:
- FuncType 表示函数类型
- InterfaceType 表示接口类型
- ArrayType 表示数组或 Slice
- MapType 表示 map
- ChanType 表示 Channel
- Ident 表示标识符,可以表示变量名,或者基础类型如 int,string
- ParenExpr 表示括号包裹的表达式
- SelectorExpr 表示字段选择器,如 a.field
- StarExpr 表示指针
其中 StructType 的 Fields 字段即结构体的字段列表,他是 Field 类型的数组。Field 包括两个关键字段, Names
[]*Ident 类型,表示字段名, Type
Expr 类型,表示字段类型。
从上面的例子,我们对 go AST 结构有了一个大概印象,同时可以总结出通过 AST 获得结构体字段列表的方法,接下来我们以 Person 结构举例,实际生成他的深拷贝方法看看。
# 深拷贝代码生成
拷贝一个对象,重点是拷贝对象的每一个字段,通过遍历 AST 树,我们可以获得 Struct 的字段列表,进一步生成每一个字段的复制代码。go/ast 包提供了 func Walk(v Visitor, node Node)
方法遍历 AST 树。Walk 方法需要两个参数,第一个参数需要实现 Visitor 接口,第二个参数即 AST 的节点。Walk 方法深度优先的调用 v.Visit (node),如果 v.Visit (node) 返回了一个非 nil 的 Visitor 对象 w,则 Walk 会对 node 的所有子节点调用 w.Visit 方法。
遍历 AST 的第一步,我们需要找到所有的 Struct 节点,而所有的结构体定义都在 Decls 列表里,所以首先我们需要实现:
- FileVisitor 从 AST 根节点 ast.File 对象读取 Decls 字段,返回 DeclVisitor
- DeclVisitor 针对 Decls 列表的每一个元素返回 TypeVisitor
- TypeVisitor 识别 type 关键字声明的类型节点
type FileVisitor int
func (v FileVisitor) Visit(node ast.Node) ast.Visitor {
if _, ok := node.(*ast.File); ok {
return DeclVisitor(0)
}
return nil
}
type DeclVisitor int
func (v DeclVisitor) Visit(node ast.Node) ast.Visitor {
// GenDecl.Tok字段表示声明的类型,type字段定义的类型都是token.TYPE
if decl, ok := node.(*ast.GenDecl); ok && decl.Tok == token.TYPE {
return TypeVisitor(0)
}
return nil
}
type TypeVisitor int
func (v TypeVisitor) Visit(node ast.Node) ast.Visitor {
if spec, ok := node.(*ast.TypeSpec); ok {
switch spec.Type.(type) {
case *ast.StructType:
generateStructCloneMethod(spec)
}
}
return nil
}
在 TypeVisitor 中,我们通过判断节点的 Type 字段类型是否为 * ast.StructType 来识别结构体定义节点。我们利用 generateStructCloneMethod 来给一个结构体生成 Clone 方法做深拷贝。我们的目标 Clone 方法应当具有如下基本结构:
func (t *<StructName>) Clone() *<StructName> {
ins := new <StructName>()
ins.<Field> = t.<Field>
ins.<StructField> = t.<StructField>.Clone()
...
return ins
}
可以看出,我们只要获取到结构体名称,和字段名列表,就可以根据以上模板生成Clone方法。
```golang
const template = `
func (t *<StructName>) Clone() *<StructName> {
ins := new(<StructName>)
<Fields>
return ins
}`
func generateStructCloneMethod(spec *ast.TypeSpec) string {
method := strings.ReplaceAll(template, "<StructName>", spec.Name.Name)
var fields bytes.Buffer
for _, field := range (spec.Type.(*ast.StructType)).Fields.List {
switch st := field.Type.(type) {
case *ast.Ident:
fields.WriteString(cloneIdentField(field.Names[0].Name, st))
case *ast.StarExpr:
switch id := st.X.(type) {
case *ast.Ident:
fields.WriteString(cloneIdentField(field.Names[0].Name, id))
}
}
}
return strings.ReplaceAll(method, "<Fields>", fields.String())
}
func cloneIdentField(fieldName string, st *ast.Ident) string {
var fieldTmp string
if isBaseType(st.Name) {
fieldTmp = "\tins.<FieldName> = t.<FieldName>\n"
} else {
fieldTmp = "\tins.<FieldName> = t.<FieldName>.Clone()\n"
}
return strings.ReplaceAll(fieldTmp, "<FieldName>", fieldName)
}
func isBaseType(tp string) bool {
switch tp {
case "string", "int", "int8", "int16", "int32", "int64",
"uint", "uint8", "uint16", "uint32", "uint64",
"float32", "float64", "bool", "rune", "byte":
return true
}
return false
}
```
这里我们在处理非基础类型时假定该字段实现了Clone方法,这样不用去读取该字段原始定义。运行程序后,输出如下:
func (t *Home) Clone() *Home {
ins := new(Home)
ins.Address = t.Address
return ins
}
func (t *Person) Clone() *Person {
ins := new(Person)
ins.Name = t.Name
ins.Age = t.Age
ins.Home = t.Home.Clone()
return ins
}
我们之后只需要将程序输出追加到person.go文件即可。来看实际使用生成的Clone方法进行深拷贝的效果:
```golang
func main() {
personA := &Person{
Name: "A",
Age: 10,
Home: &Home{
Address: "A's home",
},
}
personB := personA.Clone()
personB.Home.Address = "B's home"
fmt.Printf("person a: %+v, home: %+v\n", personA, personA.Home)
fmt.Printf("person b: %+v, home: %+v\n", personB, personB.Home)
}
```
❯ go run person.go
person a: &{Name:A Age:10 Home:0xc000038240}, home: &{Address:A's home}
person b: &{Name:A Age:10 Home:0xc000038250}, home: &{Address:B's home}
这里Person和Home结构都比较简单,所以只考虑了基础类型和指针类型的拷贝,感兴趣的读者可以试着自己实现其他类型的拷贝,这里就不赘述了。
以下为对以上的Person对象分别使用json序列化和Clone方法实现深拷贝的benchmark数据,可以看到Clone方法速度是json序列化的60倍,是反射方式的15倍,而内存占用则只有json方式的1/23,反射方式的1/5,当要复制的对象更复杂时,差距还会更大
goos: windows
goarch: amd64
pkg: clone_gen
cpu: Intel(R) Core(TM) i5-4590T CPU @ 2.00GHz
BenchmarkSerializer-4 385342 2611 ns/op 376 B/op 10 allocs/op
BenchmarkReflect-4 1757689 682.5 ns/op 88 B/op 5 allocs/op
BenchmarkClone-4 29532063 43.37 ns/op 16 B/op 1 allocs/op
PASS
ok clone_gen 4.393s