go语言查漏补缺

简单记录一下go语言开发过程中碰到的一些小曲折

1.:赋值时自动分配变量类型的坑

playground

1
2
3
4
5
6
7
a:='e'
s:="hello"
for i:=0;i<len(s);i++{
if s[i]==a{
fmt.Println(i)
}
}

报错 invalid operation: s[i] == a (mismatched types byte and rune),之所以出现类型不匹配的报错信息,是因为a:='e'执行后a自动赋予的是rune类型而不是uint8类型,可以使用如下方式转换变量类型

1
2
var a byte = 'e'
...

or

1
if s[i] == byte(a)

2.变量类型转换

整型和字符串

playground

1
2
a:=1
fmt.Println(string(a)) //报错: conversion from int to string yields a string of one rune, not a string of digits (did you mean fmt.Sprint(x)?)

正确做法是strconv.Itoa将数字转换为字符串

字符和字符串

playground

1
2
3
var x byte='a'
y:="bc"
fmt.Println(x+y) //报错: invalid operation: x + y (mismatched types byte and string)

报错,字符类型和字符串不能直接运算,需要类型转换,正确做法是

1
2
...
fmt.Println(string(x)+y)

同样地,以下代码也会报错

1
2
3
4
5
var x byte='a'
y:="a"
if x==y{
fmt.Println(1)
}

3.链表头结点的坑

1
2
3
4
var head *ListNode = nil
current:=head
current = &ListNode{Val:1,Next:nil} //这句之后current和head就没关系了,head一直是nil
fmt.Println(head)

正确写法是将新的结点写在上个结点的Next中,保持住前后关系

1
2
3
4
var head *ListNode = &ListNode{Val:1,Next:nil}
current:=head
current.Next = &ListNode{Val:1,Next:nil}
current = current.Next

4.循环引用

在编写blue加载全局中间件时,引发了一个循环引用问题,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
package blue
import "midware"
xxx.AddMidware(midware.LogMidware)
....
package midware
import "blue"
func LogMidware(c *blue.Context){
...
}

执行结果

1
2
3
4
5
import cycle not allowed
package main
imports github.com/sxk10812139/blue
imports github.com/sxk10812139/blue/midware
imports github.com/sxk10812139/blue

分析下原因,blue中需要import midware,midware中有需要importblue,陷入死循环,故报错。
如何避免循环嵌套呢?我的理解是要保持依赖是单向的,按下面示例就是parentPackage依赖childPackage,child彼此独立不依赖parentPackage。

parentPackage-->childPackage1
             -->childPackage2
             -->childPackage3

按照上述结论,我们解决blue和midware循环嵌套有两个思路:
1.将midware和blue放到一个package中,自然不会存在嵌套引入问题
2.消除midware与blue的关联依赖

按照思路1,将LogMidware也写在blue包中后问题解决

5.非接口变量类型判定的问题

对x进行类型断言前提要求x必须是接口类型。假设对非接口变量a做类型T的判定,应先转换为接口类型

1
2
3
if value,ok:=interface{}(a).(T);ok{
...
}

6.字符串

go中字符串面值是不可变的

不能直接修改字符串中的字符。

如果想要修改后的字符串,可以先将字符串转为[]byte再修改,然后转换回string。字符串默认值为””.

字符串切片字符串可以进行切片操作

比如str[3:8],可以截取3,8之间的字符串值。返回一个子串,而不是slice

字符串和字节数组可以相互进行显式转换

1
2
3
4
var str string = "abc";
charray := []byte(string );
charray[1] = d;
str:= string(charray);
1
2
3
4
var a string = "hello"
fmt.Println(reflect.TypeOf(a[:])) //string
fmt.Println(reflect.TypeOf([]byte(a))) //[]int
fmt.Println(a[:3],reflect.Typeof(a[:3])) //hel string

别名类型与类型再定义

别名类型S是原类型的别名,对别名类型S增加新方法就等于是对原类型增加新方法(对于string等基本类型无法直接增加新方法),可以与原类型等价使用

type S = string

类型再定义得到的S是与元类型完全不同的新类型

type S string

