常见坑1~10

1.常见坑1~10

01.nil slice & empty slice

1.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(无指向任何实际地址)

img

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

img

1.2 创建nil slice和empty slice

package main
import "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:0
nil:false Len:0 Capacity:0
nil:false Len:0 Capacity:0[sss]
 */

02.类型强转产生内存拷贝

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

  • 字符串转成byte数组,会发生内存拷贝吗?
  • 字符串转成切片,会产生拷贝。
  • 严格来说,只要是发生类型强转都会发生内存拷贝。
  • 那么问题来了,频繁的内存拷贝操作听起来对性能不大友好。
  • 有没有什么办法可以在字符串转成切片的时候不用发生拷贝呢?

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

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

      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
}

1.3 解释

  • StringHeader字符串在go的底层结构。
type StringHeader struct {
	Data uintptr
	Len  int
}
  • SliceHeader切片在go的底层结构。
type SliceHeader struct {
	Data uintptr
	Len  int
	Cap  int
}
  • 那么如果想要在底层转换二者,只需要把 StringHeader 的地址强转成 SliceHeader 就行。

  • 那么go有个很强的包叫 unsafe

      unsafe.Pointer(&a)

      方法可以得到变量

      a

      的地址。

      • 2.(*reflect.StringHeader)(unsafe.Pointer(&a)) 可以把字符串a转成底层结构的形式。
      • 3.(*[]byte)(unsafe.Pointer(&ssh)) 可以把ssh底层结构体转成byte的切片的指针。
      • 4.再通过 *转为指针指向的实际内容。

03.拷贝大切片一定代价大吗

拷贝大切片一定比小切片代价大吗?
  • SliceHeader
    
    切片
    
      在go的底层结构。
    
      - 第一个字是指向切片`底层数组的指针`,这是切片的存储空间
      - 第二个字段是`切片的长度`
      - 第三个字段是`容量`
    
    ```go
    type SliceHeader struct {
    	Data uintptr
    	Len  int
    	Cap  int
    }
  • 大切片跟小切片的区别无非就是 LenCap的值比小切片的这两个值大一些,如果发生拷贝,本质上就是拷贝上面的三个字段。

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

04.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)
}

05.map遍历删除安全吗

  • map 并不是一个线程安全的数据结构。
  • 同时读写一个 map 是未定义的行为,如果被检测到,会直接 panic。
  • 上面说的是发生在多个协程同时读写同一个 map 的情况下。
  • 如果在同一个协程内边遍历边删除,并不会检测到同时读写,理论上是可以这样做的。
  • sync.Map可以解决多线程读写map问题
    • 一般而言,这可以通过读写锁来解决:sync.RWMutex
    • 读之前调用 RLock() 函数,读完之后调用 RUnlock() 函数解锁;
    • 写之前调用 Lock() 函数,写完之后,调用 Unlock() 解锁。
    • 另外,sync.Map 是线程安全的 map,也可以使用。

06.for循环append坑

6.1 坑1:添加元素变覆盖

  • 不会死循环for range其实是golang语法糖,在循环开始前会获取切片的长度 len(切片),然后再执行len(切片)次数的循环。
package main
import "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)=6
len(s)=7
len(s)=8
len(s)=9
len(s)=10
 */

6.2 坑2:值全部一样

  • 每次循转中num的值是正常的,但是由append构造的res中,全是nums的最后一个值。
  • 最终总结出原因是在for range语句中,创建了变量num且只被创建了一次。
  • 即num有自己的空间内存且地址在for循环过程中不变
  • 循环过程中每次将nums中对应的值和num进行值传递。
package main
import "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: 5
res: 5
res: 5
res: 5
res: 5
 */

6.3 解决方法

  • 方法1
    
    
      - 不使用for range的形式,直接用索引来对nums取值
    
    ```go
    package main
    import "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该变量即可。
    
    ```text
    package main
    
    import "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: 1
    res: 2
    res: 3
    res: 4
    res: 5
     */

常见坑1~10
http://coderedeng.github.io/2022/04/21/Go进阶 - 常见坑1~10/
作者
Evan Deng
发布于
2022年4月21日
许可协议