slice
1.切片通过函数,传的是什么?
package main
import (
"fmt"
"reflect"
"unsafe"
)
func main() {
s := make([]int, 5, 10)
printSlice(&s)
test(s)
}
func test(i []int) {
printSlice(&i)
}
func printSlice(s *[]int) {
/*
reflect.SliceHeader:用来描述切片的底层实现
unsafe.Pointer:是一个通用类型的指针,可以用作不同类型指针相互转换
*/
ss := (*reflect.SliceHeader)(unsafe.Pointer(s))
fmt.Printf("%+v\n", ss)
}
运行结果:
&{Data:824633827808 Len:5 Cap:10}
&{Data:824633827808 Len:5 Cap:10}
2.在函数里面改变切片,函数外的切片会改变吗?
1.切片做参数传递,修改切片内容影响原切片。
package main
import (
"fmt"
"reflect"
"unsafe"
)
func main() {
s := make([]int, 5)
case1(s)
printSliceStruct(&s)
}
func case1(s []int) {
s[1] = 1
printSliceStruct(&s)
}
func printSliceStruct(s *[]int) {
/*
reflect.SliceHeader:用来描述切片的底层实现
unsafe.Pointer:是一个通用类型的指针,可以用作不同类型指针相互转换
*/
ss := (*reflect.SliceHeader)(unsafe.Pointer(s))
fmt.Printf("%+v,%v\n", ss, s)
}
输出结果:
&{Data:824633779184 Len:5 Cap:5},&[0 1 0 0 0]
&{Data:824633779184 Len:5 Cap:5},&[0 1 0 0 0]
2.引用参数传递的时候,在另个函数内部扩容了,那么就会分配新的内存地址,再修改这个切片就不会影响原切片了。
具体点可以这样来理解:append切片的时候如果容量还没满,那么这个新切片只是引用原来的底层数组,只不过是长度不一样,可以说是返回的就是原切片;如果容量已满那么扩容后返回的就是一个新切片。
package main
import (
"fmt"
"reflect"
"unsafe"
)
func main() {
s := make([]int, 5)
case2(s)
printSliceStruct(&s)
}
func case2(s []int) {
s = append(s, 0)
s[1] = 1
printSliceStruct(&s)
}
func printSliceStruct(s *[]int) {
/*
reflect.SliceHeader:用来描述切片的底层实现
unsafe.Pointer:是一个通用类型的指针,可以用作不同类型指针相互转换
*/
ss := (*reflect.SliceHeader)(unsafe.Pointer(s))
fmt.Printf("%+v,%v\n", ss, s)
}
输出结果:
&{Data:824633827808 Len:6 Cap:10},&[0 1 0 0 0 0]
&{Data:824633779184 Len:5 Cap:5},&[0 0 0 0 0]
总结:传递切片的时候如果切片的修改要反应外部最好使用指针。就是说,如果传值,那么扩容前可以反应到外部,一旦扩容就不再影响外部,传指针的话,无论是否扩容,指针都能追踪上。
以下是这个案例:
package main
import (
"fmt"
"reflect"
"unsafe"
)
func main() {
s := make([]int, 5)
//case1(s)
//case2(s)
case3(&s)
printSliceStruct(&s)
}
func case3(s *[]int) {
*s = append(*s, 0)
(*s)[1] = 1
printSliceStruct(s)
}
func printSliceStruct(s *[]int) {
/*
reflect.SliceHeader:用来描述切片的底层实现
unsafe.Pointer:是一个通用类型的指针,可以用作不同类型指针相互转换
*/
ss := (*reflect.SliceHeader)(unsafe.Pointer(s))
fmt.Printf("%+v,%v\n", ss, s)
}
输出结果:
&{Data:824634425344 Len:6 Cap:10},&[0 1 0 0 0 0]
&{Data:824634425344 Len:6 Cap:10},&[0 1 0 0 0 0]
3.截取切片
问:通过:
操作得到的新切片和原切片是什么关系?
答:截取切片只是修改了底层数组的起始指向和末尾指向,长度为截取长度,容量为起始指向到末尾指向的大小。用截取切片初始化新切片也是值传递。
4.删除元素
package main
import (
"fmt"
"reflect"
"unsafe"
)
func main() {
s := []int{0, 1, 2, 3, 4}
printSliceStruct(&s)
s1 := append(s[:1], s[2:]...)
printSliceStruct(&s1)
printSliceStruct(&s)
}
func printSliceStruct(s *[]int) {
/*
reflect.SliceHeader:用来描述切片的底层实现
unsafe.Pointer:是一个通用类型的指针,可以用作不同类型指针相互转换
*/
ss := (*reflect.SliceHeader)(unsafe.Pointer(s))
fmt.Printf("%+v,%v\n", ss, s)
}
输出结果:
&{Data:824633779184 Len:5 Cap:5},&[0 1 2 3 4]
&{Data:824633779184 Len:4 Cap:5},&[0 2 3 4]
&{Data:824633779184 Len:5 Cap:5},&[0 2 3 4 4]
分析:
切片没有内置的删除函数,而是使用append函数实现删除的功能。
第二个print打印和第三个print打印相比之下,可以知道append函数实现的删除不是真的删除,而是后边的数据向前覆盖,这也导致最后的数据没有改变。
5.新增元素
Golang中新增元素就是使用append函数,有两种情况:
- 切片的底层数组容量足够。那么,使用append添加新元素后的切片与元切片共享底层数组。
- 切片的底层数组容量不足。那么,append会返回一个新的底层数组。
package main
import (
"fmt"
"reflect"
"unsafe"
)
func main() {
case1()
}
func case1() {
sc := make([]int, 3, 5)
printSliceStruct(&sc)
s1 := append(sc, 1)
printSliceStruct(&sc)
printSliceStruct(&s1)
s1[0] = 10
printSliceStruct(&sc)
printSliceStruct(&s1)
fmt.Println("--")
s2 := append(sc, 1, 2, 3, 4, 5, 6)
printSliceStruct(&sc)
printSliceStruct(&s2)
s2[2] = 20
printSliceStruct(&sc)
printSliceStruct(&s1)
}
func printSliceStruct(s *[]int) {
/*
reflect.SliceHeader:用来描述切片的底层实现
unsafe.Pointer:是一个通用类型的指针,可以用作不同类型指针相互转换
*/
ss := (*reflect.SliceHeader)(unsafe.Pointer(s))
fmt.Printf("%+v,%v\n", ss, s)
}
输出结果:
&{Data:824633779184 Len:3 Cap:5},&[0 0 0]
&{Data:824633779184 Len:3 Cap:5},&[0 0 0]
&{Data:824633779184 Len:4 Cap:5},&[0 0 0 1]
&{Data:824633779184 Len:3 Cap:5},&[10 0 0]
&{Data:824633779184 Len:4 Cap:5},&[10 0 0 1]
--
&{Data:824633779184 Len:3 Cap:5},&[10 0 0]
&{Data:824633827808 Len:9 Cap:10},&[10 0 0 1 2 3 4 5 6]
&{Data:824633779184 Len:3 Cap:5},&[10 0 0]
&{Data:824633779184 Len:4 Cap:5},&[10 0 0 1]
6.深度拷贝
Golang中切片的传递,底层数组是浅拷贝(操作原来切片会影响新切片),那么有没有深度拷贝的方法呢?
package main
import (
"fmt"
"reflect"
"unsafe"
)
func main() {
case1()
}
func case1() {
var s1 = []int{1, 2, 3, 4}
var s2 = make([]int, 5)
copy(s2, s1)//按长度最短的copy
printSliceStruct(&s1)
printSliceStruct(&s2)
}
func printSliceStruct(s *[]int) {
/*
reflect.SliceHeader:用来描述切片的底层实现
unsafe.Pointer:是一个通用类型的指针,可以用作不同类型指针相互转换
*/
ss := (*reflect.SliceHeader)(unsafe.Pointer(s))
fmt.Printf("%+v,%v\n", ss, s)
}
输出结果:
&{Data:824633803200 Len:4 Cap:4},&[1 2 3 4]
&{Data:824633779184 Len:5 Cap:5},&[1 2 3 4 0]
面试题
1.写出打印结果
package main
import (
"fmt"
)
func main() {
doAppend := func(s []int) {
s = append(s, 1)
printSliceStruct(s)
}
s := make([]int, 8, 8)
doAppend(s[:4])
printSliceStruct(s)
doAppend(s)
printSliceStruct(s)
}
func printSliceStruct(s []int) {
fmt.Println(s)
fmt.Printf("len=%d cap=%d\n", len(s), cap(s))
}
输出结果:
[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
2.slice和array的区别?
数组长度是固定的,切片是可变长的。切片是对底层数组的引用,每个切片的底层数据结构中都有一个数组、长度和容量。切片一旦初始化会和拥有同一基础数组的其它切片共享存储。数组作为函数参数是进行值传递,切片作为函数参数进行的是指针传递,函数内部改变切片影响函数外的值。数组可以比较,切片不能直接比较(可以逐个比较或reflect.DeepEqual)。
3.切片的扩容机制?扩容之后相同吗?
1.18之后,切片的扩容机制更加平滑,当新切片容量大于旧切片容量的2倍,那么就直接使用新切片容量;否则,当旧切片容量大于256,那么直接使用旧切片的2倍,当旧切片容量小于256,那么进入循环,每次增加(旧切片+3*256)/4 。
如果原有容量还没有使用完,那么扩容之后还是原来的数组;如果数组容量已经使用完,那么就会开辟一片新的内存空间,然后把原来的值拷贝过来,然后再执行append操作,那么他们就不同。
4.零切片、空切片和nil切片是什么?有什么区别?
零切片是指make创建的切片,分配了内存,但长度为0的切片。
空切片是长度和容量都为0的切片,可以是零切片。
nil切片是没有底层数组的切片。
区别:底层数组和内存分配。零切片有底层数组但是长度为0,nil切片没有底层数组。零切片和空切片分配了内存,nil切片没有分配内存。
string
1.为什么string是只读的?
在 Go 语言中,string
实际上是一个结构体,包含了一个指向底层字节数组的指针和数组的长度。由于字符串是只读的,这使得 Go 运行时能够高效地管理内存。因为字符串一旦创建就不能修改,它在内存中的位置是固定的,避免了运行时需要追踪和管理修改的复杂性,从而减少了内存碎片。此外,字符串的不可变性也意味着在多线程环境中,不会出现数据竞争和一致性问题,因此无需额外的同步处理。
https://blog.csdn.net/weixin_43435918/article/details/134210436
2.stirng和[]byte的转化原理?
从string的底层结构就知道是不可扩容的,string和[]byte的区别就是在[]byte中多了个容量,所以string转[]byte和[]byte转string都是进行内存的拷贝,指针数据和长度的匹配。
3.[]byte转string一定需要内存拷贝吗?
如果[]byte转string是临时场景,那么就不需要内存拷贝。就比如;
- 字符串拼接,临时使用
- 查找数据,临时使用
- 用于比较,临时使用
4.如何高效拼接字符串?
Golang中常用的字符串拼接:
- strings.Builder
- strings.Join
- (加号) +
- fmt.Sprintf
- append
package main
import (
"bytes"
"fmt"
"strings"
"testing"
)
var loremIpsm = `It is a highly competitive world. One can feel the existence of competition everywhere, from the classroom to the job-hunting market. Looking for a fair opportunity to prove one's ability has become a matter of survival.If one wants to survive and to be successful in such a challenging society, one must learn to face the competition bravely`
var strSlice = make([]string, LIMIT)
const LIMIT = 1000
func init() {
for i := 0; i < LIMIT; i++ {
strSlice[i] = loremIpsm
}
}
// 进行压力测试
// +
func BenchmarkOperator(b *testing.B) {
for i := 0; i < b.N; i++ {
var q string
for _, s := range strSlice {
q = q + s
}
}
b.ReportAllocs()
}
// Sprintf
func BenchmarkSprintf(b *testing.B) {
for i := 0; i < b.N; i++ {
var q string
for _, s := range strSlice {
q = fmt.Sprintf(q, s)
}
}
b.ReportAllocs()
}
// strings.Join
func BenchmarkJoin(b *testing.B) {
for i := 0; i < b.N; i++ {
strings.Join(strSlice, "")
}
b.ReportAllocs()
}
// bytes.Buffer
func BenchmarkBuffer(b *testing.B) {
for i := 0; i < b.N; i++ {
var q bytes.Buffer
q.Grow(len(loremIpsm) * len(strSlice))
for _, s := range strSlice {
q.WriteString(s)
}
}
b.ReportAllocs()
}
// append
func BenchmarkAppend(b *testing.B) {
for i := 0; i < b.N; i++ {
var q []byte
for _, s := range strSlice {
q = append(q, s...)
}
}
b.ReportAllocs()
}
// strings.Builder
func BenchmarkBuilder(b *testing.B) {
for i := 0; i < b.N; i++ {
var q strings.Builder
q.Grow(len(loremIpsm) * len(strSlice))
for _, s := range strSlice {
q.WriteString(s)
}
}
b.ReportAllocs()
}
可以看到性能比较好的是strings.Builder、strings.Join、bytes.Buffer这三个性能相比之下比较高。
如果大量字符串进行拼接时建议使用以上性能好的拼接方式,如果是少量的字符串用+比较方便。fmt.Sprintf性能最差,它一般用于格式化返回字符串而不是拼接。
defer
1.defer的底层数据结构?
https://go.cyub.vip/feature/defer/
2.defer与return?
多个defer的顺序?
后进先出。
defer与return
package main
import "fmt"
func main() {
var n int
n = 1
defer fmt.Println(n)
defer func() {
fmt.Println(n)
}()
defer func(n int) {
fmt.Println(n)
}(n)
n = 2
return
}
输出结果:
1
2
1
为什么?
defer语句在声明时,会立即捕获当前的变量值,并将这些值保存,然后等外围函数返回之后,在后进先出执行defer语句。又因为第三个和第一个都是传值的,所以固定n就是1 。但是第二个由于是匿名函数,直接是引用了 n,而不是在defer声明是捕获n,因此在执行这个defer时,会读取当前引用的n的值。
package main
import "fmt"
func main() {
a := [4]int{1, 2, 3, 4}
defer pring(&a)
a[0] = 1234
return
}
func pring(a *[4]int) {
for i := range a {
fmt.Println(a[i])
}
}
输出结果:
1234
2
3
4
why?
我们defer时传递的是数组的地址,地址不变,但是地址上的内容被修改了,所以输出会被修改。
package main
import "fmt"
func main() {
res := df()
fmt.Println(res)
}
func df() (res int) {
n := 1
defer func() {
res++
}()
return n
}
输出结果:
2
为什么?
defer与return的操作时机是: 1.设置返回值 2.执行defer语句 3.将结果返回
所以,显示设置返回值res=1,然后执行defer,最后返回res=2 。
package main
import "fmt"
func main() {
res := df()
fmt.Println(res)
}
func df() int {
n := 1
defer func() {
n++
}()
return n
}
如果返回值,未定义,那么返回的是n=1的值。
注意:
- defer定义的延迟函数的参数在声明defer语句当时就已经确定下来了
- return不是原子级操作,执行过程是:设置返回值->执行defer语句->奖结果返回
异常捕获
- recover()只能恢复当前函数级或以当前函数为首的调用链中的panic(),恢复后调用当前函数结束,但是调用此函数的函数继续执行
- 函数发生了panic之后会一直向上传递,如果直至main函数都没有recover(),程序将终止,如果碰见了recover(),将被捕获。
package main
import "fmt"
func main() {
d1()
}
func d1() {
fmt.Println("d1上")
d2()
fmt.Println("d1下")
}
func d2() {
defer func() {
recover()
}()
fmt.Println("d2上")
d3()
fmt.Println("d2下")
}
func d3() {
fmt.Println("d3上")
panic("err")
fmt.Println("d3下")
}
输出结果:
d1上
d2上
d3上
d1下
3.循环体中可以调用defer吗?
循环体中不要使用defer调用语句,一方面影响性能,另一方面可能会发生意向不到的结果。
首先,在循环体中使用defer会发生内存逃逸,那么defer就只能分配到堆中了,相比在栈上分配和内联方式,是性能最差的内存分配方式,会导致性能问题。
另外,也有可能defer语句永远得不到关闭。
算法
1 defer使用时机
1、假如有一个函数输出2,在这个函数里面使用一个defer,在函数进入的时候输出1,在函数结束的时候输出3 。
package main
import "fmt"
func main() {
defer funcc()()
fmt.Println(2)
}
func funcc() func() {
fmt.Println(1)
return func() {
fmt.Println(3)
}
}