NOTE
1.12 slice
1. What is a slice A dynamically growable array 2. Why slices are needed Arrays have a fixed number of elements and cannot grow dynamically 3. How to use slices 3.1. Basic usage 3.2. Function parameters pass the slice descriptor - although changes to the backing array are visible, append may replace the backing array, so return the slice when needed 3.3. nil slice and empty slice
This is a historical learning note and may contain outdated or incomplete understanding.
1. What Is a Slice?
An array that supports dynamic growth.
2. Why Do We Need Slices?
An array has a fixed number of elements and cannot grow dynamically.
3. How to Use Slices
3.1. Basic Usage
func TestSlice2(t *testing.T) {
var a []int
a = append(a, 1) // append one element
a = append(a, 2, 3, 4) // append multiple elements
a = append(a, []int{5, 6, 7}...) // append another slice; it must be expanded
fmt.Println(a)
a = a[:len(a)-1] // remove the last element
fmt.Println(a)
fmt.Println(a[0])
a[0] = 9999
fmt.Println(a[0])
}
// Output
[1 2 3 4 5 6 7]
[1 2 3 4 5 6]
1
9999
3.2. Passing a Slice to a Function
func testSlices(slices []int) {
slices[0] = 2
fmt.Println(&slices, slices)
}
func TestSlice1(t *testing.T) {
ints := make([]int, 1)
testSlices(ints)
fmt.Println(&ints, ints)
}
// Output
&[2] [2]
&[2] [2]
- Although changes to the backing array are visible through the caller’s slice, an append may make the callee’s slice point to a new backing array. Therefore, when a function may append, return the resulting slice.
func testSlices(slices []int) {
fmt.Println(len(slices), cap(slices), slices)
slices = append(slices, 1) // length increases and slices changes
slices[0] = 4
fmt.Println(len(slices), cap(slices), slices)
}
func TestSlice3(t *testing.T) {
ints := make([]int, 1)
testSlices(ints)
fmt.Println(len(ints), cap(ints), ints)
}
// Output
1 1 [0]
2 2 [4 1]
1 1 [0]
3.3. nil Slice and Empty Slice
A declared slice or one created with new is a nil slice, while one created with make is an empty slice.
func TestSlice4(t *testing.T) {
var slice []int
fmt.Println(slice == nil, slice, len(slice), cap(slice))
slice2 := *new([]int)
fmt.Println(slice2 == nil, slice2, len(slice2), cap(slice2))
slice3 := make([]int, 0, 0)
fmt.Println(slice3 == nil, slice3, len(slice3), cap(slice3))
fmt.Println(reflect.DeepEqual(slice, slice2), reflect.DeepEqual(slice, slice3))
}
// Output
true [] 0 0
true [] 0 0
false [] 0 0
true false

3.4. A nil Slice Can Be Appended to Directly
Both a nil slice and an empty slice can obtain backing-array storage through append. Ultimately the runtime allocates memory through the Go memory manager and associates it with the original nil or empty slice, turning it into a slice backed by real storage.
3.5. A Slice Itself Is Immutable
The slice descriptor itself is copied as a value, but if its backing array is exposed, that array can be modified.
func TestSlice5(t *testing.T) {
s := []int{1, 1, 1}
f(s)
fmt.Println(s)
}
func f(s []int) {
for i := range s {
s[i] += 1
}
}
// Output
[2 2 2]
3.6. Copy
Copy elements from src to dst. The number copied is the smaller of the two lengths, and copy does not trigger growth.
4. Slice Internals
4.1. Data Structure
// runtime/slice.go
type slice struct {
array unsafe.Pointer // pointer to the backing array
len int // number of used elements
cap int // total capacity: used + unused
}

4.2. Creation
4.2.1. new
var ints []int // equivalent in zero-value effect to using new([]int) and dereferencing it

4.2.2. make
ints := make([]int, 2, 5)