对于类型再定义来说,string被称为S的潜在类型,潜在类型含义是某个类型在本质上是哪个类型或者是哪个类型的集合。
潜在类型相同的不同类型的值可以相互转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//别名类型
type S = string
//类型再定义
type S string
func main(){
var s S = "hello"
hello(s) //类型再定义会报错 别名类型可以执行
}
func hello(s string){
fmt.Print(s)
}

切片

长度和容量

注意cap的计算,切片是可以向右扩展的

1
2
3
4
s1:=[]int{1,2,3,4,5,6,7}
s2:=s1[2:5]
fmt.Printf("%d %d",len(s2),cap(s2)) //3 5

切片扩容

1
2
3
4
5
6
7
8
9
10
11
12
s1 := []int{1, 2, 3, 4, 5, 6, 7}
s2 := s1[2:5]
//扩容情况1 一次append多个元素超过底层数组限制
s2 = append(s2, []int{-1, -1, -1}...)
fmt.Printf("s1:%v s2:%v", s1, s2) //结果s1:[1 2 3 4 5 6 7] s2:[3 4 5 -1 -1 -1]
//扩容情况2 一次append少数 慢慢超过底层数组限制
s2 = append(s2, -1)
s2 = append(s2, -1)
s2 = append(s2, -1)
fmt.Printf("s1:%v s2:%v", s1, s2) //结果s1:[1 2 3 4 5 -1 -1] s2:[3 4 5 -1 -1 -1]

map

键类型限制

map键不可以是函数类型、字典类型和切片类型(因为这几个具有引用语义)) 且 值必须要支持判等操作

另外,如果键的类型是接口类型,那么键的实际类型也不能是上述三种类型,否则在程序运行过程中会引发panic。

1
2
3
4
5
var badMap2 = map[interface{}]int{
"1":   1,
[]int{2}: 2, // 这里会引发 panic。
3:    3,
}

有人会疑问,为什么键类型的值必须支持判等操作?Go语言一旦定位到某一个哈希桶(bucket),那么就会试图在这个桶中查找键值。具体怎么查找的呢?
首先,每个哈希桶都会把自己包含的所有键的哈希值存起来。Go语言会用被查找键的哈希值与这些哈希值逐个对比,看看是否相等。如果没有相等的,那么说明这个桶中没有要查找的键值,这时Go语言就立即返回结果了。
如果有相等的,那么就用键值本身再去比较一次。为什么还要对比?原因是,不同值的哈希值可能是相同的,即哈希碰撞。

nil map的读写操作

除了添加键值对,我们在一个值为nil的字典上做任何操作都不会引起错误。

channel

通道满足FIFO原则
需要注意一个细节,元素值从外界进入通道时会被复制。更具体的说,进入通道的并不是在接收操作符右边的元素值,而是它的副本。
这个复制是浅复制

发送和接受操作什么时候会引发panic

如果试图关闭一个已经关闭了的通道,或对已关闭的通道做发送操作,就会引发panic

注意,如果通道关闭时,里面还有元素值未被取出,那么接受表达式的第一个结果,仍会是通道中的某一个元素值,而第二个参数值会是true。因此通道接受表达式第二个结果值来判断通道是否关闭时可能有延迟的。

除非有特殊保障措施,我们千万不要让接收方关闭通道,而应当让发送方做这件事。

单向通道

chan<- 发送通道
<-chan 接收通道

小口诀:左接收右发送

  • 这里的接收和发送都是站在操作通道的代码的角度上说的

常用于约束接口类型方法定义

1
2
3
4
5
6
7
8
type Notifier interface{
SendInt(ch chan<- int)
}
//在调用SendInt时,可以直接把双向通道传入,go在这里会自动将双向通道缓缓成单向通道
func SendInt(ch chan<- int) {
ch <- rand.Intn(1000)
}

for

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func getIntChan() <-chan int {
num := 5
ch := make(chan int, num)
for i := 0; i < num; i++ {
ch <- i
}
close(ch)
return ch
}
intChan2 := getIntChan()
for elem := range intChan2 {
fmt.Printf("The element in intChan2: %v\n", elem)
}

select

1
2
3
4
5
6
7
8
9
10
11
12
13
intChan := make(chan int, 1)
// 一秒后关闭通道。
time.AfterFunc(time.Second, func() {
close(intChan)
})
select {
case _, ok := <-intChan:
if !ok {
fmt.Println("The candidate case is closed.")
break
}
fmt.Println("The candidate case is selected.")
}

