Java/Go双修 - Go切片Slice原理

Slice是什么

Slice也称为切片,切片其实就是建立在Go的数组之上的抽象类型,如果要理解切片,我们必须了解数组

对于Javaer来说,Slice就是Java的List,也就是动态数组

  • 为什么有数组还需要切片?

    数组长度在声明阶段已经固定,我们要想一个更大的数组,只能重新申请新数组,抛弃旧数组

    在应对动态数据集合处理问题的时候,显得捉襟见肘,比如从网络中读取数据等场景,难以定义一个合适大小的数组

Slice的底层剖析 - 扩容机制

go 复制代码
type slice struct {
	array unsafe.Pointer		// 底层数组指针(或者说是指向一块连续内存空间的起点)
	len   int								// 长度
	cap   int								// 容量
}
  • slice怎么做扩容的?

    1.如果新切片的长度 > 旧切片容量的两倍,则新切片容量就为新切片的长度

    2.如果旧切片的容量 < 256,那么新切片的容量就是旧切片的容量的两倍

    3.如果旧切片的容量 > 256,那么新切片的容量会按照1.25倍的增速,直到 >= 新切片的长度

go 复制代码
func growslice(et *_type, old slice, cap int) slice {
      newcap := old.cap
      doublecap := newcap + newcap
      if cap > doublecap {
        newcap = cap
      } else {
        const threshold = 256
        if old.cap < threshold {
          newcap = doublecap
        } else {
          for 0 < newcap && newcap < cap {
            newcap += (newcap + 3*threshold) / 4
          }
          if newcap <= 0 {
            newcap = cap
          }
        }
      }
}
  • slice的内存对齐

    切片的每一次扩容,都会去尝试进行内存对齐,举个例子,假设当前操作系统的内存对齐是8个字节

    那么每一次内存对齐就是8 16 24 32 48 64 80 ...,那么如果你的下一次扩容的容量为60,那么最终分配会分配64

    最终分配的mallogc的容量大小是 capmem,源码有点长,这里不贴出来了,感兴趣可以去 growslice() 看看

    ini 复制代码
    p = mallocgc(capmem, nil, false)

切片通过函数传递,传的是什么?

传递的是切片的三个值 unsafe.Pointer、len、cap,这三个值,传到一个新的slice,但是指针指向的同一个底层数组

go 复制代码
func main() {
	s := make([]int, 5, 10)
	PrintSliceStruct(&s)
	test0(s)
}

func test0(s []int) {
	PrintSliceStruct(&s)
}

func PrintSliceStruct(s *[]int) {
	// 代码 将slice 转换成 reflect.SliceHeader
	ss := (*reflect.SliceHeader)(unsafe.Pointer(s))
	// 查看slice的结构
	fmt.Printf("slice struct: %+v, slice is %v\n", ss, s)
}

输出结果可以看到,底层数组的地址都是一致的

ini 复制代码
slice struct: &{Data:1374390779904 Len:5 Cap:10}, slice is &[0 0 0 0 0]
slice struct: &{Data:1374390779904 Len:5 Cap:10}, slice is &[0 0 0 0 0]

在函数里改变切片,函数外的切片会被影响吗?append()分析

这里需要分两种情况讨论,重点是切片的底层数组变不变

上面我们提到切片的传递是值传递,传递的是数组指针,既然都指向同一个内存地址,那里层函数修改肯定会导致外层修改的

这里就需要和Java的List做一个区分了,Java中的List在每次扩容的时候,都不会改变原数组指针的指向,Go则不同

  • 切片底层数组不变的情况
  • 切片底层数组变的情况
  • 代码理解
go 复制代码
func main() {
	s := make([]int, 5)
	PrintSliceStruct(&s)
	case1(s)
	case2(s)
}

// 底层数组不变
func case1(s []int) {
	s[1] = 1
	PrintSliceStruct(&s)
}

// 底层数组变化
func case2(s []int) {
	s = append(s, 0)
	s[1] = 1
	PrintSliceStruct(&s)
}

func PrintSliceStruct(s *[]int) {
	// 代码 将slice 转换成 reflect.SliceHeader
	ss := (*reflect.SliceHeader)(unsafe.Pointer(s))

	// 查看slice的结构
	fmt.Printf("slice struct: %+v, slice is %v\n", ss, s)
}

输出结果

ini 复制代码
slice struct: &{Data:1374390755376 Len:5 Cap:5}, slice is &[0 0 0 0 0]
slice struct: &{Data:1374390755376 Len:5 Cap:5}, slice is &[0 1 0 0 0]
slice struct: &{Data:1374390837248 Len:6 Cap:10}, slice is &[0 1 0 0 0 0]

切片截取

go 复制代码
s := make([]int,5)
s1 = s[1:]	// 截取0号元素以后的元素
s2 = s[1:3]	// 截取[1,2]区间的元素 注意[1:3)左闭右开
s3 = s[:3]	// 相当于[0,3) 截取 [0,2]区间的元素

这种切片截取是怎么做的?其实是重新创建了一个切片,但是旧切片和新切片共享同一个底层数组,和append()也是一样的逻辑

  • 共享同一个底层数组

当新切片s2再进行append()的时候,此时就会触发扩容,并指向新的底层数组的地址,和上面的append()逻辑是一样的

代码题思考

输出结果是?

go 复制代码
func main() {
    doappend := func(s []int) {
        s = append(s, 1)
        printLenthAndCapacity(s)
    }
    s := make([]int, 8, 8)
    doappend(s[:4])
    printLenthAndCapacity(s)

    doappend(s)
    printLenthAndCapacity(s)
}

func printLenthAndCapacity(s []int) {
    fmt.Println(s)
    fmt.Printf("len=%d cap=%d \n", len(s), cap(s))
}
ini 复制代码
0 0 0 0 1, len 5, cap 8
0 0 0 0 1 0 0 0, len 8, cap 8
0 0 0 0 1 0 0 0 1, len 9, cap 16
0 0 0 0 1 0 0 0, len 8, cap 8
相关推荐
Mr Aokey1 小时前
Spring MVC参数绑定终极手册:单&多参/对象/集合/JSON/文件上传精讲
java·后端·spring
地藏Kelvin2 小时前
Spring Ai 从Demo到搭建套壳项目(二)实现deepseek+MCP client让高德生成昆明游玩4天攻略
人工智能·spring boot·后端
菠萝012 小时前
共识算法Raft系列(1)——什么是Raft?
c++·后端·算法·区块链·共识算法
长勺2 小时前
Spring中@Primary注解的作用与使用
java·后端·spring
小奏技术3 小时前
基于 Spring AI 和 MCP:用自然语言查询 RocketMQ 消息
后端·aigc·mcp
编程轨迹3 小时前
面试官:如何在 Java 中读取和解析 JSON 文件
后端
lanfufu3 小时前
记一次诡异的线上异常赋值排查:代码没错,结果不对
java·jvm·后端
编程轨迹3 小时前
如何在 Java 中实现 PDF 与 TIFF 格式互转
后端
编程轨迹3 小时前
面试官:你知道如何在 Java 中创建对话框吗
后端
编程轨迹4 小时前
深入理解 Java 中的信号机制
后端