Golang原理剖析(string面试与分析、slice、slice面试与分析)

文章目录

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

补充说明

  1. string.Builder 它可以存储\[\]byte,bytes.Buffer也可以,但是通过string.Builder.String() 返回的string,他底层的byte数组(往深一些是byte数组,看切片底层就知道了) 是一样的,复用同一个

b.buf 是 \[\]byte,它底层有一块连续内存。

unsafe.SliceData(b.buf) 取到的就是这块内存的首地址(*byte 指针)

  1. 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, 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及以前

  1. 如果期望容量大于当前容量的两倍就会使用期望容量;
  2. 如果当前切片的长度小于 1024 就会将容量翻倍;
  3. 如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;

1.18之后

期望容量:newLen

预估容量:newcap

扩容公式

go 复制代码
newcap = oldcap + (oldcap + 3*256) / 4

之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!

相关推荐
码事漫谈13 分钟前
时序数据库2026盘点:国产数据库如何以“融合多模”走出差异化之路?
前端·后端
浮游本尊18 分钟前
Java学习第42天 - Spring 事务传播、隔离级别、锁机制与并发一致性
后端
道友可好18 分钟前
让 AI 自己验收,等于让学生自己批卷
前端·人工智能·后端
鱼人33 分钟前
响应式三巨头:rem / vw / em 深度对比,移动端到底该选谁?
后端
小强198837 分钟前
Grid 网格布局实战:快速实现复杂网页排版
后端
胡志辉37 分钟前
深入浅出 call、apply、bind
前端·javascript·后端
长大198837 分钟前
Flex 布局完整教程:告别浮动,拥抱万能弹性布局
后端
用户497863050731 小时前
(一)小红的数组操作
算法·编程语言
iccb10131 小时前
5年,一个程序员是如何把私有化在线客服系统做到第一名的
前端·后端·github
Rust研习社1 小时前
这 8 个 Rust 学习资源值得每个新手收藏起来
后端·rust·编程语言