引子
先来看看下面的代码,思考一下最后 nums
会打印什么?
package main
import "fmt"
func main() {
// 声明一个len =0 ,cap =10的切片
var s1 = make([]int, 0, 10)
// 先往切片中填充数据
s1 = append(s1, 1, 2, 3)
//使用底层数组创建一个新的切片,共享底层数组
var s2 = s1[1:]
// 往 s2 append 数据
s2 = append(s2, 4)
// 这里打印的结果是什么?
fmt.Println(s1)
}
答案揭晓
答案是 [1, 2, 3]
。
思考过程
对于刚接触 Go 的新手来说,这个结果似乎再正常不过了,s1
的内容显然是 [1, 2, 3]
。但对老手来说,可能会产生疑问:s2
和 s1
共享了底层数组,在不触发扩容的情况下,往 s2
中追加数据,难道不应该影响 s1
吗?按理说,s1
的内容应该是 [1, 2, 3, 4]
才对啊。
源码分析
为了弄清楚这个问题,我们需要深入了解 Go 语言标准库中的 fmt.Println 函数的实现。
以下是fmt.Println
的源码片段:
// Println 使用默认格式输出操作数,并写入标准输出。
// 操作数之间会自动加空格,最后追加一个换行符。
// 它返回写入的字节数和可能的错误。
func Println(a ...any) (n int, err error) {
return Fprintln(os.Stdout, a...)
}
Println
函数只是调用了Fprintln
,并将标准输出 os.Stdout
传递给 Fprintln
作为目标输出设备。
接着,我们看看 Fprintln
的实现:
// Fprintln 使用默认格式格式化操作数并写入 w。
// 操作数之间总是添加空格,最后加一个换行符。
// 它返回写入的字节数和可能的写入错误。
func Fprintln(w io.Writer, a ...any) (n int, err error) {
p := newPrinter()
p.doPrintln(a)
n, err = w.Write(p.buf)
p.free()
return
}
Fprintln
函数首先创建了一个 printer
对象,然后调用 doPrintln
方法来处理传入的参数 a,并将结果存储在 printer
的缓冲区中。接着,它会在缓冲区末尾添加一个换行符 \n,最后通过 Write
方法将缓冲区的内容写入 w,即传入的 io.Writer
接口(这里是标准输出 os.Stdout)。
深入 doPrintln
函数,我们看到它调用了 printArg
函数来处理每一个参数:
// doPrintln is like doPrint but always adds a space between arguments
// and a newline after the last argument.
func (p *pp) doPrintln(a []any) {
for argNum, arg := range a {
if argNum > 0 {
p.buf.writeByte(' ')
}
p.printArg(arg, 'v')
}
p.buf.writeByte('\n')
}
关键函数 printArg
func (p *pp) printArg(arg any, verb rune) {
// Some types can be done without reflection.
switch f := arg.(type) {
// 省略了部分代码
default:
// If the type is not simple, it might have methods.
if !p.handleMethods(verb) {
// Need to use reflection, since the type had no
// interface methods that could be used for formatting.
p.printValue(reflect.ValueOf(f), verb, 0)
}
}
}
对printArg
进行断言,发现最后执行了p.printValue
函数
printArg
是一个关键函数,它负责处理每个参数的格式化。对于复杂类型,如切片,最终会调用 p.printValue
来输出切片内容。
// printValue is similar to printArg but starts with a reflect value, not an interface{} value.
// It does not handle 'p' and 'T' verbs because these should have been already handled by printArg.
func (p *pp) printValue(value reflect.Value, verb rune, depth int) {
// 省略了部分代码
switch f := value; value.Kind() {
case reflect.Array, reflect.Slice:
if p.fmt.sharpV {
p.buf.writeString(f.Type().String())
if f.Kind() == reflect.Slice && f.IsNil() {
p.buf.writeString(nilParenString)
return
}
p.buf.writeByte('{')
for i := 0; i < f.Len(); i++ {
if i > 0 {
p.buf.writeString(commaSpaceString)
}
p.printValue(f.Index(i), verb, depth+1)
}
p.buf.writeByte('}')
} else {
p.buf.writeByte('[')
for i := 0; i < f.Len(); i++ {
if i > 0 {
p.buf.writeByte(' ')
}
p.printValue(f.Index(i), verb, depth+1)
}
p.buf.writeByte(']')
}
}
}
通过断言找到了最终的执行核心代码,并且这儿p.fmt.sharpV=false
,最终执行的核心代码如下
p.buf.writeByte('[')
for i := 0; i < f.Len(); i++ {
if i > 0 {
p.buf.writeByte(' ')
}
p.printValue(f.Index(i), verb, depth+1)
}
p.buf.writeByte(']')
这里,f.Len()
用于获取切片的长度,然后逐个打印切片中的元素。这就解释了为什么 s1 的输出是 [1, 2, 3],因为fmt.Println
只遍历了 s1 当前的 len 长度,而没有考虑到底层数组的变化。
底层原理分析
要理解 s1 没有被修改,必须了解 Go 中切片的底层实现。切片的底层数据结构如下:
sliceHeader struct {
Data unsafe.Pointer
Len int
Cap int
}
Data 指向底层数组,Len 是切片的长度,而 Cap 是切片的容量。在 s2 追加数据之前,s1 和 s2 共享同一个底层数组,但由于 s2 的 len 较短,fmt.Println 只输出了 s1 的前 Len 个元素。
当我们向 s2 追加数据后,s2 的 len 和 cap 可能会发生变化,但这不会影响到 s1 的 len,因此 s1 的打印结果仍然是 [1, 2, 3]。
我们最后再来看看 s1
和 s2
的 cap
和 len
在修改s2
之前,s1和 s2的状态如下
当向s2 append 数据以后
最后一张图解释为什么是 [1,2,3]
总结
这段代码展示了 Go 语言中切片操作的一些微妙之处,提醒我们要小心处理切片的共享问题,尤其是在进行 append
操作时。牢记切片是一个三元素结构(指针、长度、容量),可以帮助我们更好地理解 Go 语言中切片的行为,从而避免类似的困惑。