Copy Behaviors in Go¶
In cpp, there are concepts of reference
, value
, right value
and left value
. The go is very similar with some idolisms of C++, for example we cannot copy the iostream
in cpp
, as the same io
in go always passed by reference with the help of interface
.
However, go doesn’t have reference
acurrately, as it only has concepts about value
and pointer
. The reference in title should be called pointer
also.
C++ has a dilicated way to control what happened when copying and moving, but golang doesn’t have such concepts, which means the complexities are hidden in the compiler and runtime.
Let’s focus on the copy behavior of basic types in golang, we can see that when copying, map
, chan
and context
are treated as reference, the origin one and copied one share the same one. It’s worthy to know why some types are copied by reference instead of value.
Copy interface¶
context
is an interface type, so we’d better to know how go implements interface.
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
In [runtime.runtime2.go](https://github.com/golang/go/blob/release-branch.go1.19/src/runtime/runtime2.go#L202)
, the iface
is the interface implementation, the [itab](https://github.com/golang/go/blob/release-branch.go1.19/src/runtime/runtime2.go#L909)
contains the description of a type. It’s enough to explain why the interface is copied like reference. As it only contains two pointers for underlying date, copying pointer still points the same object.
type iface struct {
tab *itab
data unsafe.Pointer
}
type itab struct {
inter *interfacetype
_type *_type
// copy of _type.hash. Used for type switches.
hash uint32
_ [4]byte
// variable sized. fun[0]==0 means _type does not implement inter.
fun [1]uintptr
}
Map¶
Interface is easy to understand once we know the interface behavior. However, how the map copys as a reference, it’s not an interface type.
Checking with the dave’s blog, we can know some key points:
- map type is actually a pointer to the runtime unexported object.
- compiler and runtime work together to implement
map
struct.
Compiling time rewrite¶
rewrite operation by runtime implementation.¶
We cannot see the definition of map
, unlike int
, double
, etc… As the real implementation is hidden inside the hmap
in runtime/map.go.
Here I only show some functions about the hmap
:
func makemap64(t *maptype, hint int64, h *hmap) *hmap
func makemap(t *maptype, hint int, h *hmap) *hmap
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer
func mapaccess2(t *maptype, h *hmap, key unsafe.Pointer) (unsafe.Pointer, bool)
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer
func mapdelete(t *maptype, h *hmap, key unsafe.Pointer)
They are the functions to operate the hmap
, but how they are connected to the map object created by ourselves? It’s the work of compiler, it will convert the function to runtime implementation:
v := m["key"] → runtime.mapaccess1(m, ”key", &v)
v, ok := m["key"] → runtime.mapaccess2(m, ”key”, &v, &ok)
m["key"] = 9001 → runtime.mapinsert(m, ”key", 9001)
delete(m, "key") → runtime.mapdelete(m, “key”)
We can confirm this from the go assembly, the runtime function is called after compiling.
// main.go:5 is m[2] = 1, m is map[int]int
0x0076 00118 (main.go:5) LEAQ type.map[int]int(SB), AX
0x007d 00125 (main.go:5) LEAQ main..autotmp_1+168(SP), BX
0x0085 00133 (main.go:5) MOVL $2, CX
0x008a 00138 (main.go:5) PCDATA $1, $0
0x008a 00138 (main.go:5) CALL runtime.mapassign_fast64(SB)
Assign type by compiler¶
Checking the function declaration, all of them have an argument with type maptype
. Accroding to the name, the map already knows the key value pair type when declaring, why it still a maptype
and where the value comes from?
Basically, interface is not used when implementing map. For generic, maptype
is introduced for each unique map declaration, which stores the type information of key-value.
Rather than having, as C++ has, a complete map implementation for each unique map declaration, the Go compiler creates a maptype
during compilation and uses that value when calling into the runtime’s map functions.
[By the way]: why map must use make, cannot new?¶
An intesting fact is that the map “object” we declared is a pointer essetially. That’s why if we now make
a map, it will cause a panic as the object is nil.
func main() {
var p uintptr
var m map[int]int
// output is: 8 8
fmt.Println(unsafe.Sizeof(m), unsafe.Sizeof(p))
// output is: true
fmt.Println(m==nil)
}
Let’s check the differences between make
and not make. The following assembly shows the code if we declare map by make(map[int]int
. However, the line4 disapper if we only declare but not allocate.
0x0026 00038 (main.go:4) MOVUPS X15, main..autotmp_1+168(SP)
0x002f 00047 (main.go:4) MOVUPS X15, main..autotmp_1+184(SP)
0x0038 00056 (main.go:4) MOVUPS X15, main..autotmp_1+200(SP)
0x0041 00065 (main.go:4) LEAQ main..autotmp_2+24(SP), DI
0x0046 00070 (main.go:4) PCDATA $0, $-2
0x0046 00070 (main.go:4) LEAQ -48(DI), DI
0x004a 00074 (main.go:4) DUFFZERO $313
0x005d 00093 (main.go:4) PCDATA $0, $-1
0x005d 00093 (main.go:4) LEAQ main..autotmp_2+24(SP), AX
0x0062 00098 (main.go:4) MOVQ AX, main..autotmp_1+184(SP)
0x006a 00106 (main.go:4) PCDATA $1, $1
0x006a 00106 (main.go:4) CALL runtime.fastrand(SB)
0x006f 00111 (main.go:4) MOVL AX, main..autotmp_1+180(SP)
0x0076 00118 (main.go:5) LEAQ type.map[int]int(SB), AX
0x007d 00125 (main.go:5) LEAQ main..autotmp_1+168(SP), BX
0x0085 00133 (main.go:5) MOVL $2, CX
0x008a 00138 (main.go:5) PCDATA $1, $0
0x008a 00138 (main.go:5) CALL runtime.mapassign_fast64(SB)
The make
calls the function in runtime and allocate the space to store data. If we only declare but not allocate, we will get a nil pointer which still contains the type(nil could has a type).
The built-in function make(T, args) serves a purpose different from new(T). It creates slices, maps, and channels only, and it returns an initialized (not zeroed) value of type T (not *T). The reason for the distinction is that these three types represent, under the covers, references to data structures that must be initialized before use. A slice, for example, is a three-item descriptor containing a pointer to the data (inside an array), the length, and the capacity, and until those items are initialized, the slice is nil. For slices, maps, and channels, make initializes the internal data structure and prepares the value for use.
If we use new
, it retruns *T
pointer which only allocates the memory, if we use new
to create map
, only panic will stay there for us.