func main() {
slice := make([]int, 5, 10) // length 5, capacity 10
slice[2] = 2 // set the element at index 2 to 2
fmt.Println(slice)
}
Use go tool compile -S main.go to print the assembly.
// main function definition, stack frame size 96B
0x0000 00000 (main.go:5)TEXT "".main(SB), $96-0
0x0000 00000 (main.go:5)MOVQ (TLS), CX
0x0009 00009 (main.go:5)CMPQ SP, 16(CX)
0x000d 00013 (main.go:5)JLS 228
0x0013 00019 (main.go:5)SUBQ $96, SP
0x0017 00023 (main.go:5)MOVQ BP, 88(SP)
0x001c 00028 (main.go:5)LEAQ 88(SP), BP
0x0021 00033 (main.go:5)FUNCDATA $0, gclocals·69c1753bd5f81501d95132d08af04464(SB)
0x0021 00033 (main.go:5)FUNCDATA $1, gclocals·57cc5e9a024203768cbab1c731570886(SB)
0x0021 00033 (main.go:5)LEAQ type.int(SB), AX
0x0028 00040 (main.go:6)MOVQ AX, (SP)
0x002c 00044 (main.go:6)MOVQ $5, 8(SP)
0x0035 00053 (main.go:6)MOVQ $10, 16(SP)
0x003e 00062 (main.go:6)PCDATA $0, $0
// create slice
0x003e 00062 (main.go:6)CALL runtime.makeslice(SB)
0x0043 00067 (main.go:6)MOVQ 24(SP), AX
0x0048 00072 (main.go:6)MOVQ 32(SP), CX
0x004d 00077 (main.go:6)MOVQ 40(SP), DX
0x0052 00082 (main.go:7)CMPQ CX, $2
0x0056 00086 (main.go:7)JLS 221
0x005c 00092 (main.go:7)MOVQ $2, 16(AX)
0x0064 00100 (main.go:8)MOVQ AX, ""..autotmp_2+64(SP)
0x0069 00105 (main.go:8)MOVQ CX, ""..autotmp_2+72(SP)
0x006e 00110 (main.go:8)MOVQ DX, ""..autotmp_2+80(SP)
0x0073 00115 (main.go:8)MOVQ $0, ""..autotmp_1+48(SP)
0x007c 00124 (main.go:8)MOVQ $0, ""..autotmp_1+56(SP)
0x0085 00133 (main.go:8)LEAQ type.[]int(SB), AX
0x008c 00140 (main.go:8)MOVQ AX, (SP)
0x0090 00144 (main.go:8)LEAQ ""..autotmp_2+64(SP), AX
0x0095 00149 (main.go:8)MOVQ AX, 8(SP)
0x009a 00154 (main.go:8)PCDATA $0, $1
// type conversion. fmt.Println requires converting the slice
0x009a 00154 (main.go:8)CALL runtime.convT2Eslice(SB)
0x009f 00159 (main.go:8)MOVQ 16(SP), AX
0x00a4 00164 (main.go:8)MOVQ 24(SP), CX
0x00a9 00169 (main.go:8)MOVQ AX, ""..autotmp_1+48(SP)
0x00ae 00174 (main.go:8)MOVQ CX, ""..autotmp_1+56(SP)
0x00b3 00179 (main.go:8)LEAQ ""..autotmp_1+48(SP), AX
0x00b8 00184 (main.go:8)MOVQ AX, (SP)
0x00bc 00188 (main.go:8)MOVQ $1, 8(SP)
0x00c5 00197 (main.go:8)MOVQ $1, 16(SP)
0x00ce 00206 (main.go:8)PCDATA $0, $1
// print function
0x00ce 00206 (main.go:8)CALL fmt.Println(SB)
0x00d3 00211 (main.go:9)MOVQ 88(SP), BP
0x00d8 00216 (main.go:9)ADDQ $96, SP
0x00dc 00220 (main.go:9)RET
0x00dd 00221 (main.go:7)PCDATA $0, $0
0x00dd 00221 (main.go:7)CALL runtime.panicindex(SB)
0x00e2 00226 (main.go:7)UNDEF
0x00e4 00228 (main.go:7)NOP
0x00e4 00228 (main.go:5)PCDATA $0, $-1
// stack growth. At function entry, the runtime checks whether the current stack has enough space.
// If not, it calls this function to grow the stack.
0x00e4 00228 (main.go:5)CALL runtime.morestack_noctxt(SB)
0x00e9 00233 (main.go:5)JMP 0
The function that creates the slice type is cmd/compile/internal/types.NewSlice.
func NewSlice(elem *Type) *Type {
if t := elem.Cache.slice; t != nil {
if t.Elem() != elem {
Fatalf("elem mismatch")
}
return t
}
t := New(TSLICE)
// only contains the element type
t.Extra = Slice{Elem: elem}
elem.Cache.slice = t
return t
}
Argument checking is handled by cmd/compile/internal/gc.typecheck1.
func typecheck1(n *Node, top int) (res *Node) {
switch n.Op {
...
case OMAKE:
args := n.List.Slice()
i := 1
switch t.Etype {
case TSLICE:
// len must be provided
if i >= len(args) {
yyerror("missing len argument to make(%v)", t)
return n
}
l = args[i]
i++
var r *Node
if i < len(args) {
r = args[i]
}
...
// ensure cap is greater than or equal to len
if Isconst(l, CTINT) && r != nil && Isconst(r, CTINT) && l.Val().U.(*Mpint).Cmp(r.Val().U.(*Mpint)) > 0 {
yyerror("len larger than cap in make(%v)", t)
return n
}
n.Left = l
n.Right = r
n.Op = OMAKESLICE
}
...
}
}
At runtime, runtime.makeslice is called.
func makeslice(et *_type, len, cap int) unsafe.Pointer {
// calculate the memory occupied by the slice and allocate a contiguous region on the heap
mem, overflow := math.MulUintptr(et.size, uintptr(cap))
if overflow || mem > maxAlloc || len < 0 || len > cap {
// memory size = element size × slice capacity
mem, overflow := math.MulUintptr(et.size, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}
return mallocgc(mem, et, true)
}
4.2.3. Creating a Slice from an Array
slice := array[5:7]

4.3. Append
If the new slice returned by append does not need to be assigned back to the original variable, the processing flow is as follows.
// append(slice, 1, 2, 3)
// get its array pointer, length, and capacity
ptr, len, cap := slice
newlen := len + 3
// if the new length exceeds the capacity, runtime.growslice grows the slice
// and the new elements are then written in sequence
if newlen > cap {
ptr, len, cap = growslice(slice, newlen)
newlen = len + 3
}
*(ptr+len) = 1
*(ptr+len+1) = 2
*(ptr+len+2) = 3
return makeslice(ptr, newlen, cap)
If the slice returned by append overwrites the original slice variable:
// slice = append(slice, 1, 2, 3)
a := &slice
ptr, len, cap := slice
newlen := len + 3
if uint(newlen) > uint(cap) {
newptr, len, newcap = growslice(slice, newlen)
vardef(a)
// do not assign back to the original variable here
*a.cap = newcap
*a.ptr = newptr
}
newlen = len + 3
*a.len = newlen
*(ptr+len) = 1
*(ptr+len+1) = 2
*(ptr+len+2) = 3
4.3.1. Growth Rules
func growslice(et *_type, old slice, cap int) slice {
newcap := old.cap
doublecap := newcap + newcap
// if the requested capacity is greater than twice the current capacity,
// use the requested capacity
if cap > doublecap {
newcap = cap
} else {
// if the current slice length is less than 1024, double the capacity
if old.len < 1024 {
newcap = doublecap
} else {
// if the current slice length is greater than 1024,
// increase capacity by 25% each time until it reaches the requested capacity
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
if newcap <= 0 {
newcap = cap
}
}
}
If the requested new size is more than twice the current size, the size grows directly to the requested size. Otherwise, repeatedly apply the following rule: if the current size is below 1024, double it; otherwise increase it by one quarter each time, until the resulting size is at least the requested size.
- Capacity growth
- Allocated memory size
- Is it simply capacity × element size? No. It must match the runtime’s memory size classes.

4.4. Access
len and cap are handled by cmd/compile/internal/gc.epxr.
func (s *state) expr(n *Node) *ssa.Value {
switch n.Op {
case OLEN, OCAP:
switch {
case n.Left.Type.IsSlice():
op := ssa.OpSliceLen
if n.Op == OCAP {
op = ssa.OpSliceCap
}
return s.newValue1(op, types.Types[TINT], s.expr(n.Left))
...
}
...
}
}
Indexed access:
func (s *state) expr(n *Node) *ssa.Value {
switch n.Op {
case OINDEX:
switch {
case n.Left.Type.IsSlice():
p := s.addr(n, false)
return s.load(n.Left.Type.Elem(), p)
...
}
...
}

Discussion
Sign in with GitHub to comment. Discussions are stored as GitHub Issues.View on GitHub