文章目录
- string面试与分析
- 1、string的底层数据结构是怎样的
- 2、字符串可以被修改吗
- 3、\[\]byte转化为string会发生内存拷贝吗
-
- [1)string -> \[\]byte:一定拷贝](#1)string -> []byte:一定拷贝)
- [2)\[\]byte -> string:通常拷贝](#2)[]byte -> string:通常拷贝)
- 4、字符串拼接有哪几种方式、各自的性能怎么样(https://www.cnblogs.com/cheyunhua/p/15769717.html)
- 测试代码
- slice
- 切片是什么
-
- 数组
-
- [Example one(将数组 传递到函数中,数组的地址不一样)](#Example one(将数组 传递到函数中,数组的地址不一样))
- [Example two(拷贝数组,修改旧数组,对新数组无影响)](#Example two(拷贝数组,修改旧数组,对新数组无影响))
- 有了数组,为什么还需要切片?
- 切片的底层剖析
- 切片的问题解密
-
- [1. 切片通过函数,传的是什么?](#1. 切片通过函数,传的是什么?)
- [2. 在函数里面改变切片,函数外的切片会被影响吗](#2. 在函数里面改变切片,函数外的切片会被影响吗)
- [3. 截取切片](#3. 截取切片)
- [4. 删除元素](#4. 删除元素)
- [5. 新增元素](#5. 新增元素)
-
- [为什么两个切片 len/cap 独立?](#为什么两个切片 len/cap 独立?)
- 为什么又会互相影响?
- case2
- case1
- case3
- [6. 深度拷贝](#6. 深度拷贝)
- [7. 一道字节面试题](#7. 一道字节面试题)
- slice面试与分析
- 1、silice的底层数据结构是怎样的
- 2、从一个切片截取出另一个切片,修改新切片的值会影响原来的切片内容吗
- 3、切片的扩容策略是怎样的
string面试与分析
1、string的底层数据结构是怎样的
go语言中字符串的底层实现是一个结构类型,包含两个字段,一个指向字节数组的指针,另一个是字符串的字节长度
\[\]byte 的"切片头"是(Data, Len, Cap),底层是一个字节数组
string 的"头"是(Data, Len),底层是一段只读字节序列(按 UTF-8 解读时才是"字符序列")
2、字符串可以被修改吗
字符串不可以被修改,但可以被重新赋值
3、\[\]byte转化为string会发生内存拷贝吗
会发生内存拷贝,所以程序中应避免出现大量的长字符串的这种转换
1)string -> \[\]byte:一定拷贝
go
b := []byte(s)
会新分配一段 \[\]byte 底层数组,并把 s 的内容复制进去。
原因:\[\]byte 可修改,而 string 不可变,必须隔离。
2)\[\]byte -> string:通常拷贝
go
s := string(b)
一般会新分配一段内存,把 b 的内容复制进去,保证得到的 string 之后不会被 b 的修改影响。
但:在某些"临时用途"场景(例如只用于比较、拼接、map 查找等),编译器/运行时可能做优化,看起来像没拷贝。这是实现细节,不保证、不可依赖。
4、字符串拼接有哪几种方式、各自的性能怎么样
strings.builder ≈ strings.join > bytes.buffer > append > "+" > fmt.sprintf
补充说明
- string.Builder 它可以存储\[\]byte,bytes.Buffer也可以,但是通过string.Builder.String() 返回的string,他底层的byte数组(往深一些是byte数组,看切片底层就知道了) 是一样的,复用同一个

b.buf 是 \[\]byte,它底层有一块连续内存。
unsafe.SliceData(b.buf) 取到的就是这块内存的首地址(*byte 指针)


- 而bytes.Buffer.String 返回的string,他底层的byte数组不一样,也就是没有复用内存


测试代码
go
package main
import (
"bytes"
"fmt"
"strings"
"unsafe"
)
func main() {
a := []byte{1, 2, 3}
// 构建string.Builder 和 bytes.Buffer
b := strings.Builder{}
b.Write(a)
b2 := bytes.NewBuffer(a)
// strings.Builder.String() 的实现倾向于零拷贝:直接用 Builder 内部 []byte 的地址和长度构造一个 string
str1 := b.String()
str2 := b.String()
// 不出意外,它们的数组指针是一样的(复用内存)
String2Bytes(str1)
String2Bytes(str2)
str3 := b2.String()
str4 := b2.String()
// 不出意外,它们的数组指针不一样的(没有复用)
String2Bytes(str3)
String2Bytes(str4)
}
func String2Bytes(s string) {
// unsafe.StringData(s) 返回指向 字符串底层字节序列首地址 的 *byte
// %p 把这个指针按地址打印出来
fmt.Printf("%p\n", unsafe.StringData(s))
}
go
root@GoLang:~/proj/goforjob# go run main.go
0xc000012098
0xc000012098
0xc0000120c0
0xc0000120c3
slice
切片是什么
简单来说,切片就是建立在Go的数组之上的抽象类型,如果要理解切片,我们必须首先了解数组
数组
Go语言中数组是一个值,数组变量就表示了整个数组,而C语言是指向第一个数组元素的指针
验证例子
Example one(将数组 传递到函数中,数组的地址不一样)
go
package main
import "fmt"
func main() {
array := [3]int{1, 2, 3}
// 数组传递到函数中
test(array)
fmt.Printf("array 外: %p\n", &array)
fmt.Printf("array 外: %p\n", &array[0])
}
func test(array [3]int) {
fmt.Printf("array 内: %p\n", &array)
}
go
root@GoLang:~/proj/goforjob# go run main.go
array 内: 0xc000018108
array 外: 0xc0000180f0
array 外: 0xc0000180f0
数组地址和数组第一个元素的地址是一样的
Example two(拷贝数组,修改旧数组,对新数组无影响)
go
package main
import "fmt"
func main() {
array1 := [3]int{1, 2, 3}
var array2 [3]int
array2 = array1
// 修改array1的值
array1[0] = 10
fmt.Println(array1)
fmt.Println(array2)
}
go
root@GoLang:~/proj/goforjob# go run main.go
[10 2 3]
[1 2 3]
有了数组,为什么还需要切片?
这里直接抛出几个数组面临的挑战来解释这个问题
-
长度如何动态扩容?
a. 数组长度在声明阶段已经固定,我们要想一个更大的数组,只能重新申请新数组,抛弃旧数组
-
应对动态数据集合处理问题,显得捉襟见肘时
a. 比如我们从文件或网络中读取数据等场景,难以定义一个合适大小的数组
切片的底层剖析
底层结构
go
type slice struct {
// 底层数组指针(或者说是指向一块连续内存空间的起点)
array unsafe.Pointer
// 长度
len int
// 容量
cap int
}

切片扩容
计算目标容量
case1:如果新切片的长度 > 旧切片容量的两倍,则新切片容量就为新切片的长度
case2:
i. 如果旧切片的容量小于256,那么新切片的容量就是旧切片的容量的两倍
ii. 反之需要用旧切片容量按照1.25倍的增速,直到 >= 新切片长度
为了更平滑的过渡,每次扩大1.25倍,还会加上 3/4 * 256
进行内存对齐
内存对齐本质上是性能与实现复杂度的权衡。很多 CPU 对某些类型的内存访问在对齐地址上更高效;当数据位于非对齐地址,硬件可能需要拆分成多次加载再合并,尤其是当访问跨越缓存行或页边界时,性能会明显下降。在部分架构或特定指令/配置下,未对齐访问还可能触发异常。即便像 x86 这样普遍支持未对齐访问,对齐访问通常仍然更快、更稳定。
需要按照 Go 内存管理的级别去对齐内存,最终容量以这个为准


以下不是完整源码!!!
go
func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice {
// 参数解释:
// oldPtr: 旧切片的底层数组指针
// newLen: 新切片的预估长度 (oldlen + num)
// oldCap: 旧切片的容量
// num: append的元素个数
// 得出原切片的长度
oldLen := newLen - num
newcap := oldCap
doublecap := newcap + newcap
// 如果新切片的长度 大于 旧切片的容量的两倍,那么就需要扩容为新切片的长度
if newLen > doublecap {
newcap = newLen
} else {
// 扩容部分分流 阈值
const threshold = 256
// 如果旧切片的容量小于256,那么新切片的容量就是旧切片的容量的两倍
if oldCap < threshold {
newcap = doublecap
} else {
// 每次进行1.25扩容,直到目标容量 达到 新切片长度
for 0 < newcap && newcap < newLen {
// 从小切片的2倍增长转变为大切片的1.25倍增长。这个公式在两者之间提供了一个平稳的过渡(比如加上3/4 * threshold)
newcap += (newcap + 3*threshold) / 4
}
}
}
var overflow bool
var lenmem, newlenmem, capmem uintptr
// 基于容量,确定新数组所需要的内存空间大小 capmem
// 同时会针对 span class 进行取整
switch {
case et.size == 1:
// 假若数组元素的大小为1
lenmem = uintptr(oldLen)
newlenmem = uintptr(newLen)
capmem = roundupsize(uintptr(newcap))
overflow = uintptr(newcap) > maxAlloc
newcap = int(capmem)
case et.size == goarch.PtrSize:
// 数组元素为指针类型,则根据指针占用空间结合元素个数计算空间大小
lenmem = uintptr(oldLen) * goarch.PtrSize
newlenmem = uintptr(newLen) * goarch.PtrSize
capmem = roundupsize(uintptr(newcap)*goarch.PtrSize)
overflow = uintptr(newcap) > maxAlloc/goarch.PtrSize
newcap = int(capmem / goarch.PtrSize)
case isPowerOfTwo(et.size):
// 假若元素大小为 2 的幂数,则直接通过位运算进行空间大小的计算
var shift uintptr
if goarch.PtrSize == 8 {
// Mask shift for better code generation.
shift = uintptr(sys.TrailingZeros64(uint64(et.size))) & 63
} else {
shift = uintptr(sys.TrailingZeros32(uint32(et.size))) & 31
}
lenmem = uintptr(oldLen) << shift
newlenmem = uintptr(newLen) << shift
// newcap * 2^shift,然后向上取整
capmem = roundupsize(uintptr(newcap) << shift)
overflow = uintptr(newcap) > (maxAlloc >> shift)
newcap = int(capmem >> shift)
capmem = uintptr(newcap) << shift
default:
lenmem = uintptr(oldLen) * et.size
newlenmem = uintptr(newLen) * et.size
capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
capmem = roundupsize(capmem)
newcap = int(capmem / et.size)
capmem = uintptr(newcap) * et.size
}
var p unsafe.Pointer
// 非指针类型
if et.ptrdata == 0 {
p = mallocgc(capmem, nil, false)
} else {
// 指针类型
p = mallocgc(capmem, et, true)
}
// 从旧的底层数组 拷贝 lenmem个字节 到 新底层数组中
memmove(p, oldPtr, lenmem)
// 返回新切片
return slice{p, newLen, newcap}
}




memmove(p, oldPtr, lenmem)
memmove(p, oldPtr, lenmem) 可以理解成:把旧底层数组那块内存里的数据,按字节原样复制到新底层数组那块内存里。


切片的问题解密
1. 切片通过函数,传的是什么?
切片通过函数传参,传的是"切片头(slice header)的拷贝",不是把整个底层数组拷贝过去。
切片头里只有 3 个信息(概念上):
指针:指向底层数组某个位置(首元素)
len:当前长度
cap:容量(从指针位置往后还能用多少)
go
package main
import (
"fmt"
"unsafe"
)
func main() {
// 建一个切片, len = 5:现在可用的元素数量是 5(下标 0~4), cap = 10:底层数组容量是 10
s := make([]int, 5, 10)
PrintSliceStruct("main", s)
test(s)
}
func test(s []int) {
PrintSliceStruct("test", s)
}
func PrintSliceStruct(tag string, s []int) {
// 指向底层数组第一个元素的指针(len==0 时可能为 nil)
data := unsafe.SliceData(s)
fmt.Printf("[%s] data=%p len=%d cap=%d slice=%v\n",
tag, data, len(s), cap(s), s)
}
go
root@GoLang:~/proj/goforjob# go run main.go
[main] data=0xc0000a0000 len=5 cap=10 slice=[0 0 0 0 0]
[test] data=0xc0000a0000 len=5 cap=10 slice=[0 0 0 0 0]
2. 在函数里面改变切片,函数外的切片会被影响吗
- 1)改元素内容:✅ 会影响外面
- 2)改变切片头(len/cap/指针):❌ 一般不会影响外面
比如重新切片、给参数重新赋值、append 后把结果赋回局部变量,这些只改了函数内那份切片头拷贝。 - 3)但有个坑:append 可能"改到外面的底层数组"
如果 append 没有触发扩容(cap 够用),它会把新元素写进同一个底层数组。
go
package main
import (
"fmt"
"unsafe"
)
func main() {
s := make([]int, 5)
// 先看初始
PrintSliceStruct("main-init", s)
// case1:只改元素,不 append
case1(s)
PrintSliceStruct("main-after-case1", s)
// case2:append 可能导致底层数组变化
case2(s)
PrintSliceStruct("main-after-case2", s)
}
// 底层数组不变(只改元素)
func case1(s []int) {
s[1] = 2
PrintSliceStruct("case1", s)
}
// append 可能导致底层数组变化(是否变化取决于 cap)
// 这里 s 初始 cap=5,append 会触发扩容 -> 底层数组通常会变
func case2(s []int) {
s = append(s, 0) // s 变成 len=6
s[1] = 1
PrintSliceStruct("case2", s)
}
func PrintSliceStruct(tag string, s []int) {
var dataPtr *int
if len(s) > 0 {
dataPtr = unsafe.SliceData(s)
} else {
dataPtr = nil
}
fmt.Printf("[%s] data=%p len=%d cap=%d slice=%v\n",
tag, dataPtr, len(s), cap(s), s)
}
go
root@GoLang:~/proj/goforjob# go run main.go
[main-init] data=0xc0000a0000 len=5 cap=5 slice=[0 0 0 0 0]
[case1] data=0xc0000a0000 len=5 cap=5 slice=[0 2 0 0 0]
[main-after-case1] data=0xc0000a0000 len=5 cap=5 slice=[0 2 0 0 0]
[case2] data=0xc0000aa000 len=6 cap=10 slice=[0 1 0 0 0 0]
[main-after-case2] data=0xc0000a0000 len=5 cap=5 slice=[0 2 0 0 0]
root@GoLang:~/proj/goforjob#
3. 截取切片
通过 : 操作得到的新 slice 和原 slice 是什么关系?
在切片表达式里,low 是左边界(起始下标),表示从哪个位置开始截取。
go
s2 := s[low:high]
切片截取本质是生成一个新的 slice header:把 Data 改为指向原底层数组的 low 元素 ,len=high-low,cap=cap(old)-low(从新起点到原切片容量上界的剩余长度),底层数组数据不复制。
go
package main
import (
"fmt"
"unsafe"
)
func main() {
s := make([]int, 5)
PrintSliceStruct("main-init", s)
case1(s)
case2(s)
case3(s)
case4(s)
// 注意:前面 case1~case3 里对 s 的重新切片,只影响函数内的切片头拷贝
// main 里的 s 不会变
PrintSliceStruct("main-end", s)
}
// 截取 1 号元素以后的元素
func case1(s []int) {
s = s[1:]
PrintSliceStruct("case1 s[1:]", s)
}
// 截取 [1, 3) 区间元素(下标 1 和 2)
func case2(s []int) {
s = s[1:3]
PrintSliceStruct("case2 s[1:3]", s)
}
// 截取 [len(s)-1, ) 区间元素(只剩最后一个元素)
func case3(s []int) {
s = s[len(s)-1:]
PrintSliceStruct("case3 s[len-1:]", s)
}
// 截取获得新切片(本质还是同一个底层数组上的不同视图)
func case4(s []int) {
s1 := s[2:]
PrintSliceStruct("case4 s1:=s[2:]", s1)
}
func PrintSliceStruct(tag string, s []int) {
var dataPtr *int
if len(s) > 0 {
dataPtr = unsafe.SliceData(s) // 指向 s[0] 的地址
}
fmt.Printf("[%s] data=%p len=%d cap=%d slice=%v\n",
tag, dataPtr, len(s), cap(s), s)
}
go
root@GoLang:~/proj/goforjob# go run main.go
[main-init] data=0xc0000240c0 len=5 cap=5 slice=[0 0 0 0 0]
[case1 s[1:]] data=0xc0000240c8 len=4 cap=4 slice=[0 0 0 0]
[case2 s[1:3]] data=0xc0000240c8 len=2 cap=4 slice=[0 0]
[case3 s[len-1:]] data=0xc0000240e0 len=1 cap=1 slice=[0]
[case4 s1:=s[2:]] data=0xc0000240d0 len=3 cap=3 slice=[0 0 0]
[main-end] data=0xc0000240c0 len=5 cap=5 slice=[0 0 0 0 0]
4. 删除元素
用 append(s:i, si+1:...) 删除元素时,本质是把后半段元素向前拷贝覆盖;返回的新切片 len 变小,而 cap 通常保持不变(仍共享原底层数组),除非发生重新分配或人为限制了容量。
go
package main
import (
"fmt"
"unsafe"
)
func main() {
s := []int{0, 1, 2, 3, 4}
PrintSliceStruct("s-before", s)
// 删除第1个元素(从0开始计数)
// [0,1) + [2,len(s)) => 把后半段拷贝覆盖到前面
// append(s[:1], s[2:]...):把后半段的元素依次追加到前半段后面
s1 := append(s[:1], s[2:]...)
// 这里发生的底层拷贝(概念上):
// 原 s: [0, 1, 2, 3, 4]
// 覆盖后: [0, 2, 3, 4, 4] (最后一个元素重复了,因为前移覆盖)
// s1 的 len 变为 4(cap 通常仍是 5,并且和 s 共享底层数组)
PrintSliceStruct("s1-after-delete", s1)
PrintSliceStruct("s-after-delete", s)
// 访问原切片(不会越界)
_ = s[4]
// 注意:s1 的 len 是 4,下标最大是 3,访问 s1[4] 会 panic
_ = s1[3]
// 如果你想"看到"底层数组里第5个位置仍然存在(cap 里还有)
// 可以通过扩展到 cap(但要确保 cap(s1) >= 5)
if cap(s1) >= 5 {
fmt.Println("s1 up to cap:", s1[:cap(s1)]) // 可能看到 [0 2 3 4 4]
}
}
func PrintSliceStruct(tag string, s []int) {
var dataPtr *int
if len(s) > 0 {
dataPtr = unsafe.SliceData(s) // 指向 s[0]
}
fmt.Printf("[%s] data=%p len=%d cap=%d slice=%v\n",
tag, dataPtr, len(s), cap(s), s)
}
go
root@GoLang:~/proj/goforjob# go run main.go
[s-before] data=0xc0000240c0 len=5 cap=5 slice=[0 1 2 3 4]
[s1-after-delete] data=0xc0000240c0 len=4 cap=5 slice=[0 2 3 4]
[s-after-delete] data=0xc0000240c0 len=5 cap=5 slice=[0 2 3 4 4]
s1 up to cap: [0 2 3 4 4]
5. 新增元素
新增元素其实就是指 append 操作
- 若 append 未超过 cap:返回切片与原切片共享底层数组,append 会写入原底层数组;原切片 len 不变,通常看不到新增部分,但底层数据已变化。
- 若 append 超过 cap:会分配新底层数组并拷贝,返回切片指向新数组,原切片仍指向旧数组 ,旧数组内容不因本次 append 被写入新元素而改变。
切片是一个"描述符"(可以理解成 3 个字段):
- data:指向底层数组某个位置的指针
- len:当前可用长度
- cap:从 data 开始到底层数组末尾的容量
为什么两个切片 len/cap 独立?
因为它们是两个不同的 slice 变量/值,各自都有自己的一份 data/len/cap 三元组。
改其中一个切片的 len/cap(比如重新切片、append 的返回值)不会自动改另一个切片的 len/cap。
为什么又会互相影响?
因为它们的 data 可能指向同一个底层数组(或同一块数组区域)。
如果你在共享区域里修改元素:s1i=...,另一个切片在对应位置也能看到(因为读的是同一块内存)。
如果 append 没扩容:新元素写进同一个底层数组,另一个切片虽然 len 不变,但底层数组内容确实变了;你把另一个切片重新切长一点(在 cap 允许范围内)就可能"看到"那些变化。
一句话总结:
len/cap 是切片自己的(独立),元素存储是底层数组的(共享)。
case2
go
package main
import (
"fmt"
"unsafe"
)
func main() {
// case1()
case2()
// case3()
}
func case1() {
s1 := make([]int, 3)
PrintSliceStruct("case1-s1-before", s1)
s1 = append(s1, 1) // cap 已满,必然扩容 -> data 通常会变
PrintSliceStruct("case1-s1-after", s1)
}
func case2() {
s1 := make([]int, 3, 4)
PrintSliceStruct("case2-s1-before", s1)
s2 := append(s1, 1) // cap 够用,不扩容 -> s1 和 s2 data 通常一样
PrintSliceStruct("case2-s1-after", s1)
PrintSliceStruct("case2-s2-after", s2)
}
func case3() {
s1 := make([]int, 3)
PrintSliceStruct("case3-s1-before", s1)
s2 := append(s1, 1) // cap 已满,扩容 -> s2 data 通常变,s1 不变
PrintSliceStruct("case3-s1-after", s1)
PrintSliceStruct("case3-s2-after", s2)
}
func PrintSliceStruct(tag string, s []int) {
var dataPtr *int
if len(s) > 0 {
dataPtr = unsafe.SliceData(s) // 指向 s[0]
}
fmt.Printf("[%s] data=%p len=%d cap=%d slice=%v\n",
tag, dataPtr, len(s), cap(s), s)
}
go
root@GoLang:~/proj/goforjob# go run main.go
[case2-s1-before] data=0xc0000a0000 len=3 cap=4 slice=[0 0 0]
[case2-s1-after] data=0xc0000a0000 len=3 cap=4 slice=[0 0 0]
[case2-s2-after] data=0xc0000a0000 len=4 cap=4 slice=[0 0 0 1]
case1
go
package main
import (
"fmt"
"unsafe"
)
func main() {
case1()
// case2()
// case3()
}
func case1() {
s1 := make([]int, 3)
PrintSliceStruct("case1-s1-before", s1)
s1 = append(s1, 1) // cap 已满,必然扩容 -> data 通常会变
PrintSliceStruct("case1-s1-after", s1)
}
func case2() {
s1 := make([]int, 3, 4)
PrintSliceStruct("case2-s1-before", s1)
s2 := append(s1, 1) // cap 够用,不扩容 -> s1 和 s2 data 通常一样
PrintSliceStruct("case2-s1-after", s1)
PrintSliceStruct("case2-s2-after", s2)
}
func case3() {
s1 := make([]int, 3)
PrintSliceStruct("case3-s1-before", s1)
s2 := append(s1, 1) // cap 已满,扩容 -> s2 data 通常变,s1 不变
PrintSliceStruct("case3-s1-after", s1)
PrintSliceStruct("case3-s2-after", s2)
}
func PrintSliceStruct(tag string, s []int) {
var dataPtr *int
if len(s) > 0 {
dataPtr = unsafe.SliceData(s) // 指向 s[0]
}
fmt.Printf("[%s] data=%p len=%d cap=%d slice=%v\n",
tag, dataPtr, len(s), cap(s), s)
}
go
root@GoLang:~/proj/goforjob# go run main.go
[case1-s1-before] data=0xc0000180f0 len=3 cap=3 slice=[0 0 0]
[case1-s1-after] data=0xc0000240c0 len=4 cap=6 slice=[0 0 0 1]
root@GoLang:~/proj/goforjob#
case3
go
package main
import (
"fmt"
"unsafe"
)
func main() {
// case1()
// case2()
case3()
}
func case1() {
s1 := make([]int, 3)
PrintSliceStruct("case1-s1-before", s1)
s1 = append(s1, 1) // cap 已满,必然扩容 -> data 通常会变
PrintSliceStruct("case1-s1-after", s1)
}
func case2() {
s1 := make([]int, 3, 4)
PrintSliceStruct("case2-s1-before", s1)
s2 := append(s1, 1) // cap 够用,不扩容 -> s1 和 s2 data 通常一样
PrintSliceStruct("case2-s1-after", s1)
PrintSliceStruct("case2-s2-after", s2)
}
func case3() {
s1 := make([]int, 3)
PrintSliceStruct("case3-s1-before", s1)
s2 := append(s1, 1) // cap 已满,扩容 -> s2 data 通常变,s1 不变
PrintSliceStruct("case3-s1-after", s1)
PrintSliceStruct("case3-s2-after", s2)
}
func PrintSliceStruct(tag string, s []int) {
var dataPtr *int
if len(s) > 0 {
dataPtr = unsafe.SliceData(s) // 指向 s[0]
}
fmt.Printf("[%s] data=%p len=%d cap=%d slice=%v\n",
tag, dataPtr, len(s), cap(s), s)
}
go
root@GoLang:~/proj/goforjob# go run main.go
[case3-s1-before] data=0xc0000180f0 len=3 cap=3 slice=[0 0 0]
[case3-s1-after] data=0xc0000180f0 len=3 cap=3 slice=[0 0 0]
[case3-s2-after] data=0xc0000240c0 len=4 cap=6 slice=[0 0 0 1]
root@GoLang:~/proj/goforjob#
6. 深度拷贝
我们从上面的一些解释就可以看到,切片的传递,底层数组是浅拷贝(操作原来切片会影响新切片),那么有没有深度拷贝的方法呢?
go
package main
import (
"fmt"
"unsafe"
)
func main() {
s1 := []int{1, 2, 3}
s2 := make([]int, len(s1))
// copy 返回实际复制的元素个数:min(len(s1), len(s2))
n := copy(s2, s1)
fmt.Println("copied:", n)
PrintSliceStruct("s1", s1)
PrintSliceStruct("s2", s2)
// 验证是深拷贝(底层数组不同):改 s2 不影响 s1
s2[0] = 999
PrintSliceStruct("s1-after", s1)
PrintSliceStruct("s2-after", s2)
}
func PrintSliceStruct(tag string, s []int) {
var dataPtr *int
if len(s) > 0 {
dataPtr = unsafe.SliceData(s) // 指向 s[0]
}
fmt.Printf("[%s] data=%p len=%d cap=%d slice=%v\n",
tag, dataPtr, len(s), cap(s), s)
}
go
root@GoLang:~/proj/goforjob# go run main.go
copied: 3
[s1] data=0xc0000a8000 len=3 cap=3 slice=[1 2 3]
[s2] data=0xc0000a8018 len=3 cap=3 slice=[1 2 3]
[s1-after] data=0xc0000a8000 len=3 cap=3 slice=[1 2 3]
[s2-after] data=0xc0000a8018 len=3 cap=3 slice=[999 2 3]
7. 一道字节面试题
请说出下面代码的执行结果
go
package main
import "fmt"
func main() {
doAppend := func(s []int) {
s = append(s, 1)
printLengthAndCapacity(s)
}
s := make([]int, 8)
doAppend(s[:4])
printLengthAndCapacity(s)
doAppend(s)
printLengthAndCapacity(s)
}
func printLengthAndCapacity(s []int) {
fmt.Println(s)
fmt.Printf("len=%d cap=%d \n", len(s), cap(s))
}
go
root@GoLang:~/proj/goforjob# go run main.go
[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
root@GoLang:~/proj/goforjob#
slice面试与分析
1、silice的底层数据结构是怎样的
slice的底层实现是一个结构类型,有三个字段,1.指向一个数组的指针Pointer,2.切片的长度len,3. 切片的容量cap
2、从一个切片截取出另一个切片,修改新切片的值会影响原来的切片内容吗
在截取完之后,如果新切片没有触发扩容,则修改切片元素会影响原切片,如果触发了扩容则不会
3、切片的扩容策略是怎样的
1.17及以前
- 如果期望容量大于当前容量的两倍就会使用期望容量;
- 如果当前切片的长度小于 1024 就会将容量翻倍;
- 如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;
1.18之后

期望容量:newLen
预估容量:newcap
扩容公式
go
newcap = oldcap + (oldcap + 3*256) / 4
之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!