如果select语句发现同时又多个候选分支满足选择条件,那么它会用一种伪随机的算法在这些分支中选择一个并执行。

如果在select语句中发现某个通道已关闭,那么应怎样屏蔽掉它所在的分支

在发现通道关闭后,将通道赋值为nil,那么每次到这个nil的channel就会忽略掉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
intChan := make(chan int, 1)
// 一秒后关闭通道。
time.AfterFunc(time.Second, func() {
close(intChan)
})
for {
select {
case _, ok := <-intChan:
if !ok {
intChan = nil
break
}
default: //default必须有,否则会陷入死锁
fmt.Println("default")
}
}

case后语句执行顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var channels = [3]chan int{
nil,
make(chan int),
nil,
}
var numbers = []int{1, 2, 3}
func main() {
select {
case getChan(0) <- getNumber(0):
fmt.Println("The first candidate case is selected.")
case getChan(1) <- getNumber(1):
fmt.Println("The second candidate case is selected.")
case getChan(2) <- getNumber(2):
fmt.Println("The third candidate case is selected")
default:
fmt.Println("No candidate case is selected!")
}
}
func getNumber(i int) int {
fmt.Printf("numbers[%d]\n", i)
return numbers[i]
}
func getChan(i int) chan int {
fmt.Printf("channels[%d]\n", i)
return channels[i]
}
答案
channels[0]
numbers[0]
channels[1]
numbers[1]
channels[2]
numbers[2]
No candidate case is selected!

缓冲信道和无缓冲信道

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//例1
var c = make(chan int, 1)
func main() {
select {
case c <- 1:
fmt.Println("The second candidate case is selected.")
default:
fmt.Println("No candidate case is selected!")
}
}
输出:The second candidate case is selected.
//例2
var c = make(chan int)
func main() {
select {
case c <- 1:
fmt.Println("The second candidate case is selected.")
default:
fmt.Println("No candidate case is selected!")
}
}
输出: No candidate case is selected!

观察两者输出,想想为什么

函数

函数式编程

Go语言在语言层面上支持了函数式编程,函数是一等公民是函数式编程的重要特征。

在Go中函数是一等公民,这意味着函数不但可以用于封装代码、分割功能、解耦逻辑、还可以化身为普通的值在其他函数间传递、赋予变量、做类型判断和转换(就像切片和map)。

需要注意函数是引用类型,其零值为nil

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import "fmt"
type Printer func(content string) (n int, err error)
func printToStd(content string) (bytesNum int, err error) {
return fmt.Println(content)
}
func main() {
var p Printer
p = printToStd
p("something") // 打印 something
}

高阶函数

极客go36讲 第12讲

  1. 接受其他的函数作为参数传入
  2. 把其他的函数作为结果返回

只要满足其中一点,就可以说这个函数是一个高阶函数。高阶函数也是函数式编程中的重要概念和特征。

闭包

结构体

struct{}类型值的表示法为 struct{}{},并且它占用的内存空间是0字节。

确切的说,这个值在整个go程序中永远都只会存在一份,虽然我们可以无数次地使用这个值字面量,但是用到的却都是同一个值。

接口的一个陷阱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type Pet interface {
Name() string
Category() string
}
type Dog struct {
name string // 名字。
}
func (dog *Dog) SetName(name string) {
dog.name = name
}
func (dog Dog) Name() string {
return dog.name
}
func (dog Dog) Category() string {
return "dog"
}
dog := Dog{"little pig"}
var pet Pet = dog
dog.SetName("monster")
//此时dog的name是monster,而pet的name依旧是little pig

为什么dog的name字段值变了,而pet的却没变?这里有一个条规则: 当我们给接口变量赋值时,接口变量会持有被赋予值的副本
更重要的是,接口变量的值不等于这个值的副本。接口变量包含两个指针,一个指向动态值,一个指向类型信息。
基于此,即使我们把一个值为nil的某个实现类型的变量赋给接口变量,后者的值也不是真正的nil。虽然这时它指向的值是nil,但其类型不是nil

playground

