文章目录
- 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、[字符串拼接有哪几种方式、各自的性能怎么样](https://www.cnblogs.com/cheyunhua/p/15769717.html) `strings.builder ≈ strings.join > bytes.buffer > append > "+" > fmt.sprintf` ### 补充说明 1. string.Builder 它可以存储\[\]byte,bytes.Buffer也可以,但是通过string.Builder.String() 返回的string,**他底层的byte数组(往深一些是byte数组,看切片底层就知道了) 是一样的,复用同一个**  b.buf 是 \[\]byte,它底层有一块连续内存。 unsafe.SliceData(b.buf) 取到的就是这块内存的首地址(\*byte 指针)   2. 而**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] ``` ### 有了数组,为什么还需要切片? 这里直接抛出几个数组面临的挑战来解释这个问题 1. 长度如何动态扩容? a. 数组长度在声明阶段已经固定,我们要想一个更大的数组,只能重新申请新数组,抛弃旧数组 2. 应对动态数据集合处理问题,显得捉襟见肘时 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\], s\[i+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 可能指向同一个底层数组(或同一块数组区域)。 如果你在共享区域里修改元素:s1\[i\]=...,另一个切片在对应位置也能看到(因为读的是同一块内存)。 **如果 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及以前 1. 如果期望容量大于当前容量的两倍就会使用期望容量; 2. 如果当前切片的长度小于 1024 就会将容量翻倍; 3. 如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量; ### 1.18之后  期望容量:newLen 预估容量:newcap 扩容公式 ```go newcap = oldcap + (oldcap + 3*256) / 4 ``` 之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!