12 - Go Slice:底层原理、扩容机制与常见坑位

文章目录

  • [12 - Go Slice:底层原理、扩容机制与常见坑位(超详细)](#12 - Go Slice:底层原理、扩容机制与常见坑位(超详细))
  • [什么是 Slice?](#什么是 Slice?)
  • [Slice 和数组的区别](#Slice 和数组的区别)
  • [Slice 的底层结构(核心重点)](#Slice 的底层结构(核心重点))
  • [Slice 的创建方式](#Slice 的创建方式)
  • [Slice 的核心操作](#Slice 的核心操作)
    • [📌 append(重点)](#📌 append(重点))
    • [📌 copy](#📌 copy)
    • [📌 截取(共享底层数组)](#📌 截取(共享底层数组))
  • [Slice 扩容机制(面试高频🔥)](#Slice 扩容机制(面试高频🔥))
    • [📌 规则(Go 1.18+)](#📌 规则(Go 1.18+))
    • [📌 示例](#📌 示例)
    • [📌 扩容本质](#📌 扩容本质)
  • [Slice 常见坑(非常重要🔥)](#Slice 常见坑(非常重要🔥))
    • [❗坑 共享底层数组导致数据污染](#❗坑 共享底层数组导致数据污染)
    • [❗坑 函数传参修改问题](#❗坑 函数传参修改问题)
  • [Slice 最佳实践](#Slice 最佳实践)
  • 面试高频问题总结
    • [🔥 Q:Slice 是值类型还是引用类型?](#🔥 Q:Slice 是值类型还是引用类型?)
    • [🔥 Q:append 一定会扩容吗?](#🔥 Q:append 一定会扩容吗?)
    • [🔥 Q:slice 扩容后原数据还在吗?](#🔥 Q:slice 扩容后原数据还在吗?)
    • [🔥 Q:为什么修改 slice 会影响原数组?](#🔥 Q:为什么修改 slice 会影响原数组?)
  • 一句话总结
  • [🚀 结语](#🚀 结语)

12 - Go Slice:底层原理、扩容机制与常见坑位(超详细)

在 Go 语言中,slice(切片)是最常用的数据结构之一。很多人会用,但不一定真的理解它。

这篇文章带你从 本质 → 原理 → 实战 → 踩坑 → 面试 全面掌握 slice。


什么是 Slice?

📌 本质一句话:

Slice 是对数组的一个"动态视图"(引用类型)


Slice 和数组的区别

对比项 数组 Slice
长度 固定 可变
类型 值类型 引用类型
传参 值拷贝 引用传递
灵活性

Slice 的底层结构(核心重点)

Slice 并不是一个简单的数据结构,它底层是一个结构体:

go 复制代码
type slice struct {
    array unsafe.Pointer // 指向底层数组
    len   int            // 当前长度
    cap   int            // 容量
}

📌 图解理解

text 复制代码
slice
  ↓
+---------------------+
| ptr | len | cap     |
+---------------------+
   ↓
底层数组 [1 2 3 4 5]

Slice 的创建方式

基于数组

go 复制代码
package main

import "fmt"

func main() {
	arr := [5]int{1, 2, 3, 4, 5}
	s := arr[1:4]

	fmt.Println(s)      // 输出:[2 3 4]
	fmt.Println(len(s)) // 输出:3
	fmt.Println(cap(s)) // 输出:4
}

使用 make

go 复制代码
package main

import "fmt"

func main() {
	s := make([]int, 3, 5)

	fmt.Println(s)      // 输出:[0 0 0]
	fmt.Println(len(s)) // 输出:3
	fmt.Println(cap(s)) // 输出:5
	s[0] = 1
	fmt.Println(s) // 输出:[1 0 0]
}

直接初始化

go 复制代码
package main

import "fmt"

func main() {
	s := []int{1, 2, 3}
	fmt.Println(s)      // 输出:[1 2 3]
	fmt.Println(len(s)) // 输出:3
	fmt.Println(cap(s)) // 输出:3
}

Slice 的核心操作

📌 append(重点)

go 复制代码
package main

import "fmt"

func main() {
	s := []int{1, 2}
	s = append(s, 3, 4)
	fmt.Println(s) // 输出:[1 2 3 4]
	s = append(s, 5)
	fmt.Println(s)      // 输出:[1 2 3 4 5]
	fmt.Println(len(s)) // 输出:5
	fmt.Println(cap(s)) // 输出:8
	// 为什么输出 8 而不是 5?
	// 因为 append 函数会将切片扩容,扩容的策略是原来的两倍。
	s = append(s, 6)
	fmt.Println(len(s)) // 输出:6
	fmt.Println(cap(s)) // 输出:8
}

📌 copy

go 复制代码
package main

import "fmt"

func main() {
	src := []int{1, 2, 3}
	dst := make([]int, len(src))

	copy(dst, src)

	fmt.Println(dst)            // 输出:[1 2 3]
	fmt.Println(src)            // 输出:[1 2 3]
	fmt.Println(copy(dst, src)) // 输出:3
	fmt.Println(len(dst))       // 输出:3
	fmt.Println(cap(dst))       // 输出:3
	fmt.Println(len(src))       // 输出:3
	fmt.Println(cap(src))       // 输出:3
	fmt.Println(dst[0])         // 输出:1
	fmt.Println(dst[1])         // 输出:2
	fmt.Println(dst[2])         // 输出:3
}

📌 截取(共享底层数组)

go 复制代码
package main

import "fmt"

func main() {
	s := []int{1, 2, 3, 4, 5}
	sub := s[1:3]
	fmt.Println(len(sub)) // 输出:2
	fmt.Println(cap(sub)) // 输出:4
	sub[0] = 100

	fmt.Println(s)        // 输出:[1 100 3 4 5]
	fmt.Println(sub)      // 输出:[100 3]
	fmt.Println(len(sub)) // 输出:2
	fmt.Println(cap(sub)) // 输出:4
}

👉 说明:共享底层数组!


Slice 扩容机制(面试高频🔥)

📌 规则(Go 1.18+)

  1. 小于 1024:翻倍扩容
  2. 大于等于 1024:每次增长约 1.25 倍

📌 示例

go 复制代码
package main

import "fmt"

func main() {
	s := make([]int, 0)

	for i := 0; i < 10; i++ {
		s = append(s, i)
		fmt.Println("长度:", len(s), "容量:", cap(s))
	}
}

输出:

bath 复制代码
长度: 1 容量: 1
长度: 2 容量: 2
长度: 3 容量: 4
长度: 4 容量: 4
长度: 5 容量: 8
长度: 6 容量: 8
长度: 7 容量: 8
长度: 8 容量: 8
长度: 9 容量: 16
长度: 10 容量: 16

📌 扩容本质

当容量不够时:

  1. 创建新数组
  2. 拷贝旧数据
  3. 指针指向新数组

Slice 常见坑(非常重要🔥)

❗坑 共享底层数组导致数据污染

go 复制代码
package main

import "fmt"

func main() {
	s1 := []int{1, 2, 3}
	s2 := s1[:2]

	s2[0] = 100

	fmt.Println(s1) // 被修改 输出:[100 2 3]
	fmt.Println(s2) // 输出:[100 2]
}

❗坑 函数传参修改问题

go 复制代码
package main

import "fmt"

func modify(s []int) {
	// 修改切片元素
	s[0] = 100
}

func main() {
	s := []int{1, 2, 3}
	modify(s)

	fmt.Println(s) // 输出:[100 2 3]
}

👉 修改元素可以影响原数据

但:

go 复制代码
package main

import "fmt"

func appendData(s []int) {
	// 这里修改的是局部变量s,并不会影响外部的s
	s = append(s, 100)
}

func main() {
	s := []int{1, 2, 3}
	appendData(s)

	fmt.Println(s) // 输出:[1 2 3]
}

👉 外部不会变!


Slice 最佳实践

提前分配容量

go 复制代码
// 创建一个长度为0,容量为1000的切片
s := make([]int, 0, 1000)

👉 避免频繁扩容

避免共享数据污染

go 复制代码
// 将切片s1的所有元素追加到空切片s2中
s2 := append([]int(nil), s1...)

使用 copy 做深拷贝

go 复制代码
// 创建一个切片
dst := make([]int, len(src))
// 将src切片的内容复制到dst中
copy(dst, src)

面试高频问题总结

🔥 Q:Slice 是值类型还是引用类型?

👉 本质是值类型,但内部包含指针 → 表现为引用类型

🔥 Q:append 一定会扩容吗?

👉 不一定,容量够就不会

🔥 Q:slice 扩容后原数据还在吗?

👉 在(被 copy 到新数组)

🔥 Q:为什么修改 slice 会影响原数组?

👉 因为共享底层数组


一句话总结

Slice = 指针 + 长度 + 容量

本质是"对数组的引用封装",灵活但容易踩坑


🚀 结语

Slice 是 Go 中最核心的数据结构之一:

✔ 用得最多

✔ 坑也最多

✔ 面试必问


相关推荐
codeejun2 小时前
每日一Go-50、Go微服务--配置中心
开发语言·微服务·golang
泽02022 小时前
LLMChat ----- 通过C++语言调用大语言模型所实现的聊天系统
开发语言·c++·语言模型
蒸汽求职2 小时前
告别静态文档:利用 Notion 搭建“交互式”简历的降维展示策略
开发语言·缓存·面试·职场和发展·金融·notion
steem_ding2 小时前
C++ 回调函数详解
开发语言·c++·算法
会编程的土豆2 小时前
字符串知识(LCS,LIS)区分总结归纳
开发语言·数据结构·c++·算法
FuckPatience2 小时前
未能加载项目文件。名称不能以“<”字符(十六进制值 0x3C)开头
开发语言
书到用时方恨少!2 小时前
Python 面向对象编程:从“过程清单”到“智能积木”的思维革命
开发语言·python·面向对象
冰暮流星2 小时前
javascript案例-简易计算器
开发语言·javascript·ecmascript
Rsun045512 小时前
5、Java 原型模式从入门到实战
java·开发语言·原型模式