Структура среза
Слайс — это массив в golang, который использует указатель для указания на непрерывный сегмент, поэтому, по сути, это ссылочный тип.
Одинslice
Занимает 24 байта в golang
a = make([]int, 0)
unsafe.Sizeof(a) // 24
var c int
unsafe.Sizeof(c) // 8, 一个 int 在 golang 中占用 8 个bytes(本机是64位操作系统)
В slice.go среды выполнения определена структура slice.
type slice struct {
array unsafe.Pointer // 8 bytes
len int // 8 bytes
cap int // 8 bytes
// 确认了,slice 的大小 24
}
- массив - это указатель, указывающий на фактический массив
- len — количество элементов в срезе
- cap относится к выделенному в данный момент пространству
готов к отладке
Просто подготовьте программу, чтобы увидеть, как golang инициализирует слайс.
package main
import "fmt"
func main() {
a := make([]int, 0)
a = append(a, 2, 3, 4)
fmt.Println(a)
}
Инициализация среза
использоватьdlv
Отладка после разборки:
(dlv) disassemble
TEXT main.main(SB) /Users/such/gomodule/runtime/main.go
main.go:5 0x10b70f0 65488b0c2530000000 mov rcx, qword ptr gs:[0x30]
main.go:5 0x10b70f9 488d4424e8 lea rax, ptr [rsp-0x18]
main.go:5 0x10b70fe 483b4110 cmp rax, qword ptr [rcx+0x10]
main.go:5 0x10b7102 0f8637010000 jbe 0x10b723f main.go:5 0x10b7108* 4881ec98000000 sub rsp, 0x98
main.go:5 0x10b710f 4889ac2490000000 mov qword ptr [rsp+0x90], rbp
main.go:5 0x10b7117 488dac2490000000 lea rbp, ptr [rsp+0x90]
main.go:6 0x10b711f 488d051a0e0100 lea rax, ptr [rip+0x10e1a]
main.go:6 0x10b7126 48890424 mov qword ptr [rsp], rax
main.go:6 0x10b712a 0f57c0 xorps xmm0, xmm0
main.go:6 0x10b712d 0f11442408 movups xmmword ptr [rsp+0x8], xmm0
main.go:6 0x10b7132 e8b99af8ff ** call $runtime.makeslice **
main.go:6 0x10b7137 488b442418 mov rax, qword ptr [rsp+0x18]
main.go:6 0x10b713c 4889442460 mov qword ptr [rsp+0x60], rax
main.go:6 0x10b7141 0f57c0 xorps xmm0, xmm0
main.go:6 0x10b7144 0f11442468 movups xmmword ptr [rsp+0x68], xmm0
...
В куче инструкций см.call $runtime.makeslice
Вызов должен быть для инициализации среза
func makeslice(et *_type, len, cap int) unsafe.Pointer {
mem, overflow := math.MulUintptr(et.size, uintptr(cap))
if overflow || mem > maxAlloc || len < 0 || len > cap {
// NOTE: Produce a 'len out of range' error instead of a
// 'cap out of range' error when someone does make([]T, bignumber).
// 'cap out of range' is true too, but since the cap is only being
// supplied implicitly, saying len is clearer.
// See golang.org/issue/4085.
mem, overflow := math.MulUintptr(et.size, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}
return mallocgc(mem, et, true)
}
makelice, наконец, возвращает адрес памяти поля массива, где хранится реальное значение, в функцииuintptr()
Что тогда?
println(uintptr(0), ^uintptr(0))
// 0 18446744073709551615 为什么按位异或后是这个数?
var c int = 1
println(^c, ^uint64(0))
// -2 18446744073709551615
Из этих строк проверки кода, со знаком 1, двоичным: 0001, XOR: 1110, старший бит 1 является отрицательным числом, указывающим -2;
Двоичный код uint64: 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
После XOR: 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111
Потому что беззнаковый, преобразованный в десятичный, равен 2^64 - 1 = 18446744073709551615
. Таким образом, фактически ^uintptr(0) относится к максимальному значению текущей машины (32-разрядная, uint32; 64-разрядная, uint64).
мы можем распечатать текущийa
(dlv) p a
[]int len: 1, cap: 0, [0]
Расширение среза
=> main.go:7 0x10b7149 eb00 jmp 0x10b714b
main.go:7 0x10b714b 488d0dee0d0100 lea rcx, ptr [rip+0x10dee]
main.go:7 0x10b7152 48890c24 mov qword ptr [rsp], rcx
main.go:7 0x10b7156 4889442408 mov qword ptr [rsp+0x8], rax
main.go:7 0x10b715b 0f57c0 xorps xmm0, xmm0
main.go:7 0x10b715e 0f11442410 movups xmmword ptr [rsp+0x10], xmm0
main.go:7 0x10b7163 48c744242003000000 mov qword ptr [rsp+0x20], 0x3
main.go:7 0x10b716c e84f9bf8ff call $runtime.growslice
main.go:7 0x10b7171 488b442428 mov rax, qword ptr [rsp+0x28]
main.go:7 0x10b7176 488b4c2430 mov rcx, qword ptr [rsp+0x30]
main.go:7 0x10b717b 488b542438 mov rdx, qword ptr [rsp+0x38]
main.go:7 0x10b7180 4883c103 add rcx, 0x3
main.go:7 0x10b7184 eb00 jmp 0x10b7186
main.go:7 0x10b7186 48c70002000000 mov qword ptr [rax], 0x2
main.go:7 0x10b718d 48c7400803000000 mov qword ptr [rax+0x8], 0x3
main.go:7 0x10b7195 48c7401004000000 mov qword ptr [rax+0x10], 0x4
main.go:7 0x10b719d 4889442460 mov qword ptr [rsp+0x60], rax
main.go:7 0x10b71a2 48894c2468 mov qword ptr [rsp+0x68], rcx
main.go:7 0x10b71a7 4889542470 mov qword
...
При добавлении к срезу он фактически вызываетcall runtime.growslice
, чтобы увидеть, что сделано:
func growslice(et *_type, old slice, cap int) slice {
if cap < old.cap {
panic(errorString("growslice: cap out of range"))
}
if et.size == 0 {
// append should not create a slice with nil pointer but non-zero len.
// We assume that append doesn't need to preserve old.array in this case.
return slice{unsafe.Pointer(&zerobase), old.len, cap}
}
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
if newcap <= 0 {
newcap = cap
}
}
}
var overflow bool
var lenmem, newlenmem, capmem uintptr
// Specialize for common values of et.size.
// For 1 we don't need any division/multiplication.
// For sys.PtrSize, compiler will optimize division/multiplication into a shift by a constant.
// For powers of 2, use a variable shift.
switch {
case et.size == 1:
lenmem = uintptr(old.len)
newlenmem = uintptr(cap)
capmem = roundupsize(uintptr(newcap))
overflow = uintptr(newcap) > maxAlloc
newcap = int(capmem)
case et.size == sys.PtrSize:
lenmem = uintptr(old.len) * sys.PtrSize
newlenmem = uintptr(cap) * sys.PtrSize
capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
overflow = uintptr(newcap) > maxAlloc/sys.PtrSize
newcap = int(capmem / sys.PtrSize)
case isPowerOfTwo(et.size):
var shift uintptr
if sys.PtrSize == 8 {
// Mask shift for better code generation.
shift = uintptr(sys.Ctz64(uint64(et.size))) & 63
} else {
shift = uintptr(sys.Ctz32(uint32(et.size))) & 31
}
lenmem = uintptr(old.len) << shift
newlenmem = uintptr(cap) << shift
capmem = roundupsize(uintptr(newcap) << shift)
overflow = uintptr(newcap) > (maxAlloc >> shift)
newcap = int(capmem >> shift)
default:
lenmem = uintptr(old.len) * et.size
newlenmem = uintptr(cap) * et.size
capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
capmem = roundupsize(capmem)
newcap = int(capmem / et.size)
}
if overflow || capmem > maxAlloc {
panic(errorString("growslice: cap out of range"))
}
var p unsafe.Pointer
if et.ptrdata == 0 {
// 申请内存
p = mallocgc(capmem, nil, false)
// 清除未使用的地址
memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
} else {
p = mallocgc(capmem, et, true)
if lenmem > 0 && writeBarrier.enabled {
bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem)
}
}
// 拷贝大小为 lenmem 个btyes,从old.array到p
memmove(p, old.array, lenmem)
return slice{p, old.len, newcap}
Конкретные стратегии расширения:
- Если запрашиваемая емкость (cap) более чем в 2 раза превышает исходную емкость (old.cap) или исходная емкость
- В противном случае рассчитайте
newcap += newcap / 4
, знайте, что newcap не меньше емкости для применения, если она переполняется, то newcap = cap (емкость для применения)
После завершения расширения адрес пересчитывается в соответствии с размером t.size, среди которых новый слайсlen
для исходного фрагментаcap
(Только если длина среза превышает колпачок, его нужно расширить).
Затем применитеcapmem
размер памяти, скопированный из old.arraylenmem
байт (т. е. всю копию исходного слайса, lenmem — вычисленный размер исходного слайса) вp
.
a := make([]int, 0)
a = append(a, 1)
println("1 times:", len(a), cap(a)) // 1 times: 1 1
a = append(a, 2, 3)
println("2 times:", len(a), cap(a)) // 2 times: 3 4
a = append(a, 4)
println("3 times:", len(a), cap(a)) // 3 times: 4 4
Как можно видеть:
- если
append
Послеlen
больше, чемcap
в 2 раза, то есть расширен до более чемlen
первое кратное 2 - если
append
Послеlen
больше, чемcap
и меньше чемcap
вдвое большеcap
увеличить в 2 раза - если
append
Послеlen
меньше, чемcap
, непосредственно добавить
Срез загрязнения
использоватьslice
, может вызвать некоторые проблемы неосознанно.
a := []int{1, 2, 3, 4, 5}
shadow := a[1:3]
shadow = append(shadow, 100)
fmt.Println(shadow, a)
// [2 3 100] [1 2 3 100 5]
Результат оказался неожиданным, но и закономерным. в структуреarray
это указатель на массив[1,2,3,4,5]
адрес памятиshadow
указывает на[2,3]
адрес памяти. в направленииshadow
После добавления он будет напрямую изменять реальный массив, косвенно затрагивая все срезы, указывающие на массив. Таким образом, вы можете изменить приведенный выше код следующим образом:
a := []int{1, 2, 3, 4, 5}
shadow := append([]int{}, a[1:3]...)
shadow = append(shadow, 100)
fmt.Println(shadow, a)
// [2 3 100] [1 2 3 4 5]
Если возвращаемое значение функции является вышеуказанным случаемreturn a[1:3]
, также вызовет[1,2,3,4,5]
Память, занятая блокировкой, не может быть освобождена.
темная магия
понялslice
сам является указателем на реальный массив, вGolang
предоставлено вunsafe
выполнять манипуляции с указателем.
a := []int{1, 2, 3, 4, 5}
shadow := a[1:3]
shadowPtr := uintptr(unsafe.Pointer(&shadow[0]))
offset := unsafe.Sizeof(int(0))
fmt.Println(*(*int)(unsafe.Pointer(shadowPtr - offset))) // 1
fmt.Println(*(*int)(unsafe.Pointer(shadowPtr + 2*offset))) // 4
shadowPtr
является положением первого нижнего индекса a, aint
8 байт на 64-битной машине, смещение 1 впередoffset
, является 0-м индексом 1 a; смещение 2 назадoffset
, является третьим индексом 4 a .
Параллельная безопасность
slice
является безопасным типом данных, не связанным с сопрограммой, если вы создаете несколькоgoroutine
правильноslice
Одновременное чтение и запись приведет к потере. увидеть кусок кода
package main
import (
"fmt"
"sync"
)
func main () {
a := make([]int, 0)
var wg sync.WaitGroup
for i := 0; i < 10000; i++ {
wg.Add(1)
go func(i int) {
a = append(a, i)
wg.Done()
}(i)
}
wg.Wait()
fmt.Println(len(a))
}
// 9403 9876 9985 9491 ...
Выполняется несколько раз, результаты получаются каждый раз разные, короче говоря, это не должно быть желаемых 10 000. Чтобы решить эту проблему, рассмотрим задачу в соответствии с программной идеей безопасности сопрограмм,
может рассмотреть возможность использованияchannel
Его собственная функция (блокировка) для обеспечения безопасного одновременного чтения и записи.
func main() {
a := make([]int, 0)
buffer := make(chan int)
go func() {
for v := range buffer {
a = append(a, v)
}
}()
var wg sync.WaitGroup
for i := 0; i < 10000; i++ {
wg.Add(1)
go func(i int) {
buffer <- i
wg.Done()
}(i)
}
wg.Wait()
fmt.Println(len(a))
}
// 10000