1
2
3
4
5
6
func main(){
func(a interface{}) {
t := reflect.TypeOf(a)
fmt.Println(t) //接收的是interface,其中是保存有赋值变量的副本的,所以可以获取到具体类型
}(1)
}

playground

1
2
3
4
dog1 := Dog{"little pig"}
dog2 := dog1 // dog2是dog1的副本,两者是完全分离的
dog1.name = "monster"
fmt.Println(dog2.name) // little pig

interface底层结构包含两个指针,一个指向类型信息的指针,一个指动态值的指针。

接口变量的值在什么情况下真正为nil

1
2
3
4
5
6
7
8
9
10
var dog1 *Dog
fmt.Println("The first dog is nil. [wrap1]")
dog2 := dog1
fmt.Println("The second dog is nil. [wrap1]")
var pet Pet = dog2
if pet == nil {
fmt.Println("The pet is nil. [wrap1]")
} else {
fmt.Println("The pet is not nil. [wrap1]")
}

当interface底层2个指针都是nil时,interface才等于nil,所以我们把一个有类型的nil赋值给接口变量,这个接口变量的值一定不会是真正的nil。

那么怎样能让一个接口变量的值是真正的nil?

  • 变量只声明但不初始化
  • 直接把字面量nil赋给它

怎样实现接口之间的组合

接口类型间的嵌入相较结构体要简单一点,因为它不涉及方法屏蔽

需要注意当组合的接口之间有同名方法时会产生冲突,无法编译通过

协程

在1.8版本中go中为每一个goroutine分配了2kb的连续内存作为它的栈空间(这个栈空间大小一直在变化)

主goroutine

与一个进程总有一个主线程类似,每个独立的go程序在运行时总会有一个主goroutine。每条go语句一般都会带有一个函数调用,这个被调用的函数常常被称为go函数。而主goroutine的go函数就是那个作为程序入口的main函数

主goroutine还有一个特性,即: 一旦主goroutine中的代码执行完毕,当前的go程序就会结束运行

go函数与go语句执行是同时的吗

一定要注意,go函数真正执行的时间总会与其所属的go语句被执行的时间不同。当程序执行到一条go语句时,go语言的运行时系统会先试图去某个存放空闲goroutine的队列中获取一个goroutine,此时由两种情况

  • 它只有在找不到空闲goroutine的情况下才会去创建一个新的goroutine。这也是为什么我们总说”启用”一个goroutine而不是说”创建”一个goroutine的原因。已存在的goroutine总是会被优先复用。
  • 在拿到一个空闲的goroutine后,go语言运行时系统会用这个goroutine去包装当前的go函数,然后再去把这个goroutine追加到某个存放可运行的goroutine的队列中。
    这类队列中的goroutine总是会按先进先出的顺序,很快地由系统内部的调度器安排运行。因此go函数的执行时间总是只会于它所属的go语句的执行时间。

怎么样让我们启用的多个goroutine按照既定的顺序运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//方法1 借助mutex
var m = sync.Mutex{}
func main() {
var count uint32
for i := uint32(0); i < 10; i++ {
go func() {
m.Lock()
fmt.Println(count)
count++
m.Unlock()
}()
}
time.Sleep(time.Second)
}
//方法2 自旋锁
func main() {
var count uint32
trigger := func(i uint32, fn func()) {
for {
if n := atomic.LoadUint32(&count); n == i {
fn()
atomic.AddUint32(&count, 1)
break
}
time.Sleep(time.Nanosecond)
}
}
for i := uint32(0); i < 10; i++ {
go func(i uint32) {
fn := func() {
fmt.Println(i)
}
trigger(i, fn)
}(i)
}
trigger(10, func() {})
}

playground

range迭代的坑

range表达式实际迭代时是对变量的副本做迭代,至于会影响什么,那么就要看这个结果值的类型是值类型还是引用类型了。
playground

1
2
3
4
5
6
7
8
9
10
numbers2 := [...]int{1, 2, 3, 4, 5, 6}
maxIndex2 := len(numbers2) - 1
for i, e := range numbers2 { // 这里会对numbers2的副本做迭代
if i == maxIndex2 {
numbers2[0] += e
} else {
numbers2[i+1] += e
}
}
fmt.Println(numbers2) //[7 3 5 7 9 11]

把numbers2类型换成切片,结果是[22 3 6 10 15 21],这是由于引用类型拷贝底层指向的值是相同的

