基础
指针
✨什么是指针和指针变量?
答案
相关信息
普通变量存储数据,而指针变量存储的是数据的地址。
- 学习指针,主要有两个运算符号
&
和*
。&
:地址运算符,从变量中取地址*
:引用运算符,取地址中数据
num := 99
fmt.Println(num) //输出: 99
ptr := &num
fmt.Println(ptr) //输出: 例如:0xc000086020
tamp := *ptr
fmt.Println(tamp) //输出: 99
为什么使用指针?
答案
相关信息
意义一:容易编码
指针在数据结构中起着重要的作用。通过指针,我们可以创建复杂的数据结构,如链表、树和图。指针可在数据结构中轻松地访问和操作节点之间的关系,从而实现高效的数据存储和检索。
指针可在函数之间传递数据的引用,而不是复制整个数据。这样可以节省内存空间,并提高程序的执行效率。通过传递指针,函数可以直接修改原始数据,而不需要返回值。
意义二:节省内存
指针可直接访问和修改内存中的数据,通过指针,我们可以在运行时动态地分配内存,以满足程序的需求,并在不需要时释放内存,避免内存泄漏。
指针可在程序运行时动态地分配内存。通过动态内存分配,我们可以根据需要分配和释放内存,从而提高程序的灵活性和效率。
哪些对象可以获取地址,哪些不行?
答案
相关信息
可以使用 &
获取内存地址的对象:
- 变量
- 指针
- 数组,切片及其内部数据
- 结构体指针
- Map
不能寻址的对象:
- 结构体
- 常量
- 字面量
- 函数
- map 非指针元素
- 数组字面量
函数参数使用值还是指针?
答案
相关信息
- 值传递
一般来说,对于常见的类型都可以使用值传递,值传递的优点是函数内对值的修改不会影响原始的变量,也不会出现并发问题。缺点是值传递会复制一份对应变量的副本,对内存占用会多一些,如果传入的结构体非常大,使用值传递就不太合适。
- 指针和引用传递
使用指针传递的好处是直接传递变量的地址,不需要额外的空间,缺点是并发操作时数据修改会影响到原始的数据。传入切片实际上就是传递切片的指针,避免重复拷贝,若传入数组则是值传递,会拷贝一份。
Golang 中指针运算有哪些?
答案
重要
操作 | 支持情况 | 示例 | 注意事项 |
---|---|---|---|
指针解引用(*) | ✅ | *p = 100 | 访问或修改指针指向的值 |
取址(&) | ✅ | p := &x | 获取变量的内存地址 |
指针算术(+/-) | ❌ | p++ 或 p + 1 | 不支持,需用切片或unsafe包 |
指针比较(==) | 部分支持 | p1 == p2 | 仅支持相等性比较,不支持大小比较 |
指针类型转换 | ❌(需unsafe) | (*float64)(&x) | 使用unsafe包可能导致内存不安全,需谨慎 |
最佳实践:
- 优先使用切片:替代数组和指针算术,更安全且符合Go语言风格。
- 避免unsafe包:仅在性能关键且无其他方案时使用,需充分测试。
- 理解指针限制:Go刻意限制指针运算,以提高代码安全性和可读性。
相关信息
指针解引用(Dereference)
通过*
操作符访问指针指向的值:
x := 42
p := &x // p是指向x的指针(*int类型)
fmt.Println(*p) // 输出:42(解引用)
*p = 100 // 修改指针指向的值
fmt.Println(x) // 输出:100
指针地址获取(取址)
通过&
操作符获取变量的内存地址:
x := 42
p := &x // 获取x的地址
fmt.Printf("%p\n", p) // 输出指针地址(如:0xc00001a098)
提示
不支持的指针运算
1. 指针算术运算(禁止)
a := [3]int{1, 2, 3}
p := &a[0]
// p++ // 非法:不支持指针自增
// p = p + 1 // 非法:不支持指针加法
2. 指针比较(有限制)
p1 := &a[0]
p2 := &a[1]
fmt.Println(p1 == p2) // 合法:比较指针是否指向同一地址
// p1 > p2 // 非法:不支持指针大小比较
3. 指针类型转换(需谨慎)
x := 42
// p := (*float64)(&x) // 非法:不能直接转换指针类型
// 需要使用unsafe包(不推荐)
替代方案
1. 使用切片替代指针算术
a := [3]int{1, 2, 3}
s := a[:] // 创建切片
fmt.Println(s[1]) // 输出:2(通过索引访问,无需指针运算)
2. 使用unsafe包(高级场景)
import "unsafe"
x := int32(42)
p := unsafe.Pointer(&x)
q := unsafe.Add(p, 4) // 指针偏移(需自行管理内存安全)
✨array 类型的值作为函数参数是引用传递还是值传递?
答案
重要
数组(array)类型的值作为函数参数时是值传递。这意味着函数接收的是原数组的副本,而非原数组的引用。
场景 | 行为 | 建议 |
---|---|---|
数组作为函数参数 | 值传递(复制整个数组) | 小数组可直接传递,大数组建议用指针或切片 |
需要修改原数组 | 使用指针参数或切片 | 优先使用切片,代码更简洁 |
性能敏感场景 | 避免大数组值传递 | 大数组(如超过1KB)使用指针或切片,减少内存拷贝 |
最佳实践:
- 优先使用切片:在Go语言中,切片是处理动态数组的标准方式,避免直接使用固定大小的数组。
- 显式使用指针:当需要明确表示引用语义时,使用数组指针(如
*[3]int
)。 - 性能测试:对大数组传递进行基准测试,验证性能差异。
相关信息
核心机制:值传递(副本)
func modifyArray(arr [3]int) {
arr[0] = 100 // 修改副本
}
func main() {
a := [3]int{1, 2, 3}
modifyArray(a) // 传递数组副本
fmt.Println(a) // 输出:[1 2 3](原数组未修改)
}
关键点:
- 函数内部对数组的修改不会影响原数组
- 传递的是整个数组的副本,内存开销与数组大小成正比
提示
若需要引用传递,应如何处理?
1. 使用指针参数
func modifyArrayPtr(arr *[3]int) {
(*arr)[0] = 100 // 通过指针修改原数组
}
func main() {
a := [3]int{1, 2, 3}
modifyArrayPtr(&a) // 传递数组指针
fmt.Println(a) // 输出:[100, 2, 3](原数组被修改)
}
2. 使用切片(更常见)
func modifySlice(slice []int) {
slice[0] = 100 // 修改底层数组
}
func main() {
a := [3]int{1, 2, 3}
modifySlice(a[:]) // 传递数组的切片
fmt.Println(a) // 输出:[100, 2, 3](原数组被修改)
}
字面量
字面量是什么意思?
答案
重要
- 下面这些基本类型赋值的文本,就是基本类型字面量。
基本类型 | 集合 |
---|---|
布尔类型 | bool |
字符串类 | string |
复数类型 | complex64 complex128 |
浮点类型 | float32 float64 |
整数类型 | int8 uint8 int16 uint16 int32 uint32 int64 uint64 int uint uintptr |
如
s := "hello world" // "hello world" 就是字面量
n := 10 // 10 就是字面量
- 未命名常量是一种特殊的常量,它没有具体的名称。这种常量只有值,没有与之关联的变量名。
如下字符串都是字符串字面量,就是 未命名常量。
"hello,world" "123"
什么是有类型常量和无类型常量?
答案
相关信息
- Golang 中,常量分为有类型常量和无类型常量。
// 无类型常量
const A = 8
// 有类型常量
const colour string = "red"
- 当无类型的常量被赋值给一个变量的时,无类型的常量会转化成对应的类型
package main
import "fmt"
func main() {
const A = 8
var t int16 = A
fmt.Printf("%T ", t)
}//输出: int16
- 或者进行显式的转换
package main
import "fmt"
func main() {
const A int8 = 8
var t int16 = int16(A)
fmt.Printf("%T ", t) //输出: int16
}
- 而有类型常量在赋值的时,类型不同会报错
package main
import "fmt"
func main() {
const A int8 = 8
var t int16 = A
fmt.Printf("type: %T \n", t)
//出错: cannot use A (type int8) as type int16 in assignment
}
不同字面量可能同值吗?
答案
相关信息
- 一个值可存在多种字面量表示,如下十进制的数值 21,可由三种字面量表示
10进制 | 8进制 | 2进制 | 16进制 |
---|---|---|---|
21 | 0o25 | 0b0001 0101 | 0x15 |
import "fmt"
func main() {
fmt.Println(21 == 0o25)
fmt.Println(21 == 0x15 )
fmt.Println(21 == 0b0001 0101)
}// 由运行结果得出他们相等
字面量和变量的区别是什么?
答案
相关信息
字面量,就是未命名的常量,跟常量一样,是不可寻址的。
举例如下
func run() string {
return "fast"
}
func main() {
fmt.Println(&run())
}
./main.go:10:14: cannot take the address of run()
若不用变量名承接,函数返回的一个字符串的文本值,也就是字符串字面量,
而这种字面量是不可寻址的,会出现错误。要用&
寻址,须用变量名承接。而下面这样就没错
func run() string {
return "fast"
}
func main() {
t := run()
fmt.Println(&t)
}
什么是组合字面量?
答案
相关信息
组合字面量就是把对象的定义和初始化放在了一起,进一步说,组合字面量是为结构体、数组、切片和map构造值,并且每次都会创建新值。它们由字面量的类型后紧跟大括号及元素列表。每个元素前面可以选择性的带一个相关key。
使用组合字面量会简单一些,而结构体、数组、切片和map的组合字面量方式如下。
结构体用组合字面量方式来定义和初始化
type man struct {
nationality string
height int
}
func main() {
// 声明和属性赋值
su := man{
nationality: "China",
height: 180,
}
}
- 结构体用繁琐的常规方式如下
type man struct {
nationality string
height int
}
func main() {
// 声明对象
var su man
// 属性赋值
su.nationality = "China"
su.height = 180
}
- map用组合字面量方式的定义和初始化如下
m := map[string]int {
"math": 96,
"Chinese": 90,
}
- 同样的,数组用组合字面量方式的定义和初始化可以如下
colours := [3]string{"black", "red", "white"}
- 切片的组合字面量方式如下
s := []string{"red", "black"}
//会自动补上切片的容量和长度
Map
✨未初始化的 map 可以读取 key 吗?
答案
相关信息
可以的,未执行 make
初始化的 map
读取任何 key
都会返回当前类型的空值
package main
import "fmt"
func main() {
var m map[int]int
fmt.Println(m[1])
}
// 结果:
// 0
✨如果对未初始化的 map 赋值会怎么样?
答案
相关信息
会触发 panic
异常错误
package main
func main() {
var m map[int]int
m[1] = 1
}
// 结果:
// panic: assignment to entry in nil map
✨如果对未初始化的 map 进行删除 key 的操作会发生什么?
答案
相关信息
早期如果对未初始化的 map
进行 delete
操作会报 panic
错误, 现在的版本对于未初始化的 map
进行 delete
是不会报错的。
package main
func main() {
var m map[int]int
delete(m, 1)
}
// 结果:
//
map不初始化长度和初始化长度的区别
答案
重要
未初始化 vs 初始化长度为0
// 未初始化(nil map)
var m1 map[string]int // 长度为0,不可直接使用
// 初始化但长度为0
m2 := make(map[string]int, 0) // 长度为0,可直接使用
特性 | 未初始化(nil map) | 初始化长度为0 |
---|---|---|
长度(len) | 0 | 0 |
可直接赋值 | ❌ 会panic | ✅ 正常工作 |
内存分配 | 未分配内存 | 已分配最小容量 |
适用场景 | 声明但暂不使用 | 需要立即使用但无初始元素 |
提示
内存使用差异
未初始化:
- 仅占用map头结构的内存(约24字节,64位系统)
初始化但无元素:
- 分配少量内存用于存储初始桶
- 例如:
make(map[string]int, 0)
通常分配8个桶
指定大容量:
- 立即分配足够的桶数组
- 例如:
make(map[string]int, 1000)
可能分配1024个桶
相关信息
适用场景建议
场景 | 初始化方式 | 原因 |
---|---|---|
元素数量未知 | make(map[K]V) | 动态扩容,适合数量波动较大的场景 |
元素数量已知或可预估 | make(map[K]V, expectedSize) | 减少扩容次数,提高性能 |
需延迟初始化 | var m map[K]V | 后续通过m = make(map[K]V) 初始化 |
用于类型断言或反射检查 | var m map[K]V | 无需初始化即可检查类型 |
map 能承载多大,过大了怎么办?
答案
提示
map的理论容量上限
元素数量限制:
- 受可用内存限制(通常数十亿级别)
- 64位系统理论可支持约2^32(40亿+)个元素
内存占用:
- 每个元素约需24-32字节额外开销(哈希表结构)
- 总内存 ≈ 元素数量 × (键值大小 + 24字节)
性能瓶颈与扩容机制
负载因子阈值:
- 默认超过6.5时触发翻倍扩容
- 扩容导致一次性内存分配和渐进式数据迁移
性能拐点:
- 元素数量超过100万后,插入/查找性能开始下降
- 大量元素可能导致频繁哈希冲突
相关信息
问题 | 解决方案 |
---|---|
扩容导致性能下降 | 预分配足够容量,避免频繁扩容 |
并发冲突严重 | 使用分片map或sync.Map,减少锁竞争 |
内存占用过高 | 优化数据结构,使用指针或延迟加载 |
元素数量超亿级 | 考虑外部存储(如LevelDB、Redis)或分片存储 |
其他
Go 中的 rune
和 byte
有什么区别?
答案
相关信息
在 Go 语言中,byte
和 rune
都是用于表示字符的类型,但它们之间有一些区别:
类型不同:
byte
:字节,是uint8
的别名类型rune
:字符,是int32
的别名类型
存储的字符不同:
//byte 用于表示 ASCII 码字符,只能存储 0-255 范围内的字符。
var a byte = 'Y' // ASCII 码字符
//rune 用于表示 Unicode 字符,可以存储任意 Unicode 字符。
var b rune = '酥' // Unicode 字符
提示
占用的字节大小不同:byte 占用1个字节,rune 占用4个字节。
import "unsafe"
var a byte = 'Y'
var b rune = '酥'
fmt.Printf("a 占用 %d 个字节数\nb 占用 %d 个字节数", unsafe.Sizeof(a), unsafe.Sizeof(b))
// 输出: a 占用 1 个字节数 b 占用 4 个字节数
表示的字符范围不同:
由于 byte 类型能表示的值是有限的,只有 2^8=256 个。所以想表示中文只能使用 rune 类型。
✨Golang 中的深拷贝和浅拷贝是什么?
答案
相关信息
- 什么是拷贝?
拷贝最简单的一种形式如下
a := 648
b := a //把a 拷贝给 b
- 那什么是深拷贝和浅拷贝?
深浅拷贝也和类型有关
类型 | 详情 |
---|---|
引用类型 | Slice Map Channels Interfaces Functions |
值类型 | String Array Int Struct Float Bool |
两种类型拷贝效果不同,先说我们比较熟悉的值类型。如什么是拷贝提问里易知,
若是值类型的话,在每一次拷贝后都会新申请一块空间存储值,拷贝后的两个值类型独立不影响。
- 以引用类型的切片(Slice)为例来讲讲深拷贝和浅拷贝
类型 | 例子 |
---|---|
深度拷贝 | copy(slice1, slice2) |
浅拷贝 | slice1 = slice2 |
浅拷贝
仅改变指针的指向,如下
package main
import "fmt"
func main() {
var slice1 = []int{7, 8, 9}
var slice2 = make([]int, 3) //切片初始化
slice2 = slice1 //浅拷贝改变了slice2的指向
fmt.Println(slice1)
slice2[0] = 648 // 改变slice2[0],slice1[0]也改变
fmt.Println(slice2)
fmt.Println(slice1)
}
输出结果如下
[7 8 9]
[648 8 9]
[648 8 9]
所以对于切片来说,
浅拷贝
改变了它的地址。而
深拷贝
会改变地址的内存内的数组值,如下
package main
import "fmt"
func main() {
var slice1 = []int{7, 8, 9}
var slice2 = make([]int, 3) //切片初始化
copy(slice2, slice1) //深拷贝会改变地址的内存内的数组值
fmt.Println(slice2)
slice2[0] = 648 // 改变slice2[0],slice1[0]不变
fmt.Println(slice2)
fmt.Println(slice1)
}
[7 8 9]
[648 8 9]
[7 8 9]
✨make
和 new
有什么区别?
答案
相关信息
new
用于给任意的类型分配内存地址,并返回该类型的指针,且初始化值为零值。
new
并不是很常用
package main
import "fmt"
func main() {
s := new(string)
n := new(int)
fmt.Println(s) // 0xc00008a030
fmt.Println(*s) // ""
fmt.Println(n) // 0xc00000a0d8
fmt.Println(*n) // 0
}
make
主要用于 slices
map
channel
初始化
package main
import "fmt"
func main() {
m := make(map[string]int, 10)
fmt.Println(m) // map[]
}
数组和切片有什么区别?
答案
相关信息
- 数组的长度是固定的,在创建的时候就已经确定,且不可改变。切片的长度是动态的,会根据添加的数据自动扩容。
- 在函数参数传递时数据是值传递,切片是引用传递
- 切片有容量 (capacity) 参数,数组没有
如果 for range
同时添加数据, for range
会无限执行吗?
答案
相关信息
不会,在执行 for range
的时候实际遍历的是变量的副本,所以改变遍历的变量是不会有影响的
package main
import "fmt"
func main() {
n := []int{1, 2, 3}
for _, v := range n {
n = append(n, v)
}
fmt.Println(n) // 结果: [1 2 3 1 2 3]
}
多个 defer 的执行顺序是什么?
答案
相关信息
执行的顺序类似堆栈,先进后出
package main
import "fmt"
func main() {
defer func() {
fmt.Println(1)
}()
defer func() {
fmt.Println(2)
}()
defer func() {
fmt.Println(3)
}()
}
// 结果:
// 3
// 2
// 1
什么是数据溢出?
答案
相关信息
在使用数字类型时如果数据达到最大值,则接下来的数据将会溢出,如 uint
溢出后会从 0 开始, int
溢出后会变为负数。
package main
import (
"fmt"
"math"
)
func main() {
var n int8 = math.MaxInt8
var m uint8 = math.MaxUint8
n += 2
m += 1
fmt.Println(n) // -127
fmt.Println(m) // 0
}
如何避免?
- 正数优先使用 uint, 范围更大
- 添加判断代码判断是否溢出
Golang 常见的字符串拼接方式有哪些?效率有何不同?
详情
相关信息
方法 | 描述 |
---|---|
+ | 使用 + 操作符进行拼接会对遍历字符串,计算并开辟一个新的空间来存储合并后的字符串 |
fmt.Sprintf | 由于 printf 中可以使用 %d 等表示变量类型, sprintf 需要使用到反射来将不同的类型进行转换,效率较低 |
strings.Builder | 使用 WriteString() 进行拼接操作,内部使用 []byte 切片和 unsafe.pointer 指针实现 |
bytes.Buffer | byte 缓冲器,底层是 []byte 切片 |
strings.Join | strings.Join 是基于 strings.Builder 来实现的,在 Join 方法内调用了 b.Grow(n) 方法, 预分配了内存空间,较为高效 |
重要
strings.Builder
和 bytes.Buffer
有什么区别?
strings.Builder
会预分配空间,减少扩容,效率更高,适合较长的字符串拼接操作bytes.Buffer
主要用于处理单个字符,拥有许多针对单个byte
的操作,如删除替换等,这个是strings.Builder
没有的。
提示
效率排行
strings.Join ≈ strings.Builder > bytes.Buffer > "+" > fmt.Sprintf
Golang 中的 tag 有什么用?
详情
相关信息
Golang 的结构体字段可以添加各类自定义的 Tag
, 在解析结构体时可以使用函数将 Tag
解析出来,方便进行操作,常见的 Tag
:
- json: json tag 主要用于声明 json 在序列化和反序列化时的操作,如字段,可选等功能
- db: 主要用于声明数据库字段配置,用在 sqlx 中
- form: 常用在 web 框架中用于声明接收表单字段
- validate: 常用于校验器对于字段校验的配置
Golang 怎么访问私有成员?
详情
提示
1. 通过公有方法访问(推荐)
在包内定义公有方法,间接访问私有成员:
// user包
package user
type User struct {
name string // 私有字段
age int // 私有字段
}
// NewUser 公有构造函数
func NewUser(name string, age int) *User {
return &User{name: name, age: age}
}
// GetName 公有方法,暴露私有字段
func (u *User) GetName() string {
return u.name
}
// SetAge 公有方法,修改私有字段
func (u *User) SetAge(age int) {
if age > 0 {
u.age = age
}
}
// main包
package main
import "example/user"
func main() {
u := user.NewUser("Alice", 30)
println(u.GetName()) // 合法:通过公有方法访问
// println(u.name) // 非法:直接访问私有字段
}
2. 使用结构体嵌入(继承访问)
通过嵌入私有结构体到公有结构体中,间接暴露私有成员:
// config包
package config
type config struct { // 私有结构体
apiKey string
}
// PublicConfig 公有结构体,嵌入私有结构体
type PublicConfig struct {
config // 匿名字段
}
// NewConfig 公有构造函数
func NewConfig(apiKey string) *PublicConfig {
return &PublicConfig{config{apiKey: apiKey}}
}
// GetAPIKey 公有方法,暴露私有字段
func (c *PublicConfig) GetAPIKey() string {
return c.apiKey
}
// main包
package main
import "example/config"
func main() {
cfg := config.NewConfig("secret-key")
println(cfg.GetAPIKey()) // 合法:通过公有方法访问
// println(cfg.apiKey) // 非法:直接访问私有字段
}
相关信息
3. 使用接口(隐藏实现细节)
定义公有接口,让私有结构体实现该接口:
// service包
package service
import "fmt"
// 私有结构体
type database struct {
conn string
}
// 公有接口
type Database interface {
Connect() error
}
// NewDatabase 公有构造函数,返回接口
func NewDatabase(conn string) Database {
return &database{conn: conn}
}
// 实现接口方法
func (db *database) Connect() error {
fmt.Println("Connecting to:", db.conn)
return nil
}
// main包
package main
import "example/service"
func main() {
db := service.NewDatabase("mysql://user:pass@localhost")
db.Connect() // 合法:通过接口调用方法
// println(db.conn) // 非法:无法访问私有字段
}
注意
4. 使用unsafe包(不推荐,破坏封装性)
通过unsafe
包绕过访问限制(仅用于测试或特殊场景,违反Go设计原则):
package main
import (
"fmt"
"unsafe"
)
type MyStruct struct {
a int // 私有字段
b string // 私有字段
}
func main() {
s := MyStruct{a: 100, b: "hello"}
// 通过反射获取字段偏移量
aOffset := unsafe.Offsetof(s.a)
bOffset := unsafe.Offsetof(s.b)
// 访问私有字段
aPtr := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + aOffset))
bPtr := (*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + bOffset))
fmt.Println(*aPtr) // 输出: 100
fmt.Println(*bPtr) // 输出: hello
// 修改私有字段
*aPtr = 200
*bPtr = "world"
fmt.Println(s.a, s.b) // 输出: 200 world
}
slice 扩容机制是什么?
详情
提示
一、扩容触发条件
当向切片追加元素且len(slice)
超过cap(slice)
时,触发扩容:
s := make([]int, 0, 2) // len=0, cap=2
s = append(s, 1, 2) // len=2, cap=2
s = append(s, 3) // 触发扩容:len=3, cap=4
二、扩容规则(Go 1.18+)
- 预估容量(newcap)计算:
- 若原容量(oldcap)小于1024,新容量直接翻倍。
- 若原容量大于等于1024,新容量每次增加25%(即乘以1.25)。
- 内存对齐优化:
- 预估容量需经过内存对齐调整,确保内存地址为8字节倍数(64位系统)。
- 元素类型大小影响:
- 对于小元素(如int8),可能进一步调整以优化内存布局。
相关信息
性能优化建议
预分配足够容量:
// 已知大致元素数量时预分配 s := make([]int, 0, 1000) // 避免多次扩容
批量追加元素:
// 单次追加多个元素,减少扩容次数 s = append(s, 1, 2, 3, 4) // 合并切片 s = append(s, anotherSlice...)
避免不必要的切片复制:
// 错误:每次复制都会重新分配内存 for _, item := range items { s = append(s, item) // 可能触发多次扩容 } // 优化:预分配并批量复制 s := make([]Item, 0, len(items)) s = append(s, items...)
注意
常见误区
扩容不修改原切片:
func appendToSlice(s []int) { s = append(s, 100) // 扩容后返回新切片,原切片不受影响 } func main() { s := make([]int, 0, 1) appendToSlice(s) fmt.Println(len(s)) // 输出: 0(未修改) }
切片共享底层数组:
a := []int{1, 2, 3} b := a[:2] // 共享底层数组 b = append(b, 4) // 可能覆盖a[2](若未触发扩容)
nil 切片和空切片一样吗?
详情
重要
核心区别
特性 | nil切片 | 空切片 |
---|---|---|
底层数组指针 | nil (未指向任何内存) | 指向一个空数组(内存地址非nil ) |
长度(len) | 0 | 0 |
容量(cap) | 0 | 0(或更大,取决于初始化方式) |
初始化方式 | var s []int (仅声明未初始化) | s := []int{} 或 make([]int, 0) |
与nil 比较 | s == nil 为 true | s == nil 为 false |
相关信息
代码示例与内存布局
1. nil切片
var nilSlice []int // 仅声明,未初始化
// 内存布局:
// ptr: nil(无指向)
// len: 0
// cap: 0
2. 空切片
emptySlice1 := []int{} // 字面量初始化
emptySlice2 := make([]int, 0) // make初始化,长度0
emptySlice3 := make([]int, 0, 5) // 长度0,容量5(仍为空切片)
// 内存布局(以emptySlice1为例):
// ptr: 指向一个空数组(非nil地址)
// len: 0
// cap: 0(emptySlice3的cap为5)
提示
行为差异
1. 与nil
的比较
var nilSlice []int
emptySlice := []int{}
fmt.Println(nilSlice == nil) // 输出:true
fmt.Println(emptySlice == nil) // 输出:false
2. 赋值与扩容
var nilSlice []int
emptySlice := []int{}
// 两者都可以直接追加元素(会触发扩容)
nilSlice = append(nilSlice, 1) // 扩容后:len=1, cap=1,不再是nil
emptySlice = append(emptySlice, 1) // 扩容后:len=1, cap=1
3. JSON序列化
var nilSlice []int
emptySlice := []int{}
data1, _ := json.Marshal(nilSlice) // 输出:null
data2, _ := json.Marshal(emptySlice) // 输出:[]
4. 反射判断
var nilSlice []int
emptySlice := []int{}
fmt.Println(reflect.ValueOf(nilSlice).IsNil()) // 输出:true
fmt.Println(reflect.ValueOf(emptySlice).IsNil()) // 输出:false
注意
使用场景与注意事项
1. 何时使用nil切片?
- 表示“未初始化”或“不存在的值”(如函数返回“无结果”):
func findItems() []int { if noItemsFound { return nil // 明确表示“无结果” } // ... 返回有效切片 }
2. 何时使用空切片?
- 表示“有结果,但结果为空”(如查询返回空列表):
func searchEmptyResult() []int { return []int{} // 明确表示“结果为空” }
3. 常见误区
判断切片是否为空的正确方式:
应使用len(s) == 0
,而非s == nil
。因为两者的“空”状态(长度为0)在功能上等价,且追加操作对两者的处理一致。// 正确:判断切片是否为空(无论nil还是空切片) if len(s) == 0 { // 处理空切片逻辑 }
JSON序列化差异:
nil切片序列化结果为null
,空切片为[]
,需根据业务需求选择(如API返回需明确空数组时用空切片)。
提示
相同点:
两者长度和容量均为0,均可直接使用append
添加元素,且大部分操作(如遍历、长度判断)行为一致。不同点:
底层指针是否为nil
,以及与nil
的比较结果不同,这会影响序列化、反射等场景的行为。