Golang基础常识性知识面试中常见的六大陷阱及应对技巧

一、nil slice & empty slice

1、nil切片与空切片底层

  • nil切片:var nilSlice [] string

    • nil slice的长度len和容量cap都是0

    • nil slice==nil

    • nil slice的pointer是nil

  • 空切片:emptySlice0 := make([]int,0)

    • empty slice的长度是0,容量是由指向底层数组决定

    • empty slice != nil

    • empty slice的pointer是底层数组的地址

  • nil切片和空切片最大的区别在指向的数组引用地址是不一样的

  • nil空切片引用数组指针地址为0(无指向任何实际地址)

  • 空切片的引用数组指针地址是有的,且固定为一个值,所有的空切片指向的数组引用地址都是一样的

2、创建nil slice 和empty slice

package mainimport "fmt"
func main() {  var nilSlice []string  // 创建一个 nil 切片  emptySlice0 := make([]int, 0)  // 方法1:创建一个空切片(零切片)  var emptySlice1 = []string{}   // 方法2:创建一个空切片
  fmt.Printf("\nnilSlice---> Nil:%v Len:%d Capacity:%d", nilSlice == nil, len(nilSlice), cap(nilSlice))  fmt.Printf("\nemptySlice0---> nil:%v Len:%d Capacity:%d", emptySlice0 == nil, len(emptySlice0), cap(emptySlice0))  fmt.Printf("\nemptySlice1---> nil:%v Len:%d Capacity:%d", emptySlice1 == nil, len(emptySlice1), cap(emptySlice1))
  // nil切片和空切片都可以正常 append数据  nilSlice = append(nilSlice, "sss")}/*Nil:true Len:0 Capacity:0nil:false Len:0 Capacity:0nil:false Len:0 Capacity:0[sss] */

二、类型强转产生内存拷贝

1、字符串转数组发送内存拷贝

  • 字符串转成byte数组,会发生内存拷贝吗?

  • 字符串转出切片,会产生拷贝

  • 严格来说,只要是发送类型强转都会发送内存拷贝

  • 那么问题来了,频繁的内存拷贝操作听起来对性能不大友好

  • 有没有什么办法可以在字符串转出切片的时候不用发生拷贝呢?

2、字符串转数组不内存拷贝方法

  • 那么如果想要在底层转换二者,只需要吧StringHeader的地址强转成SliceHeader就行,那么go有个很强的包叫unsafe

  • 1.unsafe.Pointer(&a)方法可以得到变量a的地址。

    • 2.(*reflect.StringHeader)(unsafe.Pointer(&a)) 可以把字符串a转成底层结构的形式。

    • 3.(*[]byte)(unsafe.Pointer(&ssh)) 可以把ssh底层结构体转成byte的切片的指针。

    • 4.再通过 *转为指针指向的实际内容。

      package main
      import ( "fmt" "reflect" "unsafe")
      func main() { a :="aaa" ssh := *(*reflect.StringHeader)(unsafe.Pointer(&a)) b := ([]byte)(unsafe.Pointer(&ssh)) fmt.Printf("%v---%T",b,b) // [97 97 97]---[]uint8}

三、拷贝大切片一定代价大吗?

  • SliceHeader切片在go的底层结构。

    • 第一个字是指向切片底层数组的指针,这是切片的存储空间

    • 第二个字段是切片的长度

    • 第三个字段是容量

      type SliceHeader struct { Data uintptr Len int Cap int}

  • 大切片跟小切片的区别无非就是 LenCap的值比小切片的这两个值大一些,如果发生拷贝,本质上就是拷贝上面的三个字段。

  • 所以 拷贝大切片跟小切片的代价应该是一样的

四、map不初始化使用会怎么样

  • 空map和nil map结果是一样的,都为map[]。

  • 所以,这个时候别断定map是空还是nil,而应该通过map == nil来判断。

    package main
    func main() { var m1 map[string]string // 创建一个 nil map println("m1为nil: ", m1==nil) // 报错 => panic: assignment to entry in nil map //m1["name"] = "tom"
    var m2 = make(map[string]string) // 创建一个空map m2["name"] = "jack" // 空map可以正常 println("m2为nil: ", m2==nil)}

五、map会遍历删除安全吗?

  • map 并不是一个线程安全的数据结构。

  • 同时读写一个 map 是未定义的行为,如果被检测到,会直接 panic。

  • 上面说的是发生在多个协程同时读写同一个 map 的情况下。

  • 如果在同一个协程内边遍历边删除,并不会检测到同时读写,理论上是可以这样做的。

  • sync.Map可以解决多线程读写map问题

    • 一般而言,这可以通过读写锁来解决:sync.RWMutex

    • 读之前调用 RLock() 函数,读完之后调用 RUnlock() 函数解锁;

    • 写之前调用 Lock() 函数,写完之后,调用 Unlock() 解锁。

    • 另外,sync.Map 是线程安全的 map,也可以使用

六、for循环append坑

1、坑1:添加元素变覆盖

  • 不会死循环,for range其实是golang语法糖,在循环开始前会获取切片的长度 len(切片),然后再执行len(切片)次数的循环。

    package mainimport "fmt"func main() { s := []int{1,2,3,4,5} for _, v:=range s { s =append(s, v) fmt.Printf("len(s)=%v\n",len(s)) }}/*len(s)=6len(s)=7len(s)=8len(s)=9len(s)=10 */

2、坑2:值全部一样

  • 每次循转中num的值是正常的,但是由append构造的res中,全是nums的最后一个值。

  • 最终总结出原因是在for range语句中,创建了变量num且只被创建了一次。

  • 即num有自己的空间内存且地址在for循环过程中不变

  • 循环过程中每次将nums中对应的值和num进行值传递

    package mainimport "fmt"func main() { var nums = []int{1, 2, 3, 4, 5} var res []*int for _, num := range nums { res = append(res, &num) //fmt.Println("num:", num) } for _, r := range res { fmt.Println("res:", *r) }}/*res: 5res: 5res: 5res: 5res: 5 */

3、解决方法

  • 方法1

    • 不使用for range的形式,直接用索引来对nums取值

      package mainimport "fmt"func main() { var nums = []int{1, 2, 3, 4, 5} var res []*int for i := 0; i < len(nums); i++ { res = append(res, &nums[i]) } fmt.Println("res:", res) for _, r := range res { fmt.Println("res:", *r) }}

  • 方法2

    • 在for循环中每次再定义一个新的变量num_temp,将num的值传给num_temp,之后append该变量即可。

      package mainimport "fmt"func main() { var nums = []int{1, 2, 3, 4, 5} var res []*int for _, num := range nums { numTemp := num // 创建一个新的临时变量 res = append(res, &numTemp) } for _, r := range res { fmt.Println("res:", *r) }}/*res: 1res: 2res: 3res: 4res: 5 */

相关推荐
Bruce小鬼9 分钟前
QT文件基本操作
开发语言·qt
2202_7544215415 分钟前
生成MPSOC以及ZYNQ的启动文件BOOT.BIN的小软件
java·linux·开发语言
我只会发热22 分钟前
Java SE 与 Java EE:基础与进阶的探索之旅
java·开发语言·java-ee
懷淰メ31 分钟前
PyQt飞机大战游戏(附下载地址)
开发语言·python·qt·游戏·pyqt·游戏开发·pyqt5
hummhumm1 小时前
第 22 章 - Go语言 测试与基准测试
java·大数据·开发语言·前端·python·golang·log4j
宁静@星空1 小时前
006-自定义枚举注解
java·开发语言
hummhumm1 小时前
第 28 章 - Go语言 Web 开发入门
java·开发语言·前端·python·sql·golang·前端框架
武子康1 小时前
Java-07 深入浅出 MyBatis - 一对多模型 SqlMapConfig 与 Mapper 详细讲解测试
java·开发语言·数据库·sql·mybatis·springboot
珹洺1 小时前
C语言数据结构——详细讲解 双链表
c语言·开发语言·网络·数据结构·c++·算法·leetcode
每天吃饭的羊1 小时前
python里的数据结构
开发语言·python