switch的坑

switch表达式的所有子表达式的结果值都是要与switch表达式的结果值判等的,因此它们的类型必须相同或者呢能够都统一到switch表达式的结果类型。如果无法做到,那么这条switch语句就不能通过编译

1
2
3
4
5
6
7
8
9
value1 := [...]int8{0, 1, 2, 3, 4, 5, 6}
switch 1 + 3 {
case value1[0], value1[1]: //invalid case value1[0] in switch on 1 + 3 (mismatched types int8 and int)
fmt.Println("0 or 1")
case value1[2], value1[3]:
fmt.Println("2 or 3")
case value1[4], value1[5], value1[6]:
fmt.Println("4 or 5 or 6")
}

最终输出是什么?答案是编译报错
switch语句对switch表达式的结果类型及各个case表达式中子表达式的结果类型都是有要求的。
如果switch表达式的结果值是无类型的常量,比如1+3的求值结果就是无类型的常量4,那么会被自动地转换为此常量的默认类型,比如整数4的默认类型是int,浮点数3.14的默认类型是float64。
因此,由于switch表达式结果类型是int,而case表达式中子表达式结果类型是int8,故这条switch语句无法编译通过。

1
2
3
4
5
6
7
8
9
value2 := [...]int8{0, 1, 2, 3, 4, 5, 6}
switch value2[4] {
case 0, 1:
fmt.Println("0 or 1")
case 2, 3:
fmt.Println("2 or 3")
case 4, 5, 6:
fmt.Println("4 or 5 or 6") // 4 or 5 or 6
}

再来看个很简单的例子,case表达式中子表达式的结果值是无类型的常量,那么它的类型也会被自动地转换为switch表达式的结果类型,又由于上述几个整数都可以被转换为int8类型的值,所以对这些表达式的结果值进行判等操作是没有问题的。当然如果类型转换不成功,则会引发panic

错误处理

错误类型
错误值的处理技巧
设计方式

panic、recover defer

多个defer时如何处理

将多个defer放入栈中,先进后出

测试

功能测试
基准测试
示例测试

同时写入造成的后果

同时有多个线程连续向同一个缓冲区写数据块时,如果没有机制去协调这些线程写操作的话,那么该数据块很有可能会出现错乱。

同步的作用

1.避免多个线程在同一时刻操作同一个数据块
2.协调多个线程,避免同一时刻执行同一个代码块

sync.Mutex

需要注意,syunc.Mutex一个结构体类型(值类型)。把它传递给一个函数、从函数中返回、赋给其他变量、让它进入某个通道都会产生它的副本,对值类型副本操作对原变量无影响

读写锁

对某个收到读写锁保护的共享资源,多个写操作不能同时进行,写操作和读操作不能同时进行,但多个读操作却可以同时进行

打印结构体

go语言打印结构体推荐使用”%+v”,而不是”%v”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
import "fmt"
type info struct {
name string
id int
}
func main() {
v := info{"Nan", 33}
fmt.Printf("%v\n", v)
fmt.Printf("%+v\n", v)
}
运行结果:
{Nan 33}
{name:Nan id:33}

go语言机制

栈与指针

  • 帧边界为每个函数提供了独立的内存空间,函数就是在自己的帧边界内执行的
  • 当调用函数时,上下文环境会在两个帧边界间切换
  • 按值传递的优点是可读性好
  • 栈是非常重要的,因为它为分配给每个函数的帧边界提供了可访问的物理内存空间
  • 在活动栈帧以下的栈空间是不可用的,只有活动栈帧和它之上的栈空间是可用的
  • 函数调用意味着 goroutine 需要在栈上为函数创建一个新的栈帧
  • 只有当发生了函数调用 ,栈区块被分配的栈帧占用后,相应栈空间才会被初始化
  • 使用指针是为了和被调用函数共享变量,使被调用函数可以用间接方式访问自己栈帧之外的变量
  • 每一个类型,不管是语言内置的还是用户定义的,都有一个与之对应的指针类型
  • 使用指针变量的函数,可以通过它间接访问函数栈帧之外的内存
    指针变量和其它变量一样,并不特殊,同样是有一块内存,在其中存放值而已
    20181022154019206145840.png