slice、string、defer

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函数,有两种情况:

  1. 切片的底层数组容量足够。那么,使用append添加新元素后的切片与元切片共享底层数组。
  2. 切片的底层数组容量不足。那么,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是临时场景,那么就不需要内存拷贝。就比如;

  1. 字符串拼接,临时使用
  2. 查找数据,临时使用
  3. 用于比较,临时使用

4.如何高效拼接字符串?

Golang中常用的字符串拼接:

  1. strings.Builder
  2. strings.Join
  3. (加号) +
  4. fmt.Sprintf
  5. 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的值。

注意:

  1. defer定义的延迟函数的参数在声明defer语句当时就已经确定下来了
  2. return不是原子级操作,执行过程是:设置返回值->执行defer语句->奖结果返回

异常捕获

  1. recover()只能恢复当前函数级或以当前函数为首的调用链中的panic(),恢复后调用当前函数结束,但是调用此函数的函数继续执行
  2. 函数发生了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)
}
}
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