小白求助

hackerzyh · 2020-10-23 17:28:34 · 2200 次点击 · 大约8小时之前 开始浏览    置顶
这是一个创建于 2020-10-23 17:28:34 的主题,其中的信息可能已经有所发展或是发生改变。

func b() {
        x := []int{}
        x = append(x, 0)
        x = append(x, 1)
        x = append(x, 2)
        y := append(x, 3)
        z := append(x, 6)
        fmt.Println(y)
        fmt.Println(z)
}

调用函数b 输出结果

[0 1 2 6]
[0 1 2 6]

为什么 y和z输出是一样的?

2、

func b() {
    x := []int{}
    x = append(x, 0)
    x = append(x, 1)
    x = append(x, 2)
    x = append(x, 3)
    y := append(x, 6)
    z := append(x, 7)
    fmt.Println(y)
    fmt.Println(z)
}

x多append一个数,y和z的输出又不一样了, 为什么?

golang版本1.13


有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2200 次点击  
加入收藏 微博
5 回复  |  直到 2020-10-26 09:35:34
avtion
avtion · #1 · 4年之前

这个属于入门基础问题,涉及Slice切片的底层实现,之前我已经回答过相关的问题,有兴趣看一下类似的问题

一、为什么 y 和 z 的结果一样

  1. 创建y变量时,没有发生扩容,导致复用x的底层数组
  2. 创建z变量时,同样没有发生扩容,依旧复用x的底层数组
  3. 最终y和z的底层数组是一样的,即3个变量都是用同一个底层数组,且y和z都能访问到第4个元素,而x只能访问到第3个元素
  4. 可以通过以下的例子进行验证
func TestB(t *testing.T) {
    x := []int{}
    x = append(x, 0) // 0
    x = append(x, 1) // 0,1
    x = append(x, 2) // 0,1,2
    y := append(x, 3) // y: 0,1,2,3 x: 0,1,2
    z := append(x, 6) // z: 0,1,2,6 y: 0,1,2,6, x: 0,1,2
    _x := (*reflect.SliceHeader)(unsafe.Pointer(&x))
    _y := (*reflect.SliceHeader)(unsafe.Pointer(&y))
    _z := (*reflect.SliceHeader)(unsafe.Pointer(&z))
    t.Log(x, _x)
    t.Log(y, _y)
    t.Log(z, _z)
}

/*
=== RUN   TestB
    main_test.go:44: [0 1 2] &{824633781600 3 4}
    main_test.go:45: [0 1 2 6] &{824633781600 4 4}
    main_test.go:46: [0 1 2 6] &{824633781600 4 4}
--- PASS: TestB (0.00s)
PASS
*/

二、x多append了一个数,y和z的输出又不一样

  1. x多追加了一个数,导致切片的大小lencap一样
  2. 接着再对x进行追加,会导致切片发生扩容,对底层数组发生值拷贝,生成新的底层数组
  3. 所以x、y和z所用的底层数组都不一样,互不影响
func TestB(t *testing.T) {
    x := []int{}
    x = append(x, 0) // 0
    x = append(x, 1) // 0,1
    x = append(x, 2) // 0,1,2
    x = append(x, 3) // 0,1,2,3
    y := append(x, 6) // y: 0,1,2,3,6 x: 0,1,2,3
    z := append(x, 7) // z: 0,1,2,3,7 y: 0,1,2,3,6, x: 0,1,2,3
    _x := (*reflect.SliceHeader)(unsafe.Pointer(&x))
    _y := (*reflect.SliceHeader)(unsafe.Pointer(&y))
    _z := (*reflect.SliceHeader)(unsafe.Pointer(&z))
    t.Log(x, _x)
    t.Log(y, _y)
    t.Log(z, _z)
}

/*
=== RUN   TestB
    main_test.go:45: [0 1 2 3] &{824634321440 4 4}
    main_test.go:46: [0 1 2 3 6] &{824634425536 5 8}
    main_test.go:47: [0 1 2 3 7] &{824634425600 5 8}
--- PASS: TestB (0.00s)
PASS
*/

顺便扔个小博客

Avtion

rustgo20
rustgo20 · #2 · 4年之前

x := make([]int,0, 3)

xuanwen
xuanwen · #3 · 4年之前

这里有个slice扩容的基础知识

规则1: 如果切片的容量小于1024个元素,那么扩容的时候slice的cap就直接翻番,乘以2;一旦元素个数超过1024个元素,cap扩容就要减缓,变成了四分之一,乘以0.25,即每次增加原来容量的四分之一。

规则2: 如果扩容之后,还没有触及原数组的容量,那么,切片中的指针指向的位置,就还是原数组,如果扩容之后,超过了原数组的容量,那么,Go就会开辟一块新的内存,把原来的值拷贝过来,这种情况丝毫不会影响到原数组。

package main

import (
    "fmt"
)

func main() {
    x := []int{}
    lastCapX := 0
    for i := 0; i < 1026; i++ {
        x = append(x, i)
        fmt.Printf("len=%d cap=%d 扩容:%d \n", len(x), cap(x), cap(x)-lastCapX)
        lastCapX = cap(x)
    }
}

/**
len=1 cap=1 扩容:1
len=2 cap=2 扩容:1
len=3 cap=4 扩容:2
len=4 cap=4 扩容:0
len=5 cap=8 扩容:4
len=6 cap=8 扩容:0
len=7 cap=8 扩容:0
len=8 cap=8 扩容:0
len=9 cap=16 扩容:8
len=10 cap=16 扩容:0
len=11 cap=16 扩容:0
 .
 .
len=16 cap=16 扩容:0
len=17 cap=32 扩容:16
len=18 cap=32 扩容:0
len=19 cap=32 扩容:0
 .
 .
len=31 cap=32 扩容:0
len=32 cap=32 扩容:0
len=33 cap=64 扩容:32
len=34 cap=64 扩容:0
 .
 .
len=127 cap=128 扩容:0
len=128 cap=128 扩容:0
len=129 cap=256 扩容:128
len=130 cap=256 扩容:0
 .
 .
len=255 cap=256 扩容:0
len=256 cap=256 扩容:0
len=257 cap=512 扩容:256
len=258 cap=512 扩容:0
len=259 cap=512 扩容:0
 .
 .
len=511 cap=512 扩容:0
len=512 cap=512 扩容:0
len=513 cap=1024 扩容:512
len=514 cap=1024 扩容:0
len=515 cap=1024 扩容:0
 .
 .
len=1023 cap=1024 扩容:0
len=1024 cap=1024 扩容:0
len=1025 cap=1280 扩容:256
len=1026 cap=1280 扩容:0
*/
上面的例子验证了规则1
1楼的例子验证了规则2

相信这些应该能够解开你的疑惑

avtion
avtion · #4 · 4年之前
xuanwenxuanwen #3 回复

#### 这里有个slice扩容的基础知识 **规则1: 如果切片的容量`小于1024`个元素,那么扩容的时候slice的cap就直接`翻番,乘以2`;一旦元素个数`超过1024`个元素,cap扩容就要减缓,变成了`四分之一,乘以0.25`,即每次增加原来容量的四分之一。** **规则2: 如果扩容之后,还没有触及原数组的容量,那么,切片中的指针指向的位置,就还是原数组,如果扩容之后,超过了原数组的容量,那么,Go就会开辟一块新的内存,把原来的值拷贝过来,这种情况丝毫不会影响到原数组。** ```golang package main import ( "fmt" ) func main() { x := []int{} lastCapX := 0 for i := 0; i < 1026; i++ { x = append(x, i) fmt.Printf("len=%d cap=%d 扩容:%d \n", len(x), cap(x), cap(x)-lastCapX) lastCapX = cap(x) } } /** len=1 cap=1 扩容:1 len=2 cap=2 扩容:1 len=3 cap=4 扩容:2 len=4 cap=4 扩容:0 len=5 cap=8 扩容:4 len=6 cap=8 扩容:0 len=7 cap=8 扩容:0 len=8 cap=8 扩容:0 len=9 cap=16 扩容:8 len=10 cap=16 扩容:0 len=11 cap=16 扩容:0 . . len=16 cap=16 扩容:0 len=17 cap=32 扩容:16 len=18 cap=32 扩容:0 len=19 cap=32 扩容:0 . . len=31 cap=32 扩容:0 len=32 cap=32 扩容:0 len=33 cap=64 扩容:32 len=34 cap=64 扩容:0 . . len=127 cap=128 扩容:0 len=128 cap=128 扩容:0 len=129 cap=256 扩容:128 len=130 cap=256 扩容:0 . . len=255 cap=256 扩容:0 len=256 cap=256 扩容:0 len=257 cap=512 扩容:256 len=258 cap=512 扩容:0 len=259 cap=512 扩容:0 . . len=511 cap=512 扩容:0 len=512 cap=512 扩容:0 len=513 cap=1024 扩容:512 len=514 cap=1024 扩容:0 len=515 cap=1024 扩容:0 . . len=1023 cap=1024 扩容:0 len=1024 cap=1024 扩容:0 len=1025 cap=1280 扩容:256 len=1026 cap=1280 扩容:0 */ ``` ##### 上面的例子验证了规则1 ##### 1楼的例子验证了规则2 相信这些应该能够解开你的疑惑

其实如果切片的容量小于1024个元素,那么扩容的时候slice的cap就直接翻番,乘以2;一旦元素个数超过1024个元素,cap扩容就要减缓,变成了四分之一,乘以0.25,即每次增加原来容量的四分之一这个说法并不准确,具体扩容的大小还得看容器元素是否能内存对齐,例如

func TestB(t *testing.T)  {
    a := []struct{
        b struct{}
    }{}
    a = append(a, struct{ b struct{} }{b: struct {

    }{}})
    a = append(a, struct{ b struct{} }{b: struct {

    }{}})
    a = append(a, struct{ b struct{} }{b: struct {

    }{}})
    t.Log((*reflect.SliceHeader)(unsafe.Pointer(&a)))
}

// 输出
// &{15181584 3 3}

是和int类型的切片连续追加3次容量是4有出入的

hackerzyh
hackerzyh · #5 · 4年之前

感谢大佬详细的回复,学习了 @avtion

添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传