BuBu

Zwj`Blog

My decentralized blog
github
email

Go

Go#

基础#

命令行参数解析#

package main

import (
   // 需在此处添加代码。[1]
   "flag"
   "fmt"
)

var name string

func init() {
   // 需在此处添加代码。[2]
   flag.StringVar(&name, "name", "everyone", "The greeting object.")
}

func main() {
   // 需在此处添加代码。[3]
   flag.Parse()
   fmt.Printf("Hello, %s!\n", name)
}

执行

go run main.go -name="Robert"

输出

Hello, Robert!

变量#

定义#

//变量的声明
var a int = 10

简短声明#

//简短声明
b := 20
fmt.Println(a, b)

多重赋值#

//多重赋值
c, d, e := 30, 40, 50
fmt.Println(c, d, e)

交换变量值#

//交换变量值
f, g := 99, 88
fmt.Println(f, g)//99 88
f, g = g, f
fmt.Println(f, g)//88 99

下划线忽略值#

//忽略值/返回值
h, _ := 10, 20
fmt.Println(h)

格式化输出#

//格式输出
fmt.Printf("%d\n", 1)         //%d 1 以整数输出
fmt.Printf("%f\n", 1.1)       //%f 1.100000 以浮点型输出
fmt.Printf("%.2f\n", 1.11111) //%.2f 1.11 以浮点型输出,保留2位小数
fmt.Printf("%t\n", false)     //%t false 以布尔型数据输出
fmt.Printf("%s\n", "false")   //%s false 以字符串型数据输出
fmt.Printf("%c\n", 'a')       //%c 'a' 以字符型数据输出
fmt.Printf("%p\n", &a)        //%p 0x1400012c008 以指针数据输出地址
fmt.Printf("%T\n", a)  				//%T 输出该变量的类型

获取输入#

//获取用户输入
fmt.Scan(&a)   //输入88
fmt.Println(a) //输出88

字符和字节#

var n byte = 'a' //byte只能存单个字符
var m rune = '' //rune相当于其他语言里的char,存储单个Unicode字符/中文等等
fmt.Println(n,m)

常量#

定义#

常量存储一直不会发生变化的数据

//常量的定义
const a = false

iota 枚举#

常量声明可以使用 iota 常量生成器初始化,它用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。注意:在一个 const 声明语句中,在第一个声明的常量所在的行,iota 将会被置为 0,然后在每一个有常量声明的行加一。

const (
   b = iota
   c = iota
   d = iota
)
fmt.Println(b, c, d) //0 1 2

const (
   f = iota
   g
   h
)
fmt.Println(f, g, h) //0 1 2

如果定义枚举时,常量写在同一行值是相同的,换一行就加一

const (
   i    = iota
   j, k = iota, iota
)
fmt.Println(i, j, k) //0 1 1

运算符#

算数运算符#

运算符描述
+相加
-相减
*相乘
/相除
%求余

注意: ++(自增)和–(自减)在 Go 语言中是单独的语句,并不是运算符。

关系运算符#

运算符描述
==检查两个值是否相等,如果相等返回 True 否则返回 False。
!=检查两个值是否不相等,如果不相等返回 True 否则返回 False。
>检查左边值是否大于右边值,如果是返回 True 否则返回 False。
>=检查左边值是否大于等于右边值,如果是返回 True 否则返回 False。
<检查左边值是否小于右边值,如果是返回 True 否则返回 False。
<=检查左边值是否小于等于右边值,如果是返回 True 否则返回 False。

逻辑运算符#

运算符描述
&&逻辑 AND 运算符。 如果两边的操作数都是 True,则为 True,否则为 False。
ll逻辑 OR 运算符。 如果两边的操作数有一个 True,则为 True,否则为 False。
!逻辑 NOT 运算符。 如果条件为 True,则为 False,否则为 True。

位运算符#

位运算符对整数在内存中的二进制位进行操作。

运算符描述
&参与运算的两数各对应的二进位相与。(两位均为 1 才为 1)
l参与运算的两数各对应的二进位相或。(两位有一个为 1 就为 1)
^参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为 1。(两位不一样则为 1)
<<左移 n 位就是乘以 2 的 n 次方。“a<<b” 是把 a 的各二进位全部左移 b 位,高位丢弃,低位补 0。
>>右移 n 位就是除以 2 的 n 次方。“a>>b” 是把 a 的各二进位全部右移 b 位。

赋值运算符#

运算符描述
=简单的赋值运算符,将一个表达式的值赋给一个左值
+=相加后再赋值
-=相减后再赋值
*=相乘后再赋值
/=相除后再赋值
%=求余后再赋值
<<=左移后赋值
>>=右移后赋值
&=按位与后赋值
l=按位或后赋值
^=按位异或后赋值

类型转换#

//类型转换
c := 3
d := float64(c)
fmt.Println(c, d)

流程控制#

If 语句 (Go 不支持三目)#

• 可省略条件表达式括号。
• 持初始化语句,可定义代码块局部变量。 
• 代码块左 括号必须在条件表达式尾部。

if 布尔表达式 {
  /* 在布尔表达式为 true 时执行 */
} 

可以在表达式中声明变量

x := 0
if n := "abc"; x > 0 { // 初始化语句未必就是定义变量, 如 println("init") 也是可以的。
   println(n[2])
} else if x < 0 { // 注意 else if 和 else 左大括号位置。
   println(n[1])
} else {
   println(n[0])
}

Switch 语句#

语法#

Go 中默认带 Break, 如果不需要 Break 可以用 fallthrough 关键字

switch var1 {
    case val1:
        ...
    case val2:
        ...
    default:
        ...
}

变量 var1 可以是任何类型,而 val1 和 val2 则可以是同类型的任意值。类型不被局限于常量或整数,但必须是相同的类型;或者最终结果为相同类型的表达式。
可以同时测试多个可能符合条件的值,使用逗号分割它们,例如:case val1, val2, val3。

package main

import "fmt"

func main() {
   /* 定义局部变量 */
   var grade string = "B"
   var marks int = 90

   switch marks {
      case 90: grade = "A"
      case 80: grade = "B"
      case 50,60,70 : grade = "C"
      default: grade = "D"  
   }

   switch {
      case grade == "A" :
         fmt.Printf("优秀!\n" )     
      case grade == "B", grade == "C" :
         fmt.Printf("良好\n" )      
      case grade == "D" :
         fmt.Printf("及格\n" )      
      case grade == "F":
         fmt.Printf("不及格\n" )
      default:
         fmt.Printf("差\n" )
   }
   fmt.Printf("你的等级是 %s\n", grade )
}    

以上代码执行结果为:

优秀!
你的等级是 A  
Type Switch#
switch x.(type){
    case type:
       statement(s)      
    case type:
       statement(s)
    /* 你可以定义任意个数的case */
    default: /* 可选 */
       statement(s)
}  
实例#
package main

import "fmt"

func main() {
    var x interface{}
    //写法一:
    switch i := x.(type) { // 带初始化语句
    case nil:
        fmt.Printf(" x 的类型 :%T\r\n", i)
    case int:
        fmt.Printf("x 是 int 型")
    case float64:
        fmt.Printf("x 是 float64 型")
    case func(int) float64:
        fmt.Printf("x 是 func(int) 型")
    case bool, string:
        fmt.Printf("x 是 bool 或 string 型")
    default:
        fmt.Printf("未知型")
    }
    //写法二
    var j = 0
    switch j {
    case 0:
    case 1:
        fmt.Println("1")
    case 2:
        fmt.Println("2")
    default:
        fmt.Println("def")
    }
    //写法三
    var k = 0
    switch k {
    case 0:
        println("fallthrough")
        fallthrough
        /*
            Go的switch非常灵活,表达式不必是常量或整数,执行的过程从上至下,直到找到匹配项;
            而如果switch没有表达式,它会匹配true。
            Go里面switch默认相当于每个case最后带有break,
            匹配成功后不会自动向下执行其他case,而是跳出整个switch,
            但是可以使用fallthrough强制执行后面的case代码。
        */
    case 1:
        fmt.Println("1")
    case 2:
        fmt.Println("2")
    default:
        fmt.Println("def")
    }
    //写法三
    var m = 0
    switch m {
    case 0, 1:
        fmt.Println("1")
    case 2:
        fmt.Println("2")
    default:
        fmt.Println("def")
    }
    //写法四
    var n = 0
    switch { //省略条件表达式,可当 if...else if...else
    case n > 0 && n < 10:
        fmt.Println("i > 0 and i < 10")
    case n > 10 && n < 20:
        fmt.Println("i > 10 and i < 20")
    default:
        fmt.Println("def")
    }
}   

以上代码执行结果为:

    x 的类型 :<nil>
    fallthrough
    1
    1
    def

For 语句#

for 循环是一个循环控制结构,可以执行指定次数的循环。

语法#

Go 语言的 For 循环有 3 中形式,只有其中的一种使用分号。

    for init; condition; post { }
    for condition { }
    for { }
    init: 一般为赋值表达式,给控制变量赋初值;
    condition: 关系表达式或逻辑表达式,循环控制条件;
    post: 一般为赋值表达式,给控制变量增量或减量。
    for语句执行过程如下:
    ①先对表达式 init 赋初值;
    ②判别赋值表达式 init 是否满足给定 condition 条件,若其值为真,满足循环条件,则执行循环体内语句,然后执行 post,进入第二次循环,再判别 condition;否则判断 condition 的值为假,不满足条件,就终止for循环,执行循环体外语句。   
s := "abc"

for i, n := 0, len(s); i < n; i++ { // 常见的 for 循环,支持初始化语句。
    println(s[i])
}

n := len(s)
for n > 0 {                // 替代 while (n > 0) {}
    n-- 
    println(s[n])        // 替代 for (; n > 0;) {}
}

for {                    // 替代 while (true) {}
    println(s)            // 替代 for (;;) {}
}  

不要期望编译器能理解你的想法,在初始化语句中计算出全部结果是个好主意。

package main

func length(s string) int {
    println("call length.")
    return len(s)
}

func main() {
    s := "abcd"

    for i, n := 0, length(s); i < n; i++ {     // 避免多次调用 length 函数。
        println(i, s[i])
    } 
}  

输出:

    call length.
    0 97
    1 98
    2 99
    3 100 
实例#
package main

import "fmt"

func main() {

   var b int = 15
   var a int

   numbers := [6]int{1, 2, 3, 5}

   /* for 循环 */
   for a := 0; a < 10; a++ {
      fmt.Printf("a 的值为: %d\n", a)
   }

   for a < b {
      a++
      fmt.Printf("a 的值为: %d\n", a)
      }

   for i,x:= range numbers {
      fmt.Printf("第 %d 位 x 的值 = %d\n", i,x)
   }   
} 

以上实例运行输出结果为:

    a 的值为: 0
    a 的值为: 1
    a 的值为: 2
    a 的值为: 3
    a 的值为: 4
    a 的值为: 5
    a 的值为: 6
    a 的值为: 7
    a 的值为: 8
    a 的值为: 9
    a 的值为: 1
    a 的值为: 2
    a 的值为: 3
    a 的值为: 4
    a 的值为: 5
    a 的值为: 6
    a 的值为: 7
    a 的值为: 8
    a 的值为: 9
    a 的值为: 10
    a 的值为: 11
    a 的值为: 12
    a 的值为: 13
    a 的值为: 14
    a 的值为: 15
    第 0 位 x 的值 = 1
    第 1 位 x 的值 = 2
    第 2 位 x 的值 = 3
    第 3 位 x 的值 = 5
    第 4 位 x 的值 = 0
    第 5 位 x 的值 = 0  
循环嵌套#

在 for 循环中嵌套一个或多个 for 循环

语法

以下为 Go 语言嵌套循环的格式:

for [condition |  ( init; condition; increment ) | Range]
{
   for [condition |  ( init; condition; increment ) | Range]
   {
      statement(s)
   }
   statement(s)
}  
实例

以下实例使用循环嵌套来输出 2 到 100 间的素数:

package main

import "fmt"

func main() {
   /* 定义局部变量 */
   var i, j int

   for i=2; i < 100; i++ {
      for j=2; j <= (i/j); j++ {
         if(i%j==0) {
            break // 如果发现因子,则不是素数
         }
      }
      if(j > (i/j)) {
         fmt.Printf("%d  是素数\n", i)
      }
   }  
}  

以上实例运行输出结果为:

    2  是素数
    3  是素数
    5  是素数
    7  是素数
    11  是素数
    13  是素数
    17  是素数
    19  是素数
    23  是素数
    29  是素数
    31  是素数
    37  是素数
    41  是素数
    43  是素数
    47  是素数
    53  是素数
    59  是素数
    61  是素数
    67  是素数
    71  是素数
    73  是素数
    79  是素数
    83  是素数
    89  是素数
    97  是素数  
无限循环#

如过循环中条件语句永远不为 false 则会进行无限循环,我们可以通过 for 循环语句中只设置一个条件表达式来执行无限循环:

package main

import "fmt"

func main() {
    for true  {
        fmt.Printf("这是无限循环。\n");
    }
}  

Range 语句#

语法#

Golang range 类似迭代器操作,返回 (索引,值) 或 (键,值)。

for 循环的 range 格式可以对 slice、map、数组、字符串等进行迭代循环。格式如下:

for key, value := range oldMap {
    newMap[key] = value
}   
1st value2nd value
stringindexs[index]unicode, rune
array/sliceindexs[index]
mapkeym[key]
channelelement

可忽略不想要的返回值,或 "_" 这个特殊变量。

package main

func main() {
    s := "abc"
    // 忽略 2nd value,支持 string/array/slice/map。
    for i := range s {
        println(s[i])
    }
    // 忽略 index。
    for _, c := range s {
        println(c)
    }
    // 忽略全部返回值,仅迭代。
    for range s {

    }

    m := map[string]int{"a": 1, "b": 2}
    // 返回 (key, value)。
    for k, v := range m {
        println(k, v)
    }
}   

输出结果:

    97
    98
    99
    97
    98
    99
    a 1
    b 2  
重要注意#

*注意,range 会复制对象。

package main

import "fmt"

func main() {
    a := [3]int{0, 1, 2}

    for i, v := range a { // index、value 都是从复制品中取出。

        if i == 0 { // 在修改前,我们先修改原数组。
            a[1], a[2] = 999, 999
            fmt.Println(a) // 确认修改有效,输出 [0, 999, 999]。
        }

        a[i] = v + 100 // 使用复制品中取出的 value 修改原数组。

    }

    fmt.Println(a) // 输出 [100, 101, 102]。
}   

输出结果:

    [0 999 999]
    [100 101 102]   

建议改用引用类型,其底层数据不会被复制。

package main

func main() {
    s := []int{1, 2, 3, 4, 5}

    for i, v := range s { // 复制 struct slice { pointer, len, cap }。

        if i == 0 {
            s = s[:3]  // 对 slice 的修改,不会影响 range。
            s[2] = 100 // 对底层数据的修改。
        }

        println(i, v)
    }
}   

输出结果:

    0 1
    1 2
    2 100
    3 4
    4 5

另外两种引用类型 map、channel 是指针包装,而不像 slice 是 struct。

for 和 range 区别#

主要是使用场景不同

for 可以 遍历 array 和 slice || 遍历 key 为整型递增的 map || 遍历 string

for range 可以完成所有 for 可以做的事情,却能做到 for 不能做的,包括

遍历 key 为 string 类型的 map 并同时获取 key 和 value || 遍历 channel

函数#

定义#

函数特点#
    • 无需声明原型。
    • 支持不定 变参。
    • 支持多返回值。
    • 支持命名返回参数。 
    • 支持匿名函数和闭包。
    • 函数也是一种类型,一个函数可以赋值给变量。

    • 不支持 嵌套 (nested) 一个包不能有两个名字一样的函数。
    • 不支持 重载 (overload) 
    • 不支持 默认参数 (default parameter)。 
函数声明#

函数声明包含一个函数名,参数列表, 返回值列表和函数体。如果函数没有返回值,则返回列表可以省略。函数从第一条语句开始执行,直到执行 return 语句或者执行函数的最后一条语句。

函数可以没有参数或接受多个参数。

注意类型在变量名之后 。

当两个或多个连续的函数命名参数是同一类型,则除了最后一个类型之外,其他都可以省略。

函数可以返回任意数量的返回值。

使用关键字 func 定义函数,左大括号依旧不能另起一行。

func test(x, y int, s string) (int, string) {
    // 类型相同的相邻参数,参数类型可合并。 多返回值必须用括号。
    n := x + y          
    return n, fmt.Sprintf(s, n)
}

函数是第一类对象,可作为参数传递。建议将复杂签名定义为函数类型,以便于阅读。

package main

import "fmt"

func test(fn func() int) int {
    return fn()
}
// 定义函数类型。
type FormatFunc func(s string, x, y int) string 

func format(fn FormatFunc, s string, x, y int) string {
    return fn(s, x, y)
}

func main() {
    s1 := test(func() int { return 100 }) // 直接将匿名函数当参数。

    s2 := format(func(s string, x, y int) string {
        return fmt.Sprintf(s, x, y)
    }, "%d, %d", 10, 20)

    println(s1, s2)
}

输出结果:

    100 10, 20

有返回值的函数,必须有明确的终止语句,否则会引发编译错误。

参数#

普通形参#

函数定义时指出,函数定义时有参数,该变量可称为函数的形参。形参就像定义在函数体内的局部变量。

但当调用函数,传递过来的变量就是函数的实参,函数可以通过两种方式来传递参数:

值传递

指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。

func swap(x, y int) int {
       ... ...
  }
引用传递

是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

package main

import (
    "fmt"
)

/* 定义相互交换值的函数 */
func swap(x, y *int) {
    var temp int

    temp = *x /* 保存 x 的值 */
    *x = *y   /* 将 y 值赋给 x */
    *y = temp /* 将 temp 值赋给 y*/

}

func main() {
    var a, b int = 1, 2
    /*
        调用 swap() 函数
        &a 指向 a 指针,a 变量的地址
        &b 指向 b 指针,b 变量的地址
    */
    swap(&a, &b)

    fmt.Println(a, b)
}

输出结果:

    2 1

在默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。

注意 1

无论是值传递,还是引用传递,传递给函数的都是变量的副本,不过,值传递是值的拷贝。引用传递是地址的拷贝,一般来说,地址拷贝更为高效。而值拷贝取决于拷贝的对象大小,对象越大,则性能越低。

注意 2

map、slice、chan、指针、interface 默认以引用的方式传递。

不定参数#

不定参数传值就是函数的参数不是固定的,后面的类型是固定的。(可变参数)

Golang 可变参数本质上就是 slice。只能有一个,且必须是最后一个。

在参数赋值时可以不用用一个一个的赋值,可以直接传递一个数组或者切片,特别注意的是在参数后加上 “…” 即可。

func main() {
   //不定参函数
   test(1, 2, 3, 4)//[1 2 3 4]
}

func test(args ...int) {
   fmt.Println(args)
}
Slice 传入不定参方法

使用 slice 对象做变参时,必须展开。(slice...)

package main

import (
    "fmt"
)

func test(s string, n ...int) string {
    var x int
    for _, i := range n {
        x += i
    }

    return fmt.Sprintf(s, x)
}

func main() {
    s := []int{1, 2, 3}
    res := test("sum: %d", s...)    // slice... 展开slice
    println(res)
}

返回值#

"_"标识符,用来忽略函数的某个返回值

Go 的返回值可以被命名,并且就像在函数体开头声明的变量那样使用。

返回值的名称应当具有一定的意义,可以作为文档使用。

没有参数的 return 语句返回各个返回变量的当前值。这种用法被称作 “裸” 返回。

直接返回语句仅应当用在像下面这样的短函数中。在长的函数中它们会影响代码的可读性。

package main

import (
    "fmt"
)

func add(a, b int) (c int) {
    c = a + b
    return
}

func calc(a, b int) (sum int, avg int) {
    sum = a + b
    avg = (a + b) / 2

    return
}

func main() {
    var a, b int = 1, 2
    c := add(a, b)
    sum, avg := calc(a, b)
    fmt.Println(a, b, c, sum, avg)
}

输出结果:

    1 2 3 3 1 

Golang 返回值不能用容器对象接收多返回值。只能用多个变量,或 "_" 忽略。

package main

func test() (int, int) {
    return 1, 2
}

func main() {
    // s := make([]int, 2)
    // s = test()   // Error: multiple-value test() in single-value context

    x, _ := test()
    println(x)
}

输出结果:

    1 

多返回值可直接作为其他函数调用实参。

package main

func test() (int, int) {
    return 1, 2
}

func add(x, y int) int {
    return x + y
}

func sum(n ...int) int {
    var x int
    for _, i := range n {
        x += i
    }

    return x
}

func main() {
    println(add(test()))
    println(sum(test()))
}

输出结果:

    3
    3

命名返回参数可看做与形参类似的局部变量,最后由 return 隐式返回。

package main

func add(x, y int) (z int) {
    z = x + y
    return
}

func main() {
    println(add(1, 2))
}

输出结果:

    3  

命名返回参数可被同名局部变量遮蔽,此时需要显式返回。

func add(x, y int) (z int) {
    { // 不能在一个级别,引发 "z redeclared in this block" 错误。
        var z = x + y
        // return   // Error: z is shadowed during return
        return z // 必须显式返回。
    }
}

命名返回参数允许 defer 延迟调用通过闭包读取和修改。

package main

func add(x, y int) (z int) {
    defer func() {
        z += 100
    }()

    z = x + y
    return
}

func main() {
    println(add(1, 2)) 
}

输出结果:

    103

显式 return 返回前,会先修改命名返回参数。

package main

func add(x, y int) (z int) {
    defer func() {
        println(z) // 输出: 203
    }()

    z = x + y
    return z + 200 // 执行顺序: (z = z + 200) -> (call defer) -> (return)
}

func main() {
    println(add(1, 2)) // 输出: 203
}

输出结果:

    203
    203

匿名函数#

匿名函数是指不需要定义函数名的一种函数实现方式。1958 年 LISP 首先采用匿名函数。

在 Go 里面,函数可以像普通变量一样被传递或使用,Go 语言支持随时在代码里定义匿名函数。

匿名函数由一个不带函数名的函数声明和函数体组成。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。

package main

import (
    "fmt"
    "math"
)

func main() {
    getSqrt := func(a float64) float64 {
        return math.Sqrt(a)
    }
    fmt.Println(getSqrt(4))
}

输出结果:

 2
闭包#

因为函数在调用结束后会进行销毁,所有函数内部的变量值没有进行保存

func test1(a int) {
   a++
   fmt.Println(a)
}
func main() {
   a := 1
   for i := 0; i < 10; i++ {
      test1(a)
   }
}

输出结果

2
2
2
2
2
2
2
2
2
2

返回值也是函数的函数称为闭包

可以通过匿名函数和闭包 实现函数在栈区实现持久化

func main(){
  a := 1
  f := test2(a)
  for i := 0; i < 10; i++ {
     fmt.Println(f())
  }
}
func test2(a int) func() int {
   return func() int {
      
      return a
   }
}

输出结果

2
3
4
5
6
7
8
9
10
11

递归函数#

递归,就是在运行的过程中调用自己。
一个函数调用自己,就叫做递归函数。

构成递归需具备的条件:

    1.子问题须与原始问题为同样的事,且更为简单。
    2.不能无限制地调用本身,须有个出口,化简为非递归状况处理。
数字阶乘#
package main

import "fmt"

func factorial(i int) int {
    if i <= 1 {
        return 1
    }
    return i * factorial(i-1)
}

func main() {
    var i int = 7
    fmt.Printf("Factorial of %d is %d\n", i, factorial(i))
}

输出结果:

    Factorial of 7 is 5040
斐波那契数列#

这个数列从第 3 项开始,每一项都等于前两项之和。

package main

import "fmt"

func fibonaci(i int) int {
    if i == 0 {
        return 0
    }
    if i == 1 {
        return 1
    }
    return fibonaci(i-1) + fibonaci(i-2)
}

func main() {
    var i int
    for i = 0; i < 10; i++ {
        fmt.Printf("%d\n", fibonaci(i))
    }
}

输出结果:

    0
    1
    1
    2
    3
    5
    8
    13
    21
    34

defer 延迟调用#

defer 特性#
    1. 关键字 defer 用于注册延迟调用。
    2. 这些调用直到 return 前才被执。因此,可以用来做资源清理。
    3. 多个defer语句,按先进后出的方式执行。
    4. defer语句中的变量,在defer声明时就决定了。
defer 用途#
    1. 关闭文件句柄
    2. 锁资源释放
    3. 数据库连接释放

defer 是先进后出的

package main

import "fmt"

func main() {
    var whatever [5]struct{}

    for i := range whatever {
        defer fmt.Println(i)
    }
} 

输出结果:

    4
    3
    2
    1
    0
defer 和闭包#

image-20220416190037239

defer 会延迟函数的执行,虽然立即调用了匿名函数,但是该匿名函数不会执行,等整个 main ( ) 函数结束之前在去调用执行匿名函数

image-20220416190056827

由于匿名函数前面加上了 defer 所以,匿名函数没有立即执行。但是问题是,程序从上开始执行当执行到匿名函数时,虽然没有立即调用执行匿名函数,但是已经完成了参数的传递。

defer 和异常#

多个 defer 注册,按 FILO 次序执行 (先进后出)。哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。

package main

func test(x int) {
    defer println("a")
    defer println("b")

    defer func() {
        println(100 / x) // div0 异常未被捕获,逐步往外传递,最终终止进程。
    }()

    defer println("c")
}

func main() {
    test(0)
} 

输出结果:

    c
    b
    a
    panic: runtime error: integer divide by zero
重要#

*延迟调用参数在注册时求值或复制,可用指针或闭包 “延迟” 读取。

package main

func test() {
    x, y := 10, 20

    defer func(i int) {
        println("defer:", i, y) // y 闭包引用
    }(x) // x 被复制 <<<<<------这里闭包将参数传递进来了

    x += 10
    y += 100
    println("x =", x, "y =", y)
}

func main() {
    test()
}  

输出结果:

    x = 20 y = 120
    defer: 10 120
滥用 defer#

*滥用 defer 可能会导致性能问题,尤其是在一个 “大循环” 里。

package main

import (
    "fmt"
    "sync"
    "time"
)

var lock sync.Mutex

func test() {
    lock.Lock()
    lock.Unlock()
}

func testdefer() {
    lock.Lock()
    defer lock.Unlock()
}

func main() {
    func() {
        t1 := time.Now()

        for i := 0; i < 10000; i++ {
            test()
        }
        elapsed := time.Since(t1)
        fmt.Println("test elapsed: ", elapsed)
    }()
    func() {
        t1 := time.Now()

        for i := 0; i < 10000; i++ {
            testdefer()
        }
        elapsed := time.Since(t1)
        fmt.Println("testdefer elapsed: ", elapsed)
    }()

}

输出结果:

    test elapsed:  223.162µs
    testdefer elapsed:  781.304µs
defer 陷阱#
defer 与 closure
package main

import (
    "errors"
    "fmt"
)

func foo(a, b int) (i int, err error) {
    defer fmt.Printf("first defer err %v\n", err)
    defer func(err error) { fmt.Printf("second defer err %v\n", err) }(err)
    defer func() { fmt.Printf("third defer err %v\n", err) }()
    if b == 0 {
        err = errors.New("divided by zero!")
        return
    }

    i = a / b
    return
}

func main() {
    foo(2, 0)
}  

输出结果:

    third defer err divided by zero!
    second defer err <nil>
    first defer err <nil>

解释:如果 defer 后面跟的不是一个 closure 最后执行的时候我们得到的并不是最新的值。

defer 与 return
package main

import "fmt"

func foo() (i int) {

    i = 0
    defer func() {
        fmt.Println(i)
    }()

    return 2
}

func main() {
    foo()
}

输出结果:

    2

解释:在有具名返回值的函数中(这里具名返回值为 i),执行 return 2 的时候实际上已经将 i 的值重新赋值为 2。所以 defer closure 输出结果为 2 而不是 1。

defer nil 函数
package main

import (
    "fmt"
)

func test() {
    var run func() = nil
    defer run()
    fmt.Println("runs")
}

func main() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err)
        }
    }()
    test()
} 

输出结果:

runs
runtime error: invalid memory address or nil pointer dereference

解释:名为 test 的函数一直运行至结束,然后 defer 函数会被执行且会因为值为 nil 而产生 panic 异常。然而值得注意的是,run () 的声明是没有问题,因为在 test 函数运行完成后它才会被调用。

数组#

    1. 数组:是同一种数据类型的固定长度的序列。
    2. 数组定义:var a [len]int,比如:var a [5]int,数组长度必须是常量,且是类型的组成部分。一旦定义,长度不能变。
    3. 长度是数组类型的一部分,因此,var a[5] int和var a[10]int是不同的类型。
    4. 数组可以通过下标进行访问,下标是从0开始,最后一个元素下标是:len-1
    for i := 0; i < len(a); i++ {
    }
    for index, v := range a {
    }
    5. 访问越界,如果下标在数组合法范围之外,则触发访问越界,会panic
    6. 数组是值类型,赋值和传参会复制整个数组,而不是指针。因此改变副本的值,不会改变本身的值。
    7.支持 "=="、"!=" 操作符,因为内存总是被初始化过的。
    8.指针数组 [n]*T,数组指针 *[n]T。

数组初始化#

一维数组#
    全局:
    var arr0 [5]int = [5]int{1, 2, 3}
    var arr1 = [5]int{1, 2, 3, 4, 5}
    var arr2 = [...]int{1, 2, 3, 4, 5, 6}
    var str = [5]string{3: "hello world", 4: "tom"}
    局部:
    a := [3]int{1, 2}           // 未初始化元素值为 0。
    b := [...]int{1, 2, 3, 4}   // 通过初始化值确定数组长度。
    c := [5]int{2: 100, 4: 200} // 使用索引号初始化元素。
    d := [...]struct {
        name string
        age  uint8
    }{
        {"user1", 10}, // 可省略元素类型。
        {"user2", 20}, // 别忘了最后一行的逗号。
    }

代码:

package main

import (
    "fmt"
)

var arr0 [5]int = [5]int{1, 2, 3}
var arr1 = [5]int{1, 2, 3, 4, 5}
var arr2 = [...]int{1, 2, 3, 4, 5, 6}
var str = [5]string{3: "hello world", 4: "tom"}

func main() {
    a := [3]int{1, 2}           // 未初始化元素值为 0。
    b := [...]int{1, 2, 3, 4}   // 通过初始化值确定数组长度。
    c := [5]int{2: 100, 4: 200} // 使用引号初始化元素。
    d := [...]struct {
        name string
        age  uint8
    }{
        {"user1", 10}, // 可省略元素类型。
        {"user2", 20}, // 别忘了最后一行的逗号。
    }
    fmt.Println(arr0, arr1, arr2, str)
    fmt.Println(a, b, c, d)
}

输出结果:

[1 2 3 0 0] [1 2 3 4 5] [1 2 3 4 5 6] [   hello world tom]
[1 2 0] [1 2 3 4] [0 0 100 0 200] [{user1 10} {user2 20}]
多维数组#
    全局
    var arr0 [5][3]int
    var arr1 [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}
    局部:
    a := [2][3]int{{1, 2, 3}, {4, 5, 6}}
    b := [...][2]int{{1, 1}, {2, 2}, {3, 3}} // 第 2 纬度不能用 "..."。

代码:

package main

import (
    "fmt"
)

var arr0 [5][3]int
var arr1 [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}

func main() {
    a := [2][3]int{{1, 2, 3}, {4, 5, 6}}
    b := [...][2]int{{1, 1}, {2, 2}, {3, 3}} // 第 2 纬度不能用 "..."。
    fmt.Println(arr0, arr1)
    fmt.Println(a, b)
}

输出结果:

    [[0 0 0] [0 0 0] [0 0 0] [0 0 0] [0 0 0]] [[1 2 3] [7 8 9]]
    [[1 2 3] [4 5 6]] [[1 1] [2 2] [3 3]]

值拷贝行为会造成性能问题,通常会建议使用 slice,或数组指针。

package main

import (
    "fmt"
)

func test(x [2]int) {
    fmt.Printf("x: %p\n", &x)
    x[1] = 1000
}

func main() {
    a := [2]int{}
    fmt.Printf("a: %p\n", &a)

    test(a)
    fmt.Println(a)
}

输出结果:

    a: 0xc42007c010
    x: 0xc42007c030
    [0 0]

内置函数 len 和 cap 都返回数组长度 (元素数量)。

package main

func main() {
    a := [2]int{}
    println(len(a), cap(a)) 
}

输出结果:

2 2
多维数组遍历
package main

import (
    "fmt"
)

func main() {

    var f [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}

    for k1, v1 := range f {
        for k2, v2 := range v1 {
            fmt.Printf("(%d,%d)=%d ", k1, k2, v2)
        }
        fmt.Println()
    }
}

输出结果:

    (0,0)=1 (0,1)=2 (0,2)=3 
    (1,0)=7 (1,1)=8 (1,2)=9 
数组拷贝和传参#
package main

import "fmt"

func printArr(arr *[5]int) {
    arr[0] = 10
    for i, v := range arr {
        fmt.Println(i, v)
    }
}

func main() {
    var arr1 [5]int
    printArr(&arr1)
    fmt.Println(arr1)
    arr2 := [...]int{2, 4, 6, 8, 10}
    printArr(&arr2)
    fmt.Println(arr2)
}

切片#

slice 并不是数组或数组指针。它通过内部指针和相关属性引用数组片段,以实现变长方案。

    1. 切片:切片是数组的一个引用,因此切片是引用类型。但自身是结构体,值拷贝传递。
    2. 切片的长度可以改变,因此,切片是一个可变的数组。
    3. 切片遍历方式和数组一样,可以用len()求长度。表示可用元素数量,读写操作不能超过该限制。 
    4. cap可以求出slice最大扩张容量,不能超出数组限制。0 <= len(slice) <= len(array),其中array是slice引用的数组。
    5. 切片的定义:var 变量名 []类型,比如 var str []string  var arr []int。
    6. 如果 slice == nil,那么 len、cap 结果都等于 0。

切片创建#

package main

import "fmt"

func main() {
   //1.声明切片
   var s1 []int
   if s1 == nil {
      fmt.Println("是空")
   } else {
      fmt.Println("不是空")
   }
   // 2.:=
   s2 := []int{}
   // 3.make()
   var s3 []int = make([]int, 0)
   fmt.Println(s1, s2, s3)
   // 4.初始化赋值
   var s4 []int = make([]int, 0, 0)
   fmt.Println(s4)
   s5 := []int{1, 2, 3}
   fmt.Println(s5)
   // 5.从数组切片
   arr := [5]int{1, 2, 3, 4, 5}
   var s6 []int
   // 前包后不包
   s6 = arr[1:4]
   fmt.Println(s6)
}

切片初始化#

//全局:
var arr = [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
var slice0 []int = arr[start:end]
var slice1 []int = arr[:end]
var slice2 []int = arr[start:]
var slice3 []int = arr[:]
var slice4 = arr[:len(arr)-1] //去掉切片的最后一个元素
//局部:
arr2 := [...]int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
slice5 := arr[start:end]
slice6 := arr[:end]
slice7 := arr[start:]
slice8 := arr[:]
slice9 := arr[:len(arr)-1] //去掉切片的最后一个元素

img

代码:

package main

import (
    "fmt"
)

var arr = [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
var slice0 []int = arr[2:8]
var slice1 []int = arr[0:6]        //可以简写为 var slice []int = arr[:end]
var slice2 []int = arr[5:10]       //可以简写为 var slice[]int = arr[start:]
var slice3 []int = arr[0:len(arr)] //var slice []int = arr[:]
var slice4 = arr[:len(arr)-1]      //去掉切片的最后一个元素
func main() {
    fmt.Printf("全局变量:arr %v\n", arr)
    fmt.Printf("全局变量:slice0 %v\n", slice0)
    fmt.Printf("全局变量:slice1 %v\n", slice1)
    fmt.Printf("全局变量:slice2 %v\n", slice2)
    fmt.Printf("全局变量:slice3 %v\n", slice3)
    fmt.Printf("全局变量:slice4 %v\n", slice4)
    fmt.Printf("-----------------------------------\n")
    arr2 := [...]int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
    slice5 := arr[2:8]
    slice6 := arr[0:6]         //可以简写为 slice := arr[:end]
    slice7 := arr[5:10]        //可以简写为 slice := arr[start:]
    slice8 := arr[0:len(arr)]  //slice := arr[:]
    slice9 := arr[:len(arr)-1] //去掉切片的最后一个元素
    fmt.Printf("局部变量: arr2 %v\n", arr2)
    fmt.Printf("局部变量: slice5 %v\n", slice5)
    fmt.Printf("局部变量: slice6 %v\n", slice6)
    fmt.Printf("局部变量: slice7 %v\n", slice7)
    fmt.Printf("局部变量: slice8 %v\n", slice8)
    fmt.Printf("局部变量: slice9 %v\n", slice9)
}

输出结果:

    全局变量:arr [0 1 2 3 4 5 6 7 8 9]
    全局变量:slice0 [2 3 4 5 6 7]
    全局变量:slice1 [0 1 2 3 4 5]
    全局变量:slice2 [5 6 7 8 9]
    全局变量:slice3 [0 1 2 3 4 5 6 7 8 9]
    全局变量:slice4 [0 1 2 3 4 5 6 7 8]
    -----------------------------------
    局部变量: arr2 [9 8 7 6 5 4 3 2 1 0]
    局部变量: slice5 [2 3 4 5 6 7]
    局部变量: slice6 [0 1 2 3 4 5]
    局部变量: slice7 [5 6 7 8 9]
    局部变量: slice8 [0 1 2 3 4 5 6 7 8 9]
    局部变量: slice9 [0 1 2 3 4 5 6 7 8]

通过 make 来创建切片#

    var slice []type = make([]type, len)
    slice  := make([]type, len)
    slice  := make([]type, len, cap)

null

代码:

package main

import (
    "fmt"
)

var slice0 []int = make([]int, 10)
var slice1 = make([]int, 10)
var slice2 = make([]int, 10, 10)

func main() {
    fmt.Printf("make全局slice0 :%v\n", slice0)
    fmt.Printf("make全局slice1 :%v\n", slice1)
    fmt.Printf("make全局slice2 :%v\n", slice2)
    fmt.Println("--------------------------------------")
    slice3 := make([]int, 10)
    slice4 := make([]int, 10)
    slice5 := make([]int, 10, 10)
    fmt.Printf("make局部slice3 :%v\n", slice3)
    fmt.Printf("make局部slice4 :%v\n", slice4)
    fmt.Printf("make局部slice5 :%v\n", slice5)
}

输出结果:

    make全局slice0 :[0 0 0 0 0 0 0 0 0 0]
    make全局slice1 :[0 0 0 0 0 0 0 0 0 0]
    make全局slice2 :[0 0 0 0 0 0 0 0 0 0]
    --------------------------------------
    make局部slice3 :[0 0 0 0 0 0 0 0 0 0]
    make局部slice4 :[0 0 0 0 0 0 0 0 0 0]
    make局部slice5 :[0 0 0 0 0 0 0 0 0 0]

切片的内存布局

null

读写操作实际目标是底层数组,只需注意索引号的差别。

package main

import (
    "fmt"
)

func main() {
    data := [...]int{0, 1, 2, 3, 4, 5}

    s := data[2:4]
    s[0] += 100
    s[1] += 200

    fmt.Println(s)
    fmt.Println(data)
}

输出:

    [102 203]
    [0 1 102 203 4 5]

可直接创建 slice 对象,自动分配底层数组。

package main

import "fmt"

func main() {
    s1 := []int{0, 1, 2, 3, 8: 100} // 通过初始化表达式构造,可使用索引号。
    fmt.Println(s1, len(s1), cap(s1))

    s2 := make([]int, 6, 8) // 使用 make 创建,指定 len 和 cap 值。
    fmt.Println(s2, len(s2), cap(s2))

    s3 := make([]int, 6) // 省略 cap,相当于 cap = len。
    fmt.Println(s3, len(s3), cap(s3))
}

输出结果:

    [0 1 2 3 0 0 0 0 100] 9 9
    [0 0 0 0 0 0] 6 8
    [0 0 0 0 0 0] 6 6

使用 make 动态创建 slice,避免了数组必须用常量做长度的麻烦。还可用指针直接访问底层数组,退化成普通数组操作。

package main

import "fmt"

func main() {
    s := []int{0, 1, 2, 3}
    p := &s[2] // *int, 获取底层数组元素指针。
    *p += 100

    fmt.Println(s)
}

输出结果:

    [0 1 102 3]

至于 [][] T,是指元素类型为 [] T 。

package main

import (
    "fmt"
)

func main() {
    data := [][]int{
        []int{1, 2, 3},
        []int{100, 200},
        []int{11, 22, 33, 44},
    }
    fmt.Println(data)
}

输出结果:

    [[1 2 3] [100 200] [11 22 33 44]]

切片追加#

用 append 内置函数操作切片(切片追加

package main

import (
    "fmt"
)

func main() {

    var a = []int{1, 2, 3}
    fmt.Printf("slice a : %v\n", a)
    var b = []int{4, 5, 6}
    fmt.Printf("slice b : %v\n", b)
    c := append(a, b...)
    fmt.Printf("slice c : %v\n", c)
    d := append(c, 7)
    fmt.Printf("slice d : %v\n", d)
    e := append(d, 8, 9, 10)
    fmt.Printf("slice e : %v\n", e)

}

输出结果:

    slice a : [1 2 3]
    slice b : [4 5 6]
    slice c : [1 2 3 4 5 6]
    slice d : [1 2 3 4 5 6 7]
    slice e : [1 2 3 4 5 6 7 8 9 10]

append :向 slice 尾部添加数据,返回新的 slice 对象。

package main

import (
    "fmt"
)

func main() {

    s1 := make([]int, 0, 5)
    fmt.Printf("%p\n", &s1)

    s2 := append(s1, 1)
    fmt.Printf("%p\n", &s2)

    fmt.Println(s1, s2)

}

输出结果:

    0xc42000a060
    0xc42000a080
    [] [1]

超出原 slice.cap 限制,就会重新分配底层数组,即便原数组并未填满。#

package main

import (
    "fmt"
)

func main() {
  //这里的10:0是指第10位置的索引值为0,那么其他没有赋初始值的元素也为0
    data := [...]int{0, 1, 2, 3, 4, 10: 0}
    s := data[:2:3]

    s = append(s, 100, 200) // 一次 append 两个值,超出 s.cap 限制。

    fmt.Println(s, data)         // 重新分配底层数组,与原数组无关。
    fmt.Println(&s[0], &data[0]) // 比对底层数组起始指针。

}

输出结果:

    [0 1 100 200] [0 1 2 3 4 0 0 0 0 0 0]
    0xc4200160f0 0xc420070060

从输出结果可以看出,append 后的 s 重新分配了底层数组,并复制数据。如果只追加一个值,则不会超过 s.cap 限制,也就不会重新分配。
通常以 2 倍容量重新分配底层数组。在大批量添加数据时,建议一次性分配足够大的空间,以减少内存分配和数据复制开销。或初始化足够长的 len 属性,改用索引号进行操作。及时释放不再使用的 slice 对象,避免持有过期数组,造成 GC 无法回收。

slice 中 cap 重新分配规律#

package main

import (
    "fmt"
)

func main() {

    s := make([]int, 0, 1)
    c := cap(s)

    for i := 0; i < 50; i++ {
        s = append(s, i)
        if n := cap(s); n > c {
            fmt.Printf("cap: %d -> %d\n", c, n)
            c = n
        }
    }

}

输出结果:

    cap: 1 -> 2
    cap: 2 -> 4
    cap: 4 -> 8
    cap: 8 -> 16
    cap: 16 -> 32
    cap: 32 -> 64

切片删除指定元素#

package main

import (
    "fmt"
)

//删除函数
func remove(s []string, i int) []string {
    return append(s[:i], s[i+1:]...)
}

func main() {
    s := []string{"a", "b", "c"}
    fmt.Println(s)
    s = remove(s, 1)
    fmt.Println(s)
}

切片拷贝#

package main

import (
    "fmt"
)

func main() {

    s1 := []int{1, 2, 3, 4, 5}
    fmt.Printf("slice s1 : %v\n", s1)
    s2 := make([]int, 10)
    fmt.Printf("slice s2 : %v\n", s2)
    copy(s2, s1)
    fmt.Printf("copied slice s1 : %v\n", s1)
    fmt.Printf("copied slice s2 : %v\n", s2)
    s3 := []int{1, 2, 3}
    fmt.Printf("slice s3 : %v\n", s3)
    s3 = append(s3, s2...)
    fmt.Printf("appended slice s3 : %v\n", s3)
    s3 = append(s3, 4, 5, 6)
    fmt.Printf("last slice s3 : %v\n", s3)

}

输出结果:

    slice s1 : [1 2 3 4 5]
    slice s2 : [0 0 0 0 0 0 0 0 0 0]
    copied slice s1 : [1 2 3 4 5]
    copied slice s2 : [1 2 3 4 5 0 0 0 0 0]
    slice s3 : [1 2 3]
    appended slice s3 : [1 2 3 1 2 3 4 5 0 0 0 0 0]
    last slice s3 : [1 2 3 1 2 3 4 5 0 0 0 0 0 4 5 6]

copy :函数 copy 在两个 slice 间复制数据,复制长度以 len 小的为准。两个 slice 可指向同一底层数组,允许元素区间重叠。

package main

import (
    "fmt"
)

func main() {

    data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    fmt.Println("array data : ", data)
    s1 := data[8:]
    s2 := data[:5]
    fmt.Printf("slice s1 : %v\n", s1)
    fmt.Printf("slice s2 : %v\n", s2)
    copy(s2, s1)
    fmt.Printf("copied slice s1 : %v\n", s1)
    fmt.Printf("copied slice s2 : %v\n", s2)
    fmt.Println("last array data : ", data)

}

输出结果:

    array data :  [0 1 2 3 4 5 6 7 8 9]
    slice s1 : [8 9]
    slice s2 : [0 1 2 3 4]
    copied slice s1 : [8 9]
    copied slice s2 : [8 9 2 3 4]
    last array data :  [8 9 2 3 4 5 6 7 8 9]

应及时将所需数据 copy 到较小的 slice,以便释放超大号底层数组内存。

slice 遍历#

package main

import (
    "fmt"
)

func main() {

    data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    slice := data[:]
    for index, value := range slice {
        fmt.Printf("inde : %v , value : %v\n", index, value)
    }

}

输出结果:

    inde : 0 , value : 0
    inde : 1 , value : 1
    inde : 2 , value : 2
    inde : 3 , value : 3
    inde : 4 , value : 4
    inde : 5 , value : 5
    inde : 6 , value : 6
    inde : 7 , value : 7
    inde : 8 , value : 8
    inde : 9 , value : 9

切片 resize(调整大小)#

package main

import (
    "fmt"
)

func main() {
    var a = []int{1, 3, 4, 5}
    fmt.Printf("slice a : %v , len(a) : %v\n", a, len(a))
    b := a[1:2]
    fmt.Printf("slice b : %v , len(b) : %v\n", b, len(b))
    c := b[0:3]
    fmt.Printf("slice c : %v , len(c) : %v\n", c, len(c))
}

输出结果:

    slice a : [1 3 4 5] , len(a) : 4
    slice b : [3] , len(b) : 1
    slice c : [3 4 5] , len(c) : 3

数组和切片的内存布局#

null

字符串和切片(string and slice)#

string 底层就是一个 byte 的数组,因此,也可以进行切片操作。

package main

import (
    "fmt"
)

func main() {
    str := "hello world"
    s1 := str[0:5]
    fmt.Println(s1)

    s2 := str[6:]
    fmt.Println(s2)
}

输出结果:

    hello
    world

string 本身是不可变的,因此要改变 string 中字符。需要如下操作:
英文字符串:

package main

import (
    "fmt"
)

func main() {
    str := "Hello world"
    s := []byte(str) //中文字符需要用[]rune(str)
    s[6] = 'G'
    s = s[:8]
    s = append(s, '!')
    str = string(s)
    fmt.Println(str)
}

输出结果:

    Hello Go!

含有中文字符串#

package main

import (
    "fmt"
)

func main() {
    str := "你好,世界!hello world!"
    s := []rune(str) 
    s[3] = ''
    s[4] = ''
    s[12] = 'g'
    s = s[:14]
    str = string(s)
    fmt.Println(str)
}

输出结果:

你好,够浪!hello go

切片切割理解#

golang slice data [:6:8] 两个冒号的理解

常规 slice , data [6:8],从第 6 位到第 8 位(返回 6, 7),长度 len 为 2, 最大可扩充长度 cap 为 4(6-9)

另一种写法: data [:6:8] 每个数字前都有个冒号, slice 内容为 data 从 0 到第 6 位,长度 len 为 6,最大扩充项 cap 设置为 8

a[x:y] 切片内容 [x] 切片长度: y-x 切片容量

package main

import (
    "fmt"
)

func main() {
    slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    d1 := slice[6:8]
    fmt.Println(d1, len(d1), cap(d1))
    d2 := slice[:6:8]
    fmt.Println(d2, len(d2), cap(d2))
}

数组 or 切片转字符串:

    strings.Replace(strings.Trim(fmt.Sprint(array_or_slice), "[]"), " ", ",", -1)

切片的底层实现#

切片的类型#

o 数组是值类型,赋值和函数传参操作都会复制整个数组数据。

func main() {
    arrayA := [2]int{100, 200}
    var arrayB [2]int

    arrayB = arrayA

    fmt.Printf("arrayA : %p , %v\n", &arrayA, arrayA)
    fmt.Printf("arrayB : %p , %v\n", &arrayB, arrayB)

    testArray(arrayA)
}

func testArray(x [2]int) {
    fmt.Printf("func Array : %p , %v\n", &x, x)
}

打印结果:

    arrayA : 0xc4200bebf0 , [100 200]
    arrayB : 0xc4200bec00 , [100 200]
    func Array : 0xc4200bec30 , [100 200]

三个内存地址都不同,Go 中数组赋值和函数传参都是值复制的

可以采用指针传值的方式

func main() {
    arrayA := []int{100, 200}
    testArrayPoint(&arrayA)   // 1.传数组指针
    arrayB := arrayA[:]
    testArrayPoint(&arrayB)   // 2.传切片
    fmt.Printf("arrayA : %p , %v\n", &arrayA, arrayA)
}

func testArrayPoint(x *[]int) {
    fmt.Printf("func Array : %p , %v\n", x, *x)
    (*x)[1] += 100
}

打印结果:

    func Array : 0xc4200b0140 , [100 200]
    func Array : 0xc4200b0180 , [100 300]
    arrayA : 0xc4200b0140 , [100 400]
切片的数据结构#

切片本身并不是动态数组或者数组指针。它内部实现的数据结构通过指针引用底层数组,设定相关属性将数据读写操作限定在指定的区域内。切片本身是一个只读对象,其工作机制类似数组指针的一种封装。

切片(slice)是对数组一个连续片段的引用,所以切片是一个引用类型(因此更类似于 C/C++ 中的数组类型,或者 Python 中的 list 类型)。这个片段可以是整个数组,或者是由起始和终止索引标识的一些项的子集。需要注意的是,终止索引标识的项不包括在切片内。切片提供了一个与指向数组的动态窗口。

给定项的切片索引可能比相关数组的相同元素的索引小。和数组不同的是,切片的长度可以在运行时修改,最小为 0 最大为相关数组的长度:切片是一个长度可变的数组。

Slice 的数据结构定义如下:

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}
切片扩容#

当一个切片的容量满了,就需要扩容了。怎么扩,策略是什么?

func growslice(et *_type, old slice, cap int) slice {
    if raceenabled {
        callerpc := getcallerpc(unsafe.Pointer(&et))
        racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, funcPC(growslice))
    }
    if msanenabled {
        msanread(old.array, uintptr(old.len*int(et.size)))
    }

    if et.size == 0 {
        // 如果新要扩容的容量比原来的容量还要小,这代表要缩容了,那么可以直接报panic了。
        if cap < old.cap {
            panic(errorString("growslice: cap out of range"))
        }

        // 如果当前切片的大小为0,还调用了扩容方法,那么就新生成一个新的容量的切片返回。
        return slice{unsafe.Pointer(&zerobase), old.len, cap}
    }

  // 这里就是扩容的策略
    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        if old.len < 1024 {
            newcap = doublecap
        } else {
            for newcap < cap {
                newcap += newcap / 4
            }
        }
    }

    // 计算新的切片的容量,长度。
    var lenmem, newlenmem, capmem uintptr
    const ptrSize = unsafe.Sizeof((*byte)(nil))
    switch et.size {
    case 1:
        lenmem = uintptr(old.len)
        newlenmem = uintptr(cap)
        capmem = roundupsize(uintptr(newcap))
        newcap = int(capmem)
    case ptrSize:
        lenmem = uintptr(old.len) * ptrSize
        newlenmem = uintptr(cap) * ptrSize
        capmem = roundupsize(uintptr(newcap) * ptrSize)
        newcap = int(capmem / ptrSize)
    default:
        lenmem = uintptr(old.len) * et.size
        newlenmem = uintptr(cap) * et.size
        capmem = roundupsize(uintptr(newcap) * et.size)
        newcap = int(capmem / et.size)
    }

    // 判断非法的值,保证容量是在增加,并且容量不超过最大容量
    if cap < old.cap || uintptr(newcap) > maxSliceCap(et.size) {
        panic(errorString("growslice: cap out of range"))
    }

    var p unsafe.Pointer
    if et.kind&kindNoPointers != 0 {
        // 在老的切片后面继续扩充容量
        p = mallocgc(capmem, nil, false)
        // 将 lenmem 这个多个 bytes 从 old.array地址 拷贝到 p 的地址处
        memmove(p, old.array, lenmem)
        // 先将 P 地址加上新的容量得到新切片容量的地址,然后将新切片容量地址后面的 capmem-newlenmem 个 bytes 这块内存初始化。为之后继续 append() 操作腾出空间。
        memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
    } else {
        // 重新申请新的数组给新切片
        // 重新申请 capmen 这个大的内存地址,并且初始化为0值
        p = mallocgc(capmem, et, true)
        if !writeBarrier.enabled {
            // 如果还不能打开写锁,那么只能把 lenmem 大小的 bytes 字节从 old.array 拷贝到 p 的地址处
            memmove(p, old.array, lenmem)
        } else {
            // 循环拷贝老的切片的值
            for i := uintptr(0); i < lenmem; i += et.size {
                typedmemmove(et, add(p, i), add(old.array, i))
            }
        }
    }
    // 返回最终新切片,容量更新为最新扩容之后的容量
    return slice{p, old.len, newcap}
}

上述就是扩容的实现。主要需要关注的有两点,一个是扩容时候的策略,还有一个就是扩容是生成全新的内存地址还是在原来的地址后追加。

扩容策略#
func main() {
    slice := []int{10, 20, 30, 40}
    newSlice := append(slice, 50)
    fmt.Printf("Before slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, &slice, len(slice), cap(slice))
    fmt.Printf("Before newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, &newSlice, len(newSlice), cap(newSlice))
    newSlice[1] += 10
    fmt.Printf("After slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, &slice, len(slice), cap(slice))
    fmt.Printf("After newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, &newSlice, len(newSlice), cap(newSlice))
}

输出结果:

    Before slice = [10 20 30 40], Pointer = 0xc4200b0140, len = 4, cap = 4
    Before newSlice = [10 20 30 40 50], Pointer = 0xc4200b0180, len = 5, cap = 8
    After slice = [10 20 30 40], Pointer = 0xc4200b0140, len = 4, cap = 4
    After newSlice = [10 30 30 40 50], Pointer = 0xc4200b0180, len = 5, cap = 8

用图表示出上述过程。

null

如果切片的容量小于 1024 个元素,于是扩容的时候就翻倍增加容量。上面那个例子也验证了这一情况,总容量从原来的 4 个翻倍到现在的 8 个。

一旦元素个数超过 1024 个元素,那么增长因子就变成 1.25 ,即每次增加原来容量的四分之一。

注意:扩容扩大的容量都是针对原来的容量而言的,而不是针对原来数组的长度而言的。

注意点#
func main() {
    slice := []int{10, 20, 30, 40}
    for index, value := range slice {
        fmt.Printf("value = %d , value-addr = %x , slice-addr = %x\n", value, &value, &slice[index])
    }
}

输出:

    value = 10 , value-addr = c4200aedf8 , slice-addr = c4200b0320
    value = 20 , value-addr = c4200aedf8 , slice-addr = c4200b0328
    value = 30 , value-addr = c4200aedf8 , slice-addr = c4200b0330
    value = 40 , value-addr = c4200aedf8 , slice-addr = c4200b0338

从上面结果我们可以看到,如果用 range 的方式去遍历一个切片,拿到的 Value 其实是切片里面的值拷贝。所以每次打印 Value 的地址都不变。

null

由于 Value 是值拷贝的,并非引用传递,所以直接改 Value 是达不到更改原切片值的目的的,需要通过 &slice [index] 获取真实的地址。

指针#

Go 语言中的函数传参都是值拷贝,当我们想要修改某个变量的时候,我们可以创建一个指向该变量地址的指针变量。传递数据使用指针,而无须拷贝数据。类型指针不能进行偏移和运算。Go 语言中的指针操作非常简单,只需要记住两个符号:&(取地址)和*(根据地址取值)。

指针地址和指针类型#

每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go 语言中使用 & 字符放在变量前面对变量进行 “取地址” 操作。 Go 语言中的值类型(int、float、bool、string、array、struct)都有对应的指针类型,如:*int、*int64、*string等。

取变量指针的语法如下:

    ptr := &v    // v的类型为T

其中:

    v:代表被取地址的变量,类型为T
    ptr:用于接收地址的变量,ptr的类型就为*T,称做T的指针类型。*代表指针。

举个例子:

func main() {
    a := 10
    b := &a
    fmt.Printf("a:%d ptr:%p\n", a, &a) // a:10 ptr:0xc00001a078
    fmt.Printf("b:%p type:%T\n", b, b) // b:0xc00001a078 type:*int
    fmt.Println(&b)                    // 0xc00000e018
}

我们来看一下b := &a的图示:

null

指针取值#

在对普通变量使用 & 操作符取地址后会获得这个变量的指针,然后可以对指针使用*操作,也就是指针取值,代码如下。

func main() {
    //指针取值
    a := 10
    b := &a // 取变量a的地址,将指针保存到b中
    fmt.Printf("type of b:%T\n", b)
    c := *b // 指针取值(根据指针去内存取值)
    fmt.Printf("type of c:%T\n", c)
    fmt.Printf("value of c:%v\n", c)
}

输出如下:

    type of b:*int
    type of c:int
    value of c:10

总结: 取地址操作符 & 和取值操作符*是一对互补操作符,&取出地址,*根据地址取出地址指向的值。

变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:\

    1.对变量进行取地址(&)操作,可以获得这个变量的指针变量。
    2.指针变量的值是指针地址。
    3.对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。

指针传值示例:

func modify1(x int) {
    x = 100
}

func modify2(x *int) {
    *x = 100
}

func main() {
    a := 10
    modify1(a)
    fmt.Println(a) // 10
    modify2(&a)
    fmt.Println(a) // 100
}

空指针#

  • 当一个指针被定义后没有分配到任何变量时,它的值为 nil
  • 空指针的判断
package main

import "fmt"

func main() {
    var p *string
    fmt.Println(p)
    fmt.Printf("p的值是%v\n", p)
    if p != nil {
        fmt.Println("非空")
    } else {
        fmt.Println("空值")
    }
}

new 和 make#

例子:

func main() {
    var a *int
    *a = 100
    fmt.Println(*a)

    var b map[string]int
    b["测试"] = 100
    fmt.Println(b)
}

执行上面的代码会引发 panic,为什么呢? 在 Go 语言中对于引用类型的变量,我们在使用的时候不仅要声明它,还要为它分配内存空间,否则我们的值就没办法存储。而对于值类型的声明不需要分配内存空间,是因为它们在声明的时候已经默认分配好了内存空间。要分配内存,就引出来今天的 new 和 make。 Go 语言中 new 和 make 是内建的两个函数,主要用来分配内存

new#

new 是一个内置的函数,它的函数签名如下:

    func new(Type) *Type

其中,

    1.Type表示类型,new函数只接受一个参数,这个参数是一个类型
    2.*Type表示类型指针,new函数返回一个指向该类型内存地址的指针。

new 函数不太常用,使用 new 函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值。举个例子:

func main() {
    a := new(int)
    b := new(bool)
    fmt.Printf("%T\n", a) // *int
    fmt.Printf("%T\n", b) // *bool
    fmt.Println(*a)       // 0
    fmt.Println(*b)       // false
}    

本节开始的示例代码中var a *int只是声明了一个指针变量 a 但是没有初始化,指针作为引用类型需要初始化后才会拥有内存空间,才可以给它赋值。应该按照如下方式使用内置的 new 函数对 a 进行初始化之后就可以正常对其赋值了:

func main() {
    var a *int
    a = new(int)
    *a = 10
    fmt.Println(*a)
}

make#

make 也是用于内存分配的,区别于 new,它只用于 slice、map 以及 chan 的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。make 函数的函数签名如下:

func make(t Type, size ...IntegerType) Type

make 函数是无可替代的,我们在使用 slice、map 以及 channel 的时候,都需要使用 make 进行初始化,然后才可以对它们进行操作。这个我们在上一章中都有说明,关于 channel 我们会在后续的章节详细说明。

本节开始的示例中var b map[string]int只是声明变量 b 是一个 map 类型的变量,需要像下面的示例代码一样使用 make 函数进行初始化操作之后,才能对其进行键值对赋值:

func main() {
    var b map[string]int
    b = make(map[string]int, 10)
    b["测试"] = 100
    fmt.Println(b)
}

new 与 make 的区别#

    1.二者都是用来做内存分配的。
    2.make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身;
    3.而new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针。

map#

map 定义#

Go 语言中 map 的定义语法如下

    map[KeyType]ValueType

其中,

    KeyType:表示键的类型。

    ValueType:表示键对应的值的类型。

map 类型的变量默认初始值为 nil,需要使用 make () 函数来分配内存。语法为:

    make(map[KeyType]ValueType, [cap])

其中 cap 表示 map 的容量,该参数虽然不是必须的,但是我们应该在初始化 map 的时候就为其指定一个合适的容量。

map 基本使用#

map 中的数据都是成对出现的,map 的基本使用示例代码如下:

func main() {
    scoreMap := make(map[string]int, 8)
    scoreMap["张三"] = 90
    scoreMap["小明"] = 100
    fmt.Println(scoreMap)
    fmt.Println(scoreMap["小明"])
    fmt.Printf("type of a:%T\n", scoreMap)
}   

输出:

    map[小明:100 张三:90]
    100
    type of a:map[string]int  

map 也支持在声明的时候填充元素,例如:

func main() {
    userInfo := map[string]string{
        "username": "pprof.cn",
        "password": "123456",
    }
    fmt.Println(userInfo) //
}

判断某个键是否存在#

Go 语言中有个判断 map 中键是否存在的特殊写法,格式如下:

    value, ok := map[key]   

举个例子:

func main() {
    scoreMap := make(map[string]int)
    scoreMap["张三"] = 90
    scoreMap["小明"] = 100
    // 如果key存在ok为true,v为对应的值;不存在ok为false,v为值类型的零值
    v, ok := scoreMap["张三"]
    if ok {
        fmt.Println(v)
    } else {
        fmt.Println("查无此人")
    }
}  

map 的遍历#

Go 语言中使用 for range 遍历 map。

func main() {
    scoreMap := make(map[string]int)
    scoreMap["张三"] = 90
    scoreMap["小明"] = 100
    scoreMap["王五"] = 60
    for k, v := range scoreMap {
        fmt.Println(k, v)
    }
}  

但我们只想遍历 key 的时候,可以按下面的写法:

func main() {
    scoreMap := make(map[string]int)
    scoreMap["张三"] = 90
    scoreMap["小明"] = 100
    scoreMap["王五"] = 60
    for k := range scoreMap {
        fmt.Println(k)
    }
}  

注意: 遍历 map 时的元素顺序与添加键值对的顺序无关。

使用 delete () 函数删除键值对#

使用 delete () 内建函数从 map 中删除一组键值对,delete () 函数的格式如下:

    delete(map, key)  

其中,

    map:表示要删除键值对的map

    key:表示要删除的键值对的键   

示例代码如下:

func main(){
    scoreMap := make(map[string]int)
    scoreMap["张三"] = 90
    scoreMap["小明"] = 100
    scoreMap["王五"] = 60
    delete(scoreMap, "小明")//将小明:100从map中删除
    for k,v := range scoreMap{
        fmt.Println(k, v)
    }
}  

元素为 map 类型的切片#

下面的代码演示了切片中的元素为 map 类型时的操作:

func main() {
    var mapSlice = make([]map[string]string, 3)
    for index, value := range mapSlice {
        fmt.Printf("index:%d value:%v\n", index, value)
    }
    fmt.Println("after init")
    // 对切片中的map元素进行初始化
    mapSlice[0] = make(map[string]string, 10)
    mapSlice[0]["name"] = "王五"
    mapSlice[0]["password"] = "123456"
    mapSlice[0]["address"] = "红旗大街"
    for index, value := range mapSlice {
        fmt.Printf("index:%d value:%v\n", index, value)
    }
} 

值为切片类型的 map#

下面的代码演示了 map 中值为切片类型的操作:

func main() {
    var sliceMap = make(map[string][]string, 3)
    fmt.Println(sliceMap)
    fmt.Println("after init")
    key := "中国"
    value, ok := sliceMap[key]
    if !ok {
        value = make([]string, 0, 2)
    }
    value = append(value, "北京", "上海")
    sliceMap[key] = value
    fmt.Println(sliceMap)
}

Map 实现原理#

key,value 存储#

最通俗的话说 Map 是一种通过 key 来获取 value 的一个数据结构,其底层存储方式为数组,在存储时 key 不能重复,当 key 重复时,value 进行覆盖,我们通过 key 进行 hash 运算(可以简单理解为把 key 转化为一个整形数字)然后对数组的长度取余,得到 key 存储在数组的哪个下标位置,最后将 key 和 value 组装为一个结构体,放入数组下标处,看下图:

    length = len(array) = 4
    hashkey1 = hash(xiaoming) = 4
    index1  = hashkey1% length= 0
    hashkey2 = hash(xiaoli) = 6
    index2  = hashkey2% length= 2

null

hash 冲突#

如上图所示,数组一个下标处只能存储一个元素,也就是说一个数组下标只能存储一对 key,value, hashkey (xiaoming)=4 占用了下标 0 的位置,假设我们遇到另一个 key,hashkey (xiaowang) 也是 4,这就是 hash 冲突(不同的 key 经过 hash 之后得到的值一样),那么 key=xiaowang 的怎么存储?
hash 冲突的常见解决方法

开放定址法:也就是说当我们存储一个 key,value 时,发现 hashkey (key) 的下标已经被别 key 占用,那我们在这个数组中空间中重新找一个没被占用的存储这个冲突的 key,那么没被占用的有很多,找哪个好呢?常见的有线性探测法,线性补偿探测法,随机探测法,这里我们主要说一下线性探测法

线性探测,字面意思就是按照顺序来,从冲突的下标处开始往后探测,到达数组末尾时,从数组开始处探测,直到找到一个空位置存储这个 key,当数组都找不到的情况下回扩容(事实上当数组容量快满的时候就会扩容了);查找某一个 key 的时候,找到 key 对应的下标,比较 key 是否相等,如果相等直接取出来,否则按照顺寻探测直到碰到一个空位置,说明 key 不存在。如下图:首先存储 key=xiaoming 在下标 0 处,当存储 key=xiaowang 时,hash 冲突了,按照线性探测,存储在下标 1 处,(红色的线是冲突或者下标已经被占用了) 再者 key=xiaozhao 存储在下标 4 处,当存储 key=xiaoliu 是,hash 冲突了,按照线性探测,从头开始,存储在下标 2 处 (黄色的是冲突或者下标已经被占用了)

null

拉链法:何为拉链,简单理解为链表,当 key 的 hash 冲突时,我们在冲突位置的元素上形成一个链表,通过指针互连接,当查找时,发现 key 冲突,顺着链表一直往下找,直到链表的尾节点,找不到则返回空,如下图:

null

开放定址(线性探测)和拉链的优缺点

  • 由上面可以看出拉链法比线性探测处理简单
  • 线性探测查找是会被拉链法会更消耗时间
  • 线性探测会更加容易导致扩容,而拉链不会
  • 拉链存储了指针,所以空间上会比线性探测占用多一点
  • 拉链是动态申请存储空间的,所以更适合链长不确定的
Go 中 Map 的使用#

直接用代码描述,直观,简单,易理解

//直接创建初始化一个mao
var mapInit = map[string]string {"xiaoli":"湖南", "xiaoliu":"天津"}
//声明一个map类型变量,
//map的key的类型是string,value的类型是string
var mapTemp map[string]string
//使用make函数初始化这个变量,并指定大小(也可以不指定)
mapTemp = make(map[string]string,10)
//存储key ,value
mapTemp["xiaoming"] = "北京"
mapTemp["xiaowang"]= "河北"
//根据key获取value,
//如果key存在,则ok是true,否则是flase
//v1用来接收key对应的value,当ok是false时,v1是nil
v1,ok := mapTemp["xiaoming"]
fmt.Println(ok,v1)
//当key=xiaowang存在时打印value
if v2,ok := mapTemp["xiaowang"]; ok{
    fmt.Println(v2)
}
//遍历map,打印key和value
for k,v := range mapTemp{
    fmt.Println(k,v)
}
//删除map中的key
delete(mapTemp,"xiaoming")
//获取map的大小
l := len(mapTemp)
fmt.Println(l)

看了上面的 map 创建,初始化,增删改查等操作,我们发现 go 的 api 其实挺简单易学的

Go 中 Map 的实现原理#

知其然,更得知其所以然,会使用 map 了,多问问为什么,go 底层 map 到底怎么存储呢?接下来我们一探究竟。map 的源码位于 src/runtime/map.go 中 map 同样也是数组存储的的,每个数组下标处存储的是一个 bucket, 这个 bucket 的类型见下面代码,每个 bucket 中可以存储 8 个 kv 键值对,当每个 bucket 存储的 kv 对到达 8 个之后,会通过 overflow 指针指向一个新的 bucket,从而形成一个链表,看 bmap 的结构,我想大家应该很纳闷,没看见 kv 的结构和 overflow 指针啊,事实上,这两个结构体并没有显示定义,是通过指针运算进行访问的。

//bucket结构体定义 b就是bucket
type bmap{
    // tophash generally contains the top byte of the hash value
    // for each key  in this bucket. If tophash[0] < minTopHash,
    // tophash[0] is a bucket               evacuation state instead.
    //翻译:top hash通常包含该bucket中每个键的hash值的高八位。
    如果tophash[0]小于mintophash,则tophash[0]为桶疏散状态    //bucketCnt 的初始值是8
    tophash [bucketCnt]uint8
    // Followed by bucketCnt keys and then bucketCnt values.
    // NOTE: packing all the keys together and then all the values together makes the    // code a bit more complicated than alternating key/value/key/value/... but it allows    // us to eliminate padding which would be needed for, e.g., map[int64]int8.// Followed by an overflow pointer.    //翻译:接下来是bucketcnt键,然后是bucketcnt值。
    注意:将所有键打包在一起,然后将所有值打包在一起,    使得代码比交替键/值/键/值/更复杂。但它允许//我们消除可能需要的填充,    例如map[int64]int8./后面跟一个溢出指针}

看上面代码以及注释,我们能得到 bucket 中存储的 kv 是这样的,tophash 用来快速查找 key 值是否在该 bucket 中,而不同每次都通过真值进行比较;还有 kv 的存放,为什么不是 k1v1,k2v2….. 而是 k1k2…v1v2…,我们看上面的注释说的 map [int64] int8,key 是 int64(8 个字节),value 是 int8(一个字节),kv 的长度不同,如果按照 kv 格式存放,则考虑内存对齐 v 也会占用 int64,而按照后者存储时,8 个 v 刚好占用一个 int64, 从这个就可以看出 go 的 map 设计之巧妙。

null

最后我们分析一下 go 的整体内存结构,阅读一下 map 存储的源码,如下图所示,当往 map 中存储一个 kv 对时,通过 k 获取 hash 值,hash 值的低八位和 bucket 数组长度取余,定位到在数组中的那个下标,hash 值的高八位存储在 bucket 中的 tophash 中,用来快速判断 key 是否存在,key 和 value 的具体值则通过指针运算存储,当一个 bucket 满时,通过 overfolw 指针链接到下一个 bucket。

null

go 的 map 存储源码如下,省略了一些无关紧要的代码

func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
    //获取hash算法
    alg := t.key.alg
    //计算hash值
    hash := alg.hash(key, uintptr(h.hash0))
    //如果bucket数组一开始为空,则初始化
    if h.buckets == nil {
        h.buckets = newobject(t.bucket) // newarray(t.bucket, 1)
    }
again:
    // 定位存储在哪一个bucket中
    bucket := hash & bucketMask(h.B)
    //得到bucket的结构体
    b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) +bucket*uintptr(t.bucketsize)))
    //获取高八位hash值
    top := tophash(hash)
    var inserti *uint8
    var insertk unsafe.Pointer
    var val unsafe.Pointer
bucketloop:
    //死循环
    for {
        //循环bucket中的tophash数组
        for i := uintptr(0); i < bucketCnt; i++ {
            //如果hash不相等
            if b.tophash[i] != top {
             //判断是否为空,为空则插入
                if isEmpty(b.tophash[i]) && inserti == nil {
                    inserti = &b.tophash[i]
                    insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
                    val = add( unsafe.Pointer(b), 
                    dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize) )
                }
              //插入成功,终止最外层循环
                if b.tophash[i] == emptyRest {
                    break bucketloop
                }
                continue
            }
            //到这里说明高八位hash一样,获取已存在的key
            k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
            if t.indirectkey() {
                k = *((*unsafe.Pointer)(k))
            }
            //判断两个key是否相等,不相等就循环下一个
            if !alg.equal(key, k) {
                continue
            }
            // 如果相等则更新
            if t.needkeyupdate() {
                typedmemmove(t.key, k, key)
            }
            //获取已存在的value
            val = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize))
            goto done
        }
        //如果上一个bucket没能插入,则通过overflow获取链表上的下一个bucket
        ovf := b.overflow(t)
        if ovf == nil {
            break
        }
        b = ovf
    }

    if inserti == nil {
        // all current buckets are full, allocate a new one.
        newb := h.newoverflow(t, b)
        inserti = &newb.tophash[0]
        insertk = add(unsafe.Pointer(newb), dataOffset)
        val = add(insertk, bucketCnt*uintptr(t.keysize))
    }

    // store new key/value at insert position
    if t.indirectkey() {
        kmem := newobject(t.key)
        *(*unsafe.Pointer)(insertk) = kmem
        insertk = kmem
    }
    if t.indirectvalue() {
        vmem := newobject(t.elem)
        *(*unsafe.Pointer)(val) = vmem
    }
    typedmemmove(t.key, insertk, key)
    //将高八位hash值存储
    *inserti = top
    h.count++
    return val
}

Struct 结构体#

自定义类型#

在 Go 语言中有一些基本的数据类型,如 string、整型、浮点型、布尔等数据类型,Go 语言中可以使用 type 关键字来定义自定义类型。

自定义类型是定义了一个全新的类型。我们可以基于内置的基本类型定义,也可以通过 struct 定义。例如:

//将MyInt定义为int类型
type MyInt int 

通过 Type 关键字的定义,MyInt 就是一种新的类型,它具有 int 的特性。

类型别名#

类型别名规定:TypeAlias 只是 Type 的别名,本质上 TypeAlias 与 Type 是同一个类型。就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。

type TypeAlias = Type 

我们之前见过的 rune 和 byte 就是类型别名,他们的定义如下:

type byte = uint8
type rune = int32

类型定义和类型别名的区别#

类型别名与类型定义表面上看只有一个等号的差异,我们通过下面的这段代码来理解它们之间的区别。

//类型定义
type NewInt int

//类型别名
type MyInt = int

func main() {
    var a NewInt
    var b MyInt

    fmt.Printf("type of a:%T\n", a) //type of a:main.NewInt
    fmt.Printf("type of b:%T\n", b) //type of b:int
} 

结果显示 a 的类型是 main.NewInt,表示 main 包下定义的 NewInt 类型。b 的类型是 int。MyInt 类型只会在代码中存在,编译完成时并不会有 MyInt 类型。

结构体的定义#

使用 type 和 struct 关键字来定义结构体,具体代码格式如下:

type 类型名 struct {
  字段名 字段类型
  字段名 字段类型

} 

其中:

1.类型名:标识自定义结构体的名称,在同一个包内不能重复。
2.字段名:表示结构体字段名。结构体中的字段名必须唯一。
3.字段类型:表示结构体字段的具体类型。 
type person struct {
  name string
  city string
  age  int8
} 

type person1 struct {
  name, city string
  age        int8
} 

结构体实例化#

type person struct {
   name string
   city string
   age  int8
}

func main() {
   var p = person{
      "名字",
      "城市",
      18,
   }
   fmt.Println(p)
}

匿名结构体#

在定义一些临时数据结构等场景下还可以使用匿名结构体。

package main

import (
    "fmt"
)

func main() {
    var user struct{Name string; Age int}
    user.Name = "pprof.cn"
    user.Age = 18
    fmt.Printf("%#v\n", user)
} 

创建指针类型结构体#

还可以通过使用 new 关键字对结构体进行实例化,得到的是结构体的地址。 格式如下:

    var p2 = new(person)
    fmt.Printf("%T\n", p2)     //*main.person
    fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"", city:"", age:0}  

从打印的结果中我们可以看出 p2 是一个结构体指针。

需要注意的是在 Go 语言中支持对结构体指针直接使用。来访问结构体的成员。

var p2 = new(person)
p2.name = "测试"
p2.age = 18
p2.city = "北京"
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"测试", city:"北京", age:18} 

取结构体的地址实例化#

使用 & 对结构体进行取地址操作相当于对该结构体类型进行了一次 new 实例化操作。

p3 := &person{}
fmt.Printf("%T\n", p3)     //*main.person
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"", city:"", age:0}
p3.name = "博客"
p3.age = 30
p3.city = "成都"
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"博客", city:"成都", age:30}

p3.name = “博客” 其实在底层是 (*p3).name = “博客”,这是 Go 语言帮我们实现的语法糖。

结构体初始化#

type person struct {
    name string
    city string
    age  int8
}

func main() {
    var p4 person
    fmt.Printf("p4=%#v\n", p4) //p4=main.person{name:"", city:"", age:0}
} 

使用键值对初始化#

使用键值对对结构体进行初始化时,键对应结构体的字段,值对应该字段的初始值。

p5 := person{
    name: "pprof.cn",
    city: "北京",
    age:  18,
}
fmt.Printf("p5=%#v\n", p5) //p5=main.person{name:"pprof.cn", city:"北京", age:18}

也可以对结构体指针进行键值对初始化,例如:

p6 := &person{
    name: "pprof.cn",
    city: "北京",
    age:  18,
}
fmt.Printf("p6=%#v\n", p6) //p6=&main.person{name:"pprof.cn", city:"北京", age:18} 

当某些字段没有初始值的时候,该字段可以不写。此时,没有指定初始值的字段的值就是该字段类型的零值。

p7 := &person{
    city: "北京",
}
fmt.Printf("p7=%#v\n", p7) //p7=&main.person{name:"", city:"北京", age:0}

使用值的列表初始化#

初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值:

p8 := &person{
    "pprof.cn",
    "北京",
    18,
}
fmt.Printf("p8=%#v\n", p8) //p8=&main.person{name:"pprof.cn", city:"北京", age:18}

使用这种格式初始化时,需要注意:

    1.必须初始化结构体的所有字段。
    2.初始值的填充顺序必须与字段在结构体中的声明顺序一致。
    3.该方式不能和键值初始化方式混用。  

结构体内存布局#

type test struct {
    a int8
    b int8
    c int8
    d int8
}
n := test{
    1, 2, 3, 4,
}
fmt.Printf("n.a %p\n", &n.a)
fmt.Printf("n.b %p\n", &n.b)
fmt.Printf("n.c %p\n", &n.c)
fmt.Printf("n.d %p\n", &n.d) 

输出:

    n.a 0xc0000a0060
    n.b 0xc0000a0061
    n.c 0xc0000a0062
    n.d 0xc0000a0063

方法和接收者#

Go 语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于其他语言中的 this 或者 self。

方法的定义格式如下:

    func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
        函数体
    } 

其中,

    1.接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名的第一个小写字母,而不是self、this之类的命名。例如,Person类型的接收者变量应该命名为 p,Connector类型的接收者变量应该命名为c等。
    2.接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
    3.方法名、参数列表、返回参数:具体格式与函数定义相同。

举个例子:

//Person 结构体
type Person struct {
    name string
    age  int8
}

//NewPerson 构造函数
func NewPerson(name string, age int8) *Person {
    return &Person{
        name: name,
        age:  age,
    }
}

//Dream Person做梦的方法
func (p Person) Dream() {
    fmt.Printf("%s的梦想是学好Go语言!\n", p.name)
}

func main() {
    p1 := NewPerson("测试", 25)
    p1.Dream()
}

方法与函数的区别是,函数不属于任何类型,方法属于特定的类型。

指针类型的接收者#

指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。这种方式就十分接近于其他语言中面向对象中的 this 或者 self。 例如我们为 Person 添加一个 SetAge 方法,来修改实例变量的年龄。

    // SetAge 设置p的年龄
    // 使用指针接收者
    func (p *Person) SetAge(newAge int8) {
        p.age = newAge
    }

调用该方法:

func main() {
    p1 := NewPerson("测试", 25)
    fmt.Println(p1.age) // 25
    p1.SetAge(30)
    fmt.Println(p1.age) // 30
} 

值类型的接收者#

当方法作用于值类型接收者时,Go 语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。

// SetAge2 设置p的年龄
// 使用值接收者
func (p Person) SetAge2(newAge int8) {
    p.age = newAge
}

func main() {
    p1 := NewPerson("测试", 25)
    p1.Dream()
    fmt.Println(p1.age) // 25
    p1.SetAge2(30) // (*p1).SetAge2(30)
    fmt.Println(p1.age) // 25
} 

什么时候应该使用指针类型接收者

    1.需要修改接收者中的值
    2.接收者是拷贝代价比较大的大对象
    3.保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。

任意类型添加方法#

在 Go 语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。 举个例子,我们基于内置的 int 类型使用 type 关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。

//MyInt 将int定义为自定义MyInt类型
type MyInt int

//SayHello 为MyInt添加一个SayHello的方法
func (m MyInt) SayHello() {
    fmt.Println("Hello, 我是一个int。")
}
func main() {
    var m1 MyInt
    m1.SayHello() //Hello, 我是一个int。
    m1 = 100
    fmt.Printf("%#v  %T\n", m1, m1) //100  main.MyInt
} 

注意事项: 非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。

结构体的匿名字段#

结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。

//Person 结构体Person类型
type Person struct {
    string
    int
}

func main() {
    p1 := Person{
        "pprof.cn",
        18,
    }
    fmt.Printf("%#v\n", p1)        //main.Person{string:"pprof.cn", int:18}
    fmt.Println(p1.string, p1.int) //pprof.cn 18
}  

匿名字段默认采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。

嵌套结构体#

一个结构体中可以嵌套包含另一个结构体或结构体指针。

//Address 地址结构体
type Address struct {
    Province string
    City     string
}

//User 用户结构体
type User struct {
    Name    string
    Gender  string
    Address Address
}

func main() {
    user1 := User{
        Name:   "pprof",
        Gender: "女",
        Address: Address{
            Province: "黑龙江",
            City:     "哈尔滨",
        },
    }
    fmt.Printf("user1=%#v\n", user1)//user1=main.User{Name:"pprof", Gender:"女", Address:main.Address{Province:"黑龙江", City:"哈尔滨"}}
}

嵌套匿名结构体#

//Address 地址结构体
type Address struct {
    Province string
    City     string
}

//User 用户结构体
type User struct {
    Name    string
    Gender  string
    Address //匿名结构体
}

func main() {
    var user2 User
    user2.Name = "pprof"
    user2.Gender = "女"
    user2.Address.Province = "黑龙江"    //通过匿名结构体.字段名访问
    user2.City = "哈尔滨"                //直接访问匿名结构体的字段名
    fmt.Printf("user2=%#v\n", user2) //user2=main.User{Name:"pprof", Gender:"女", Address:main.Address{Province:"黑龙江", City:"哈尔滨"}}
} 

当访问结构体成员时会先在结构体中查找该字段,找不到再去匿名结构体中查找。

嵌套结构体的字段名冲突#

嵌套结构体内部可能存在相同的字段名。这个时候为了避免歧义需要指定具体的内嵌结构体的字段。

//Address 地址结构体
type Address struct {
    Province   string
    City       string
    CreateTime string
}

//Email 邮箱结构体
type Email struct {
    Account    string
    CreateTime string
}

//User 用户结构体
type User struct {
    Name   string
    Gender string
    Address
    Email
}

func main() {
    var user3 User
    user3.Name = "pprof"
    user3.Gender = "女"
    // user3.CreateTime = "2019" //ambiguous selector user3.CreateTime
    user3.Address.CreateTime = "2000" //指定Address结构体中的CreateTime
    user3.Email.CreateTime = "2000"   //指定Email结构体中的CreateTime
}  

结构体的 “继承”#

Go 语言中使用结构体也可以实现其他编程语言中面向对象的继承。

//Animal 动物
type Animal struct {
    name string
}

func (a *Animal) move() {
    fmt.Printf("%s会动!\n", a.name)
}

//Dog 狗
type Dog struct {
    Feet    int8
    *Animal //通过嵌套匿名结构体实现继承
}

func (d *Dog) wang() {
    fmt.Printf("%s会汪汪汪~\n", d.name)
}

func main() {
    d1 := &Dog{
        Feet: 4,
        Animal: &Animal{ //注意嵌套的是结构体指针
            name: "乐乐",
        },
    }
    d1.wang() //乐乐会汪汪汪~
    d1.move() //乐乐会动!
}

结构体字段的可见性#

结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。

结构体与 JSON 序列化#

JSON (JavaScript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。JSON 键值对是用来保存 JS 对象的一种方式,键 / 值对组合中的键名写在前面并用双引号”” 包裹,使用冒号:分隔,然后紧接着值;多个键值之间使用英文,分隔。

//Student 学生
type Student struct {
    ID     int
    Gender string
    Name   string
}

//Class 班级
type Class struct {
    Title    string
    Students []*Student
}

func main() {
    c := &Class{
        Title:    "101",
        Students: make([]*Student, 0, 200),
    }
    for i := 0; i < 10; i++ {
        stu := &Student{
            Name:   fmt.Sprintf("stu%02d", i),
            Gender: "男",
            ID:     i,
        }
        c.Students = append(c.Students, stu)
    }
    //JSON序列化:结构体-->JSON格式的字符串
    data, err := json.Marshal(c)
    if err != nil {
        fmt.Println("json marshal failed")
        return
    }
    fmt.Printf("json:%s\n", data)
    //JSON反序列化:JSON格式的字符串-->结构体
    str := `{"Title":"101","Students":[{"ID":0,"Gender":"男","Name":"stu00"},{"ID":1,"Gender":"男","Name":"stu01"},{"ID":2,"Gender":"男","Name":"stu02"},{"ID":3,"Gender":"男","Name":"stu03"},{"ID":4,"Gender":"男","Name":"stu04"},{"ID":5,"Gender":"男","Name":"stu05"},{"ID":6,"Gender":"男","Name":"stu06"},{"ID":7,"Gender":"男","Name":"stu07"},{"ID":8,"Gender":"男","Name":"stu08"},{"ID":9,"Gender":"男","Name":"stu09"}]}`
    c1 := &Class{}
    err = json.Unmarshal([]byte(str), c1)
    if err != nil {
        fmt.Println("json unmarshal failed!")
        return
    }
    fmt.Printf("%#v\n", c1)
} 

结构体标签(Tag)#

Tag 是结构体的元信息,可以在运行的时候通过反射的机制读取出来。

Tag 在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:

    `key1:"value1" key2:"value2"`  

结构体标签由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。键值对之间使用一个空格分隔。 注意事项: 为结构体编写 Tag 时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在 key 和 value 之间添加空格。

例如我们为 Student 结构体的每个字段定义 json 序列化时使用的 Tag:

//Student 学生
type Student struct {
    ID     int    `json:"id"` //通过指定tag实现json序列化该字段时的key
    Gender string //json序列化是默认使用字段名作为key
    name   string //私有不能被json包访问
}

func main() {
    s1 := Student{
        ID:     1,
        Gender: "女",
        name:   "pprof",
    }
    data, err := json.Marshal(s1)
    if err != nil {
        fmt.Println("json marshal failed!")
        return
    }
    fmt.Printf("json str:%s\n", data) //json str:{"id":1,"Gender":"女"}
} 

接口#

接口类型#

在 Go 语言中接口(interface)是一种类型,一种抽象的类型。

interface 是一组 method 的集合,是 duck-type programming 的一种体现。接口做的事情就像是定义一个协议(规则),只要一台机器有洗衣服和甩干的功能,我就称它为洗衣机。不关心属性(数据),只关心行为(方法)。

为了保护你的 Go 语言职业生涯,请牢记接口(interface)是一种类型。

为什么要使用接口#

type Cat struct{}

func (c Cat) Say() string { return "喵喵喵" }

type Dog struct{}

func (d Dog) Say() string { return "汪汪汪" }

func main() {
    c := Cat{}
    fmt.Println("猫:", c.Say())
    d := Dog{}
    fmt.Println("狗:", d.Say())
}

上面的代码中定义了猫和狗,然后它们都会叫,你会发现 main 函数中明显有重复的代码,如果我们后续再加上猪、青蛙等动物的话,我们的代码还会一直重复下去。那我们能不能把它们当成 “能叫的动物” 来处理呢?

像类似的例子在我们编程过程中会经常遇到:

比如一个网上商城可能使用支付宝、微信、银联等方式去在线支付,我们能不能把它们当成 “支付方式” 来处理呢?

比如三角形,四边形,圆形都能计算周长和面积,我们能不能把它们当成 “图形” 来处理呢?

比如销售、行政、程序员都能计算月薪,我们能不能把他们当成 “员工” 来处理呢?

Go 语言中为了解决类似上面的问题,就设计了接口这个概念。接口区别于我们之前所有的具体类型,接口是一种抽象的类型。当你看到一个接口类型的值时,你不知道它是什么,唯一知道的是通过它的方法能做什么。

接口的定义#

Go 语言提倡面向接口编程。

    接口是一个或多个方法签名的集合。
    任何类型的方法集中只要拥有该接口'对应的全部方法'签名。
    就表示它 "实现" 了该接口,无须在该类型上显式声明实现了哪个接口。
    这称为Structural Typing。
    所谓对应方法,是指有相同名称、参数列表 (不包括参数名) 以及返回值。
    当然,该类型还可以有其他方法。

    接口只有方法声明,没有实现,没有数据字段。
    接口可以匿名嵌入其他接口,或嵌入到结构中。
    对象赋值给接口时,会发生拷贝,而接口内部存储的是指向这个复制品的指针,既无法修改复制品的状态,也无法获取指针。
    只有当接口存储的类型和对象都为nil时,接口才等于nil。
    接口调用不会做receiver的自动转换。
    接口同样支持匿名字段方法。
    接口也可实现类似OOP中的多态。
    空接口可以作为任何类型数据的容器。
    一个类型可实现多个接口。
    接口命名习惯以 er 结尾。 

每个接口由数个方法组成,接口的定义格式如下:

    type 接口类型名 interface{
        方法名1( 参数列表1 ) 返回值列表1
        方法名2( 参数列表2 ) 返回值列表2

    } 

其中:

    1.接口名:使用type将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有字符串功能的接口叫Stringer等。接口名最好要能突出该接口的类型含义。
    2.方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
    3.参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。  

举个例子:

type writer interface{
    Write([]byte) error
} 

当你看到这个接口类型的值时,你不知道它是什么,唯一知道的就是可以通过它的 Write 方法来做一些事情。

实现接口的条件#

一个对象只要全部实现了接口中的方法,那么就实现了这个接口。换句话说,接口就是一个需要实现的方法列表。

我们来定义一个 Sayer 接口:

// Sayer 接口
type Sayer interface {
    say()
} 

定义 dog 和 cat 两个结构体:

type dog struct {}

type cat struct {} 

因为 Sayer 接口里只有一个 say 方法,所以我们只需要给 dog 和 cat 分别实现 say 方法就可以实现 Sayer 接口了。

// dog实现了Sayer接口
func (d dog) say() {
    fmt.Println("汪汪汪")
}

// cat实现了Sayer接口
func (c cat) say() {
    fmt.Println("喵喵喵")
} 

接口的实现就是这么简单,只要实现了接口中的所有方法,就实现了这个接口。

接口类型变量#

那实现了接口有什么用呢?

接口类型变量能够存储所有实现了该接口的实例。 例如上面的示例中,Sayer 类型的变量能够存储 dog 和 cat 类型的变量。

func main() {
    var x Sayer // 声明一个Sayer类型的变量x
    a := cat{}  // 实例化一个cat
    b := dog{}  // 实例化一个dog
    x = a       // 可以把cat实例直接赋值给x
    x.say()     // 喵喵喵
    x = b       // 可以把dog实例直接赋值给x
    x.say()     // 汪汪汪
} 

值接收者和指针接收者实现接口的区别#

使用值接收者实现接口和使用指针接收者实现接口有什么区别呢?接下来我们通过一个例子看一下其中的区别。

我们有一个 Mover 接口和一个 dog 结构体。

type Mover interface {
    move()
}

type dog struct {} 

值接收者实现接口#

func (d dog) move() {
    fmt.Println("狗会动")
}  

此时实现接口的是 dog 类型:

func main() {
    var x Mover
    var wangcai = dog{} // 旺财是dog类型
    x = wangcai         // x可以接收dog类型
    var fugui = &dog{}  // 富贵是*dog类型
    x = fugui           // x可以接收*dog类型
    x.move()
} 

从上面的代码中我们可以发现,使用值接收者实现接口之后,不管是 dog 结构体还是结构体指针*dog类型的变量都可以赋值给该接口变量。因为 Go 语言中有对指针类型变量求值的语法糖,dog 指针 fugui 内部会自动求值*fugui

指针接收者实现接口#

同样的代码我们再来测试一下使用指针接收者有什么区别:

func (d *dog) move() {
    fmt.Println("狗会动")
}
func main() {
    var x Mover
    var wangcai = dog{} // 旺财是dog类型
    x = wangcai         // x不可以接收dog类型
    var fugui = &dog{}  // 富贵是*dog类型
    x = fugui           // x可以接收*dog类型
} 

此时实现 Mover 接口的是*dog类型,所以不能给 x 传入 dog 类型的 wangcai,此时 x 只能存储*dog类型的值。

下面的代码是一个比较好的面试题#

请问下面的代码是否能通过编译?

不能,必须 var peo People = &Student {}

type People interface {
    Speak(string) string
}

type Student struct{}

func (stu *Student) Speak(think string) (talk string) {
    if think == "sb" {
        talk = "你是个大帅比"
    } else {
        talk = "您好"
    }
    return
}

func main() {
    var peo People = Student{}
    think := "bitch"
    fmt.Println(peo.Speak(think))
}

类型与接口的关系#

一个类型实现多个接口#

一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。 例如,狗可以叫,也可以动。我们就分别定义 Sayer 接口和 Mover 接口,如下: Mover 接口。

// Sayer 接口
type Sayer interface {
    say()
}

// Mover 接口
type Mover interface {
    move()
} 

dog 既可以实现 Sayer 接口,也可以实现 Mover 接口。

type dog struct {
    name string
}

// 实现Sayer接口
func (d dog) say() {
    fmt.Printf("%s会叫汪汪汪\n", d.name)
}

// 实现Mover接口
func (d dog) move() {
    fmt.Printf("%s会动\n", d.name)
}

func main() {
    var x Sayer
    var y Mover

    var a = dog{name: "旺财"}
    x = a
    y = a
    x.say()
    y.move()
} 
多个类型实现同一接口#

Go 语言中不同的类型还可以实现同一接口 首先我们定义一个 Mover 接口,它要求必须由一个 move 方法。

// Mover 接口
type Mover interface {
    move()
} 

例如狗可以动,汽车也可以动,可以使用如下代码实现这个关系:

type dog struct {
    name string
}

type car struct {
    brand string
}

// dog类型实现Mover接口
func (d dog) move() {
    fmt.Printf("%s会跑\n", d.name)
}

// car类型实现Mover接口
func (c car) move() {
    fmt.Printf("%s速度70迈\n", c.brand)
}

这个时候我们在代码中就可以把狗和汽车当成一个会动的物体来处理了,不再需要关注它们具体是什么,只需要调用它们的 move 方法就可以了。

func main() {
    var x Mover
    var a = dog{name: "旺财"}
    var b = car{brand: "保时捷"}
    x = a
    x.move()
    x = b
    x.move()
} 

上面的代码执行结果如下:

    旺财会跑
    保时捷速度70迈 

并且一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。

// WashingMachine 洗衣机
type WashingMachine interface {
    wash()
    dry()
}

// 甩干器
type dryer struct{}

// 实现WashingMachine接口的dry()方法
func (d dryer) dry() {
    fmt.Println("甩一甩")
}

// 海尔洗衣机
type haier struct {
    dryer //嵌入甩干器
}

// 实现WashingMachine接口的wash()方法
func (h haier) wash() {
    fmt.Println("洗刷刷")
}
接口嵌套#

接口与接口间可以通过嵌套创造出新的接口。

// Sayer 接口
type Sayer interface {
    say()
}

// Mover 接口
type Mover interface {
    move()
}

// 接口嵌套
type animal interface {
    Sayer
    Mover
} 

嵌套得到的接口的使用与普通接口一样,这里我们让 cat 实现 animal 接口:

type cat struct {
    name string
}

func (c cat) say() {
    fmt.Println("喵喵喵")
}

func (c cat) move() {
    fmt.Println("猫会动")
}

func main() {
    var x animal
    x = cat{name: "花花"}
    x.move()
    x.say()
}  

空接口#

空接口的定义#

空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。

空接口类型的变量可以存储任意类型的变量。

func main() {
    // 定义一个空接口x
    var x interface{}
    s := "pprof.cn"
    x = s
    fmt.Printf("type:%T value:%v\n", x, x)
    i := 100
    x = i
    fmt.Printf("type:%T value:%v\n", x, x)
    b := true
    x = b
    fmt.Printf("type:%T value:%v\n", x, x)
}
空接口的应用#
空接口作为函数的参数

使用空接口实现可以接收任意类型的函数参数。

// 空接口作为函数参数
func show(a interface{}) {
    fmt.Printf("type:%T value:%v\n", a, a)
} 
空接口作为 map 的值#

使用空接口实现可以保存任意值的字典。

// 空接口作为map值
    var studentInfo = make(map[string]interface{})
    studentInfo["name"] = "李白"
    studentInfo["age"] = 18
    studentInfo["married"] = false
    fmt.Println(studentInfo) 
类型断言#

空接口可以存储任意类型的值,那我们如何获取其存储的具体数据呢?

接口值

一个接口的值(简称接口值)是由一个具体类型和具体类型的值两部分组成的。这两部分分别称为接口的动态类型和动态值。

我们来看一个具体的例子:

var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil 

请看下图分解:

null

想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:

    x.(T) 

其中:

    x:表示类型为interface{}的变量
    T:表示断言x可能是的类型。

该语法返回两个参数,第一个参数是 x 转化为 T 类型后的变量,第二个值是一个布尔值,若为 true 则表示断言成功,为 false 则表示断言失败。

举个例子:

func main() {
    var x interface{}
    x = "pprof.cn"
    v, ok := x.(string)
    if ok {
        fmt.Println(v)
    } else {
        fmt.Println("类型断言失败")
    }
} 

上面的示例中如果要断言多次就需要写多个 if 判断,这个时候我们可以使用 switch 语句来实现:

func justifyType(x interface{}) {
    switch v := x.(type) {
    case string:
        fmt.Printf("x is a string,value is %v\n", v)
    case int:
        fmt.Printf("x is a int is %v\n", v)
    case bool:
        fmt.Printf("x is a bool is %v\n", v)
    default:
        fmt.Println("unsupport type!")
    }
} 

因为空接口可以存储任意类型值的特点,所以空接口在 Go 语言中的使用十分广泛。

关于接口需要注意的是,只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗。

异常处理#

Go 中可以抛出一个 panic 的异常,然后在 defer 中通过 recover 捕获这个异常,然后正常处理。

panic:

    1、内置函数
    2、假如函数F中书写了panic语句,会终止其后要执行的代码,在panic所在函数F内如果存在要执行的defer函数列表,按照defer的逆序执行
    3、返回函数F的调用者G,在G中,调用函数F语句之后的代码不会执行,假如函数G中存在要执行的defer函数列表,按照defer的逆序执行
    4、直到goroutine整个退出,并报告错误

recover:

    1、内置函数
    2、用来控制一个goroutine的panicking行为,捕获panic,从而影响应用的行为
    3、一般的调用建议
        a). 在defer函数中,通过recever来终止一个goroutine的panicking过程,从而恢复正常代码的执行
        b). 可以获取通过panic传递的error

注意:

    1.利用recover处理panic指令,defer 必须放在 panic 之前定义,另外 recover 只有在 defer 调用的函数中才有效。否则当panic时,recover无法捕获到panic,无法防止panic扩散。
    2.recover 处理异常后,逻辑并不会恢复到 panic 那个点去,函数跑到 defer 之后的那个点。
    3.多个 defer 会形成 defer 栈,后定义的 defer 语句会被最先调用。
package main

func main() {
    test()
}

func test() {
    defer func() {
        if err := recover(); err != nil {
            println(err.(string)) // 将 interface{} 转型为具体类型。
        }
    }()

    panic("panic error!")
}  

输出结果:

    panic error! 

由于 panic、recover 参数类型为 interface {},因此可抛出任何类型对象。

    func panic(v interface{})
    func recover() interface{}

向已关闭的通道发送数据会引发 panic

package main

import (
    "fmt"
)

func main() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err)
        }
    }()

    var ch chan int = make(chan int, 10)
    close(ch)
    ch <- 1
}  

输出结果:

    send on closed channel

延迟调用中引发的错误,可被后续延迟调用捕获,但仅最后一个错误可被捕获。

package main

import "fmt"

func test() {
    defer func() {
        fmt.Println(recover())
    }()

    defer func() {
        panic("defer panic")
    }()

    panic("test panic")
}

func main() {
    test()
} 

输出:

    defer panic 

捕获函数 recover 只有在延迟调用内直接调用才会终止错误,否则总是返回 nil。任何未捕获的错误都会沿调用堆栈向外传递。

package main

import "fmt"

func test() {
    defer func() {
        fmt.Println(recover()) //有效
    }()
    defer recover()              //无效!
    defer fmt.Println(recover()) //无效!
    defer func() {
        func() {
            println("defer inner")
            recover() //无效!
        }()
    }()

    panic("test panic")
}

func main() {
    test()
}

输出:

    defer inner
    <nil>
    test panic

使用延迟匿名函数或下面这样都是有效的。

package main

import (
    "fmt"
)

func except() {
    fmt.Println(recover())
}

func test() {
    defer except()
    panic("test panic")
}

func main() {
    test()
}

输出结果:

    test panic

如果需要保护代码 段,可将代码块重构成匿名函数,如此可确保后续代码被执 。

package main

import "fmt"

func test(x, y int) {
    var z int

    func() {
        defer func() {
            if recover() != nil {
                z = 0
            }
        }()
        panic("test panic")
        z = x / y
        return
    }()

    fmt.Printf("x / y = %d\n", z)
}

func main() {
    test(2, 1)
}

输出结果:

    x / y = 0

除用 panic 引发中断性错误外,还可返回 error 类型错误对象来表示函数调用状态。

type error interface {
    Error() string
} 

标准库 errors.New 和 fmt.Errorf 函数用于创建实现 error 接口的错误对象。通过判断错误对象实例来确定具体错误类型。

package main

import (
    "errors"
    "fmt"
)

var ErrDivByZero = errors.New("division by zero")

func div(x, y int) (int, error) {
    if y == 0 {
        return 0, ErrDivByZero
    }
    return x / y, nil
}

func main() {
    defer func() {
        fmt.Println(recover())
    }()
    switch z, err := div(10, 0); err {
    case nil:
        println(z)
    case ErrDivByZero:
        panic(err)
    }
} 

输出结果:

    division by zero

Go 实现类似 try catch 的异常处理

package main

import "fmt"

func Try(fun func(), handler func(interface{})) {
    defer func() {
        if err := recover(); err != nil {
            handler(err)
        }
    }()
    fun()
}

func main() {
    Try(func() {
        panic("test panic")
    }, func(err interface{}) {
        fmt.Println(err)
    })
} 

输出结果:

    test panic

IO 流#

package main

import (
   "bufio"
   "fmt"
   "io"
   "log"
   "os"
)

func main01() {
   file := Create("a.txt")
   defer func(file *os.File) {
      err := file.Close()
      if err != nil {
         log.Fatal("文件关闭失败")
      }
   }(file)
   Write(file)

   fp := Create("b.txt")
   defer func(file *os.File) {
      err := file.Close()
      if err != nil {
         log.Fatal("文件关闭失败")
      }
   }(fp)
   //b := []byte{'h', 'e', 'l', 'l', 'o'}
   b := []byte("HelloWorld")
   _, _ = fp.Write(b)
}

func Write(file *os.File) {
   n, _ := file.WriteString("文件写入")
   fmt.Println(n)
   n1, _ := file.WriteString("文件hhh")
   fmt.Println(n1)
}

// Create 创建文件
func Create(path string) (file *os.File) {
   //创建文件 文件路径
   file, err := os.Create(path)
   if err != nil {
      log.Fatal(err.Error())
   }
   fmt.Println("文件创建成功")
   return file
}

func main02() {
   //打开文件
   file, err := os.OpenFile("a.txt", os.O_RDWR, 6)
   defer file.Close()
   if err != nil {
      fmt.Println(err.Error())
   }
   //覆盖文件原来的内容进行写入
   _, err = file.WriteAt([]byte("芜湖"), 3)
   if err != nil {
      fmt.Errorf(err.Error())
   }
}

func main03() {
   //打开文件
   file, err := os.OpenFile("a.txt", os.O_RDWR, 6)
   defer file.Close()
   if err != nil {
      fmt.Println(err.Error())
   }
   //将偏移量移动到文件末尾
   seek, err := file.Seek(0, io.SeekEnd)
   if err != nil {
      fmt.Errorf(err.Error())
   }
   _, err = file.WriteAt([]byte("芜湖"), seek)
   if err != nil {
      fmt.Errorf(err.Error())
   }
}

func main04() {
   //打开文件
   file, err := os.Open("b.txt")
   if err != nil {
      fmt.Errorf(err.Error())
   }
   b := make([]byte, 1024)
   for i := 0; i < len(b); i++ {
      file.Read(b)
   }
   fmt.Println(b)
}

func main05() {
   //打开文件
   file, err := os.Open("b.txt")
   if err != nil {
      fmt.Errorf(err.Error())
   }
   r := bufio.NewReader(file)
   b, _ := r.ReadBytes('\n')
   fmt.Println(string(b))
   b, _ = r.ReadBytes('\n')
}

//字节读取
func main06() {
   file, err := os.Open("b.txt")
   if err != nil {
      fmt.Println(err.Error())
   }
   defer func(file *os.File) {
      err := file.Close()
      if err != nil {
         fmt.Println("文件关闭失败")
      }
   }(file)
   b := make([]byte, 20)
   for {
      n, err := file.Read(b)
      //表示读取到文件结尾了
      if err == io.EOF {
         break
      }
      fmt.Println(string(b[:n]))
   }
}

//行读取
func main07() {
   file, err := os.Open("b.txt")
   if err != nil {
      fmt.Println(err.Error())
   }
   defer func(file *os.File) {
      err := file.Close()
      if err != nil {
         fmt.Println("文件关闭失败")
      }
   }(file)
   r := bufio.NewReader(file)
   for {
      bytes, err := r.ReadBytes('\n')
      fmt.Print(string(bytes))
      if err == io.EOF {
         break
      }
   }
}

//文件拷贝
func main() {
   file, err := os.Open("labuladong.pdf")
   if err != nil {
      fmt.Println("文件打开失败")
   }
   defer func(file *os.File) {
      err := file.Close()
      if err != nil {
         fmt.Println("关闭失败")
      }
   }(file)

   fp, err := os.Create("labuladonghhhh.pdf")
   if err != nil {
      fmt.Println("文件创建失败")
   }
   defer func(fp *os.File) {
      err := fp.Close()
      if err != nil {
         fmt.Println("关闭失败")
      }
   }(fp)

   data := make([]byte, 1*1024*1024)
   for {
      n, err := file.Read(data)
      if err == io.EOF {
         break
      }
      fp.Write(data[:n])
   }
}

Test 测试#

例如测试这个方法

func calcTriangle(a, b int) int {
   var c int
   c = int(math.Sqrt(float64(a*a + b*b)))
   return c
}

可以在同目录下创建对应的 包名_test.go

image-20220418005055464

使用表格驱动测试

func Test_calcTriangle(t *testing.T) {
   tests := []struct {
      a, b, c int
   }{
      {3, 4, 5},
      {8, 15, 17},
      {6, 8, 10},
      {12, 35, 37},
      {30000, 40000, 50000},
   }
   for _, tt := range tests {
      if got := calcTriangle(tt.a, tt.b); got != tt.c {
         t.Errorf("calcTriangle() = %v, want %v", got, tt.c)
      }
   }
}

testing.B 是性能测试

func BenchmarkName(b *testing.B) {
   for i := 0; i < b.N; i++ {
      arr := []int{1, 9, 10, 30, 2, 5, 45, 8, 63, 234, 12}
      for i := 0; i < len(arr); i++ {
         for j := i + 1; j < len(arr); j++ {
            if arr[i] > arr[j] {
               arr[i], arr[j] = arr[j], arr[i]
            }
         }
      }
   }
}

标准库#

strings#

package main

import (
   "fmt"
   "strconv"
   "strings"
)

func main01() {
   //判断字符串是否包含在目标字符串里
   str := "hello world"
   if isCont := strings.Contains(str, "hello"); isCont {
      fmt.Println("存在")
   } else {
      fmt.Println("不存在")
   }

   //字符串拼接
   s := []string{"1234", "3876", "9017", "1859"}
   str = strings.Join(s, "-")
   fmt.Println(str)

   //返回目标字符串第一次出现的字符
   str1 := "日照香炉生紫烟"
   fmt.Println(strings.Index(str1, "生"))

   //字符串重复
   str2 := "日照香炉生紫烟"
   repeat := strings.Repeat(str2, 2)
   fmt.Println(repeat)

   //字符串替换
   str3 := "日照香炉生紫烟烟"
   //源字符,替换字符,替换成的字符,替换次数 -1代表全部替换
   replace := strings.Replace(str3, "烟", "hhh", -1)
   fmt.Println(replace)

   //字符串分割
   str4 := "日,照,香,炉,生,紫,烟"
   split := strings.Split(str4, ",")
   fmt.Println(split)

   //去除前后空格
   str5 := "           日照香炉生紫烟              "
   trim := strings.Trim(str5, " ")
   fmt.Println(trim)

   str6 := "           日 照 香   炉  生 紫 烟              "
   fields := strings.Fields(str6)
   fmt.Println(fields)
}

strconv#

func main02() {
   //字符串转换
   formatBool := strconv.FormatBool(true)
   fmt.Println(formatBool)

   //目标值 进制
   s := strconv.FormatInt(123, 10)
   fmt.Println(s)
   s = strconv.FormatInt(3, 3)
   fmt.Println(s)

   //数字转成10进制
   itoa := strconv.Itoa(10)
   fmt.Println(itoa)
   //数据 'f' 小数点后几位保留 以几位处理
   //会四舍五入
   float := strconv.FormatFloat(3.1232224, 'f', 3, 64)
   fmt.Println(float)
}

func main() {
   //将字符串转成其他类型
   str := "false"
   parseBool, _ := strconv.ParseBool(str)
   fmt.Println(parseBool)
   str1 := "111"
   fmt.Println(strconv.ParseInt(str1, 10, 64))

   atoi, _ := strconv.Atoi(str1)
   fmt.Println(atoi)

   //将整数转成字符后添加到切片里
   b := make([]byte, 0, 1024)
   b = strconv.AppendBool(b, false)
   b = strconv.AppendInt(b, 10, 10)
   fmt.Println(string(b))
}

进阶#

并发编程#

goroutine#

使用 goroutine#

Go 语言中使用 goroutine 非常简单,只需要在调用函数的时候在前面加上 go 关键字,就可以为一个函数创建一个 goroutine。

一个 goroutine 必定对应一个函数,可以创建多个 goroutine 去执行相同的函数。

启动单个 goroutine#

启动 goroutine 的方式非常简单,只需要在调用的函数(普通函数和匿名函数)前面加上一个 go 关键字。

举个例子如下:

func hello() {
    fmt.Println("Hello Goroutine!")
}
func main() {
    hello()
    fmt.Println("main goroutine done!")
}

这个示例中 hello 函数和下面的语句是串行的,执行的结果是打印完 Hello Goroutine! 后打印 main goroutine done!。

接下来我们在调用 hello 函数前面加上关键字 go,也就是启动一个 goroutine 去执行 hello 这个函数。

func main() {
    go hello() // 启动另外一个goroutine去执行hello函数
    fmt.Println("main goroutine done!")
}  

这一次的执行结果只打印了 main goroutine done!,并没有打印 Hello Goroutine!。为什么呢?

在程序启动时,Go 程序就会为 main () 函数创建一个默认的 goroutine。

当 main () 函数返回的时候该 goroutine 就结束了,所有在 main () 函数中启动的 goroutine 会一同结束,main 函数所在的 goroutine 就像是权利的游戏中的夜王,其他的 goroutine 都是异鬼,夜王一死它转化的那些异鬼也就全部 GG 了。

所以我们要想办法让 main 函数等一等 hello 函数,最简单粗暴的方式就是 time.Sleep 了。

func main() {
    go hello() // 启动另外一个goroutine去执行hello函数
    fmt.Println("main goroutine done!")
    time.Sleep(time.Second)
}  

执行上面的代码你会发现,这一次先打印 main goroutine done!,然后紧接着打印 Hello Goroutine!。

首先为什么会先打印 main goroutine done! 是因为我们在创建新的 goroutine 的时候需要花费一些时间,而此时 main 函数所在的 goroutine 是继续执行的。

启动多个 goroutine#

在 Go 语言中实现并发就是这样简单,我们还可以启动多个 goroutine。让我们再来一个例子: (这里使用了 sync.WaitGroup 来实现 goroutine 的同步)

var wg sync.WaitGroup

func hello(i int) {
    defer wg.Done() // goroutine结束就登记-1
    fmt.Println("Hello Goroutine!", i)
}
func main() {

    for i := 0; i < 10; i++ {
        wg.Add(1) // 启动一个goroutine就登记+1
        go hello(i)
    }
    wg.Wait() // 等待所有登记的goroutine都结束
}

多次执行上面的代码,会发现每次打印的数字的顺序都不一致。这是因为 10 个 goroutine 是并发执行的,而 goroutine 的调度是随机的。

goroutine 与线程#
可增长的栈

OS 线程(操作系统线程)一般都有固定的栈内存(通常为 2MB), 一个 goroutine 的栈在其生命周期开始时只有很小的栈(典型情况下 2KB),goroutine 的栈不是固定的,他可以按需增大和缩小,goroutine 的栈大小限制可以达到 1GB,虽然极少会用到这个大。所以在 Go 语言中一次创建十万左右的 goroutine 也是可以的。

goroutine 调度

GPM 是 Go 语言运行时(runtime)层面的实现,是 go 语言自己实现的一套调度系统。区别于操作系统调度 OS 线程。

  • 1.G 很好理解,就是个 goroutine 的,里面除了存放本 goroutine 信息外 还有与所在 P 的绑定等信息。
  • 2.P 管理着一组 goroutine 队列,P 里面会存储当前 goroutine 运行的上下文环境(函数指针,堆栈地址及地址边界),P 会对自己管理的 goroutine 队列做一些调度(比如把占用 CPU 时间较长的 goroutine 暂停、运行后续的 goroutine 等等)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他 P 的队列里抢任务。
  • 3.M(machine)是 Go 运行时(runtime)对操作系统内核线程的虚拟, M 与内核线程一般是一一映射的关系, 一个 groutine 最终是要放到 M 上执行的;

P 与 M 一般也是一一对应的。他们关系是: P 管理着一组 G 挂载在 M 上运行。当一个 G 长久阻塞在一个 M 上时,runtime 会新建一个 M,阻塞 G 所在的 P 会把其他的 G 挂载在新建的 M 上。当旧的 G 阻塞完成或者认为其已经死掉时 回收旧的 M。

P 的个数是通过 runtime.GOMAXPROCS 设定(最大 256),Go1.5 版本之后默认为物理线程数。 在并发量大的时候会增加一些 P 和 M,但不会太多,切换太频繁的话得不偿失。

单从线程调度讲,Go 语言相比起其他语言的优势在于 OS 线程是由 OS 内核来调度的,goroutine 则是由 Go 运行时(runtime)自己的调度器调度的,这个调度器使用一个称为 m调度的技术(复用 / 调度 m 个 goroutine 到 n 个 OS 线程)。 其一大特点是 goroutine 的调度是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的 malloc 函数(除非内存池需要改变),成本比调度 OS 线程低很多。 另一方面充分利用了多核的硬件资源,近似的把若干 goroutine 均分在物理线程上, 再加上本身 goroutine 的超轻量,以上种种保证了 go 调度方面的性能。

runtime#

runtime.Gosched()#

让出 CPU 时间片,重新等待安排任务 (大概意思就是本来计划的好好的周末出去烧烤,但是你妈让你去相亲,两种情况第一就是你相亲速度非常快,见面就黄不耽误你继续烧烤,第二种情况就是你相亲速度特别慢,见面就是你侬我侬的,耽误了烧烤,但是还馋就是耽误了烧烤你还得去烧烤)

package main

import (
    "fmt"
    "runtime"
)

func main() {
    go func(s string) {
        for i := 0; i < 2; i++ {
            fmt.Println(s)
        }
    }("world")
    // 主协程
    for i := 0; i < 2; i++ {
        // 切一下,再次分配任务
        runtime.Gosched()
        fmt.Println("hello")
    }
}
runtime.Goexit()#

退出当前协程 (一边烧烤一边相亲,突然发现相亲对象太丑影响烧烤,果断让她滚蛋,然后也就没有然后了)

package main

import (
    "fmt"
    "runtime"
)

func main() {
    go func() {
        defer fmt.Println("A.defer")
        func() {
            defer fmt.Println("B.defer")
            // 结束协程
            runtime.Goexit()
            defer fmt.Println("C.defer")
            fmt.Println("B")
        }()
        fmt.Println("A")
    }()
    for {
    }
}
runtime.GOMAXPROCS#

Go 运行时的调度器使用 GOMAXPROCS 参数来确定需要使用多少个 OS 线程来同时执行 Go 代码。默认值是机器上的 CPU 核心数。例如在一个 8 核心的机器上,调度器会把 Go 代码同时调度到 8 个 OS 线程上(GOMAXPROCS 是 m调度中的 n)。

Go 语言中可以通过 runtime.GOMAXPROCS () 函数设置当前程序并发时占用的 CPU 逻辑核心数。

Go1.5 版本之前,默认使用的是单核心执行。Go1.5 版本之后,默认使用全部的 CPU 逻辑核心数。

我们可以通过将任务分配到不同的 CPU 逻辑核心上实现并行的效果,这里举个例子:

func a() {
    for i := 1; i < 10; i++ {
        fmt.Println("A:", i)
    }
}

func b() {
    for i := 1; i < 10; i++ {
        fmt.Println("B:", i)
    }
}

func main() {
    runtime.GOMAXPROCS(1)
    go a()
    go b()
    time.Sleep(time.Second)
}  

两个任务只有一个逻辑核心,此时是做完一个任务再做另一个任务。 将逻辑核心数设为 2,此时两个任务并行执行,代码如下。

func a() {
    for i := 1; i < 10; i++ {
        fmt.Println("A:", i)
    }
}

func b() {
    for i := 1; i < 10; i++ {
        fmt.Println("B:", i)
    }
}

func main() {
    runtime.GOMAXPROCS(2)
    go a()
    go b()
    time.Sleep(time.Second)
}  

Go 语言中的操作系统线程和 goroutine 的关系:

  • 1. 一个操作系统线程对应用户态多个 goroutine。
  • 2.go 程序可以同时使用多个操作系统线程。
  • 3.goroutine 和 OS 线程是多对多的关系,即 m

channel#

单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。

虽然可以使用共享内存进行数据交换,但是共享内存在不同的 goroutine 中容易发生竞态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。

Go 语言的并发模型是 CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。

如果说 goroutine 是 Go 程序并发的执行体,channel 就是它们之间的连接。channel 是可以让一个 goroutine 发送特定值到另一个 goroutine 的通信机制。

Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明 channel 的时候需要为其指定元素类型。

channel 类型#

channel 是一种类型,一种引用类型。声明通道类型的格式如下:

    var 变量 chan 元素类型  

举几个例子:

    var ch1 chan int   // 声明一个传递整型的通道
    var ch2 chan bool  // 声明一个传递布尔型的通道
    var ch3 chan []int // 声明一个传递int切片的通道    
创建 channel#

通道是引用类型,通道类型的空值是 nil。

var ch chan int
fmt.Println(ch) // <nil>

声明的通道后需要使用 make 函数初始化之后才能使用。

创建 channel 的格式如下:

    make(chan 元素类型, [缓冲大小])   

channel 的缓冲大小是可选的。

举几个例子:

ch4 := make(chan int)
ch5 := make(chan bool)
ch6 := make(chan []int)    
channel 操作#

通道有发送(send)、接收 (receive)和关闭(close)三种操作。

发送和接收都使用 <- 符号。

现在我们先使用以下语句定义一个通道:

ch := make(chan int)    
发送

将一个值发送到通道中。

ch <- 10 // 把10发送到ch中   
接收

从一个通道中接收值。

x := <- ch // 从ch中接收值并赋值给变量x
<-ch       // 从ch中接收值,忽略结果   
关闭

我们通过调用内置的 close 函数来关闭通道。

    close(ch)   

关于关闭通道需要注意的事情是,只有在通知接收方 goroutine 所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。

关闭后的通道有以下特点:

    1.对一个关闭的通道再发送值就会导致panic。
    2.对一个关闭的通道进行接收会一直获取值直到通道为空。
    3.对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
    4.关闭一个已经关闭的通道会导致panic。  
无缓冲的通道#

null

无缓冲的通道又称为阻塞的通道。我们来看一下下面的代码:

func main() {
    ch := make(chan int)
    ch <- 10
    fmt.Println("发送成功")
}   

上面这段代码能够通过编译,但是执行的时候会出现以下错误:

    fatal error: all goroutines are asleep - deadlock!

    goroutine 1 [chan send]:
    main.main()
            .../src/github.com/pprof/studygo/day06/channel02/main.go:8 +0x54   

为什么会出现 deadlock 错误呢?

因为我们使用 ch := make (chan int) 创建的是无缓冲的通道,无缓冲的通道只有在有人接收值的时候才能发送值。就像你住的小区没有快递柜和代收点,快递员给你打电话必须要把这个物品送到你的手中,简单来说就是无缓冲的通道必须有接收才能发送。

上面的代码会阻塞在 ch <- 10 这一行代码形成死锁,那如何解决这个问题呢?

一种方法是启用一个 goroutine 去接收值,例如:

func recv(c chan int) {
    ret := <-c
    fmt.Println("接收成功", ret)
}
func main() {
    ch := make(chan int)
    go recv(ch) // 启用goroutine从通道接收值
    ch <- 10
    fmt.Println("发送成功")
}   

无缓冲通道上的发送操作会阻塞,直到另一个 goroutine 在该通道上执行接收操作,这时值才能发送成功,两个 goroutine 将继续执行。相反,如果接收操作先执行,接收方的 goroutine 将阻塞,直到另一个 goroutine 在该通道上发送一个值。

使用无缓冲通道进行通信将导致发送和接收的 goroutine 同步化。因此,无缓冲通道也被称为同步通道。

有缓冲的通道#

解决上面问题的方法还有一种就是使用有缓冲区的通道。

null

我们可以在使用 make 函数初始化通道的时候为其指定通道的容量,例如:

func main() {
    ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
    ch <- 10
    fmt.Println("发送成功")
}   

只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量。就像你小区的快递柜只有那么个多格子,格子满了就装不下了,就阻塞了,等到别人取走一个快递员就能往里面放一个。

我们可以使用内置的 len 函数获取通道内元素的数量,使用 cap 函数获取通道的容量,虽然我们很少会这么做。

close()#

可以通过内置的 close () 函数关闭 channel(如果你的管道不往里存值或者取值的时候一定记得关闭管道)

package main

import "fmt"

func main() {
    c := make(chan int)
    go func() {
        for i := 0; i < 5; i++ {
            c <- i
        }
        close(c)
    }()
    for {
        if data, ok := <-c; ok {
            fmt.Println(data)
        } else {
            break
        }
    }
    fmt.Println("main结束")
}
优雅的从通道循环取值#

当通过通道发送有限的数据时,我们可以通过 close 函数关闭通道来告知从该通道接收值的 goroutine 停止等待。当通道被关闭时,往该通道发送值会引发 panic,从该通道里接收的值一直都是类型零值。那如何判断一个通道是否被关闭了呢?

我们来看下面这个例子:

// channel 练习
func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)
    // 开启goroutine将0~100的数发送到ch1中
    go func() {
        for i := 0; i < 100; i++ {
            ch1 <- i
        }
        close(ch1)
    }()
    // 开启goroutine从ch1中接收值,并将该值的平方发送到ch2中
    go func() {
        for {
            i, ok := <-ch1 // 通道关闭后再取值ok=false
            if !ok {
                break
            }
            ch2 <- i * i
        }
        close(ch2)
    }()
    // 在主goroutine中从ch2中接收值打印
    for i := range ch2 { // 通道关闭后会退出for range循环
        fmt.Println(i)
    }
}   

从上面的例子中我们看到有两种方式在接收值的时候判断通道是否被关闭,我们通常使用的是 for range 的方式。

单向通道#

有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如限制通道在函数中只能发送或只能接收。

Go 语言中提供了单向通道来处理这种情况。例如,我们把上面的例子改造如下:

func counter(out chan<- int) {
    for i := 0; i < 100; i++ {
        out <- i
    }
    close(out)
}

func squarer(out chan<- int, in <-chan int) {
    for i := range in {
        out <- i * i
    }
    close(out)
}
func printer(in <-chan int) {
    for i := range in {
        fmt.Println(i)
    }
}

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)
    go counter(ch1)
    go squarer(ch2, ch1)
    printer(ch2)
}   

其中,

    1.chan<- int是一个只能发送的通道,可以发送但是不能接收;
    2.<-chan int是一个只能接收的通道,可以接收但是不能发送。   

在函数传参及任何赋值操作中将双向通道转换为单向通道是可以的,但反过来是不可以的。

总结#

image-20220420210031635

例子#
package main

import (
   "fmt"
   "time"
)

var channel = make(chan int)

func printer(s string) {
   for _, ch := range s {
      fmt.Printf("%c", ch)
      time.Sleep(300 * time.Millisecond)
   }
}

func person1() {
   printer("hello")
   channel <- 8
}
func person2() {
   <-channel
   printer("word")
}

func main01() {
   go person1()
   go person2()
   for {

   }
}

func main02() {
   ch := make(chan string)
   go func() {
      for i := 0; i < 2; i++ {
         fmt.Println("i=", i)
      }
      //通知主go打印完毕
      ch <- "打印完毕"
   }()
   str := <-ch
   fmt.Println(str)
}

func main03() {
   //无缓冲channel
   ch := make(chan int)
   go func() {
      for i := 0; i < 5; i++ {
         fmt.Println("子go程, i = ", i)
         ch <- i
      }
   }()
   for i := 0; i < 5; i++ {
      num := <-ch
      fmt.Println(num)
   }

}

func main() {
   //无缓冲channel 关闭后还能读,默认值是0
   ch := make(chan int)
   go func() {
      for i := 0; i < 8; i++ {
         ch <- i
         fmt.Println("子go程 i= ", i, " len : ", len(ch), " cap ", cap(ch))
      }
      close(ch)
   }()
   for {
      if num, ok := <-ch; ok {
         fmt.Println("主go程 读= ", num)
      } else {
         fmt.Println(<-ch)
         break
      }
   }
}

定时器#

package main

import (
   "fmt"
   "time"
)

func main01() {
   fmt.Println(time.Now())
   //创建定时器
   myTime := time.NewTimer(time.Second * 2)
   //定时满系统会自动写入时间
   nowTime := <-myTime.C
   fmt.Println(nowTime)
}

func main02() {
   fmt.Println(time.Now())
   nowTime := <-time.After(time.Second * 2)
   fmt.Println(nowTime)

}
func main03() {
   myTimer := time.NewTimer(1 * time.Second)
   go func() {
      <-myTimer.C
      fmt.Println("子go程读取定时完毕")
      // 每5分钟执行一次
   }()
   for {

   }
}
func main() {
   quit := make(chan bool)
   myTicker := time.NewTicker(1 * time.Second)
   i := 0
   go func() {
      for {
         <-myTicker.C
         i++
         //定时执行此方法
         fmt.Println("哈哈哈哈哈")
         if i == 10 {
            quit <- true
         }
      }
   }()
   <-quit
}

select#

多路复用#

在某些场景下我们需要同时从多个通道接收数据。通道在接收数据时,如果没有数据可以接收将会发生阻塞。你也许会写出如下代码使用遍历的方式来实现:

for{
    // 尝试从ch1接收值
    data, ok := <-ch1
    // 尝试从ch2接收值
    data, ok := <-ch2

}

这种方式虽然可以实现从多个通道接收值的需求,但是运行性能会差很多。为了应对这种场景,Go 内置了 select 关键字,可以同时响应多个通道的操作。

select 的使用类似于 switch 语句,它有一系列 case 分支和一个默认的分支。每个 case 会对应一个通道的通信(接收或发送)过程。select 会一直等待,直到某个 case 的通信操作完成时,就会执行 case 分支对应的语句。具体格式如下:

    select {
    case <-chan1:
       // 如果chan1成功读到数据,则进行该case处理语句
    case chan2 <- 1:
       // 如果成功向chan2写入数据,则进行该case处理语句
    default:
       // 如果上面都没有成功,则进入default处理流程
    }
  • select 可以同时监听一个或多个 channel,直到其中一个 channel ready
package main

import (
   "fmt"
   "time"
)

func test1(ch chan string) {
   time.Sleep(time.Second * 5)
   ch <- "test1"
}
func test2(ch chan string) {
   time.Sleep(time.Second * 2)
   ch <- "test2"
}

func main() {
   // 2个管道
   output1 := make(chan string)
   output2 := make(chan string)
   // 跑2个子协程,写数据
   go test1(output1)
   go test2(output2)
   // 用select监控
   select {
   case s1 := <-output1:
      fmt.Println("s1=", s1)
   case s2 := <-output2:
      fmt.Println("s2=", s2)
   }
}
  • 如果多个 channel 同时 ready,则随机选择一个执行
package main

import (
   "fmt"
)

func main() {
   // 创建2个管道
   int_chan := make(chan int, 1)
   string_chan := make(chan string, 1)
   go func() {
      //time.Sleep(2 * time.Second)
      int_chan <- 1
   }()
   go func() {
      string_chan <- "hello"
   }()
   select {
   case value := <-int_chan:
      fmt.Println("int:", value)
   case value := <-string_chan:
      fmt.Println("string:", value)
   }
   fmt.Println("main结束")
}
  • 可以用于判断管道是否存满
package main

import (
   "fmt"
   "time"
)

// 判断管道有没有存满
func main() {
   // 创建管道
   output1 := make(chan string, 10)
   // 子协程写数据
   go write(output1)
   // 取数据
   for s := range output1 {
      fmt.Println("res:", s)
      time.Sleep(time.Second)
   }
}

func write(ch chan string) {
   for {
      select {
      // 写数据
      case ch <- "hello":
         fmt.Println("write hello")
      default:
         fmt.Println("channel full")
      }
      time.Sleep(time.Millisecond * 500)
   }
}
例子#
package main

import (
   "fmt"
   "time"
)

func main01() {
   ch := make(chan int)    //数据通信
   quit := make(chan bool) //判断是否退出
   go func() {             //写数据
      for i := 0; i < 5; i++ {
         ch <- i
         time.Sleep(time.Second)
      }
      close(ch)
      quit <- true //通知主go程退出
      close(quit)
   }()

   go func() {

   }()

   for {
      select {
      case data := <-ch:
         fmt.Println(data)
      case <-quit:
         //break //beak跳出select
         return //终止进程
      }
   }
   fmt.Println("=============")
}

//chan 实现斐波那契数列
func main() {
   ch := make(chan int)
   quit := make(chan bool)
   go func() {
      for i := 1; i <= 20; i++ {
         ch <- fib(i)
      }
      close(ch)
      quit <- true //通知主go程退出
      close(quit)
   }()
   for {
      select {
      case data := <-ch:
         fmt.Println(data)
      case <-quit:
         return
      }
   }
}

func fib(n int) int {
   if n < 3 {
      return 1
   }
   res := make([]int, n+1)
   res[1] = 1
   res[2] = 1
   for i := 3; i <= n; i++ {
      res[i] = res[i-1] + res[i-2]
   }
   return res[n]
}

并发安全和锁#

死锁#

channel 应该在至少 2 个以上的 go 程进行通信,否则会发生死锁

使用 channel 一端读 (写),要保证另一端写 (读) 操作,同时有机会执行。否则死锁

GMP 模型#

image-20220516221138875

并发模型案例#

广播模式#
func broadcast(ch chan struct{} )  {
   fmt.Println("开始广播!")
   close(ch)
}
func worker(ch chan struct{}) {
   <-ch
   fmt.Println("收到广播")
}
func main_B() {
   ch := make(chan struct{})
   wg := sync.WaitGroup{}
   wg.Add(5)
   for i := 0; i < 5; i++ {
      go func() {
         defer wg.Done()
         worker(ch)
      }()
   }
   broadcast(ch)
   wg.Wait()
}
channel 超时控制#
func WorkWithTimeout(timeout time.Duration) {
	workCh := make(chan struct{}, 1)
	go func() {
		//LongTimeWork() //把要控制超时的函数放到子协程里去执行
		workCh <- struct{}{}
	}()
	select { //只执行最先到来的case
	case <-workCh: //work先结束
		fmt.Println("work finish")
	case <-time.After(timeout): //超时先来
		fmt.Println("work timeout")
	}
}
上下游模式#
func main() {
   upstreamNum := 4
   downstreamNum := 5
   upstreamCh := make(chan struct{}, upstreamNum)
   downstreamCh := make(chan struct{}, downstreamNum)
   for i := 0; i < upstreamNum; i++ {
      go upstream(upstreamCh)
   }
   for i := 0; i < downstreamNum; i++ {
      go downstream(downstreamCh)
   }
   //释放上游
   for i := 0; i < upstreamNum; i++ {
      <-upstreamCh
   }
   //下游执行
   for i := 0; i < downstreamNum; i++ {
      downstreamCh <- struct{}{}
   }
   time.Sleep(1 * time.Second)
}


func upstream(ch chan struct{}) {
   time.Sleep(1 * time.Second)
   fmt.Println("一个上游执行结束")
   ch<- struct{}{}
}
func downstream(ch chan struct{}) {
   <-ch
   fmt.Println("下游开始执行")
}

(待) 网络编程#

(待) 反射#

(待) Web#

Gin#

Gorm#

连接数据库#

dsn := "用户名:密码@tcp(127.0.0.1:3306)/表名?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic(err)
	}

打印日志#

dsn := "root:123@tcp(127.0.0.1:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"
newLogger := logger.New(
   log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日志输出的目标,前缀和日志包含的内容——译者注)
   logger.Config{
      SlowThreshold:             time.Second,   // 慢 SQL 阈值
      LogLevel:                  logger.Silent, // 日志级别
      IgnoreRecordNotFoundError: true,          // 忽略ErrRecordNotFound(记录未找到)错误
      Colorful:                  false,         // 禁用彩色打印
   },
)
_, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
   Logger: newLogger,
})
if err != nil {
   panic(err)
}

迁移#

定义表结构#
type Product struct {
   gorm.Model
   Code  string
   Price uint
}
//定义一个表结构,自动生成表
err = db.AutoMigrate(&Product{}) //执行建表的语句

关系查询#

BelongsTo#
package main

import (
   "fmt"
   "gorm.io/gorm"
)

func BelongsTo() {
   //err := GlOBAl_DB.AutoMigrate(TestUser{})
   //GlOBAl_DB.AutoMigrate(Company{})

   //GlOBAl_DB.Create(&TestUser{
   // Name:      "bobby2",
   // CompanyID: 1,
   //})
  
  //预加载
   var user TestUser
   GlOBAl_DB.Preload("Company").First(&user)
   fmt.Println(user.Company)
}

// `User` 属于 `Company` `CompanyID` 是外键
type TestUser struct {
   gorm.Model
   Name      string
   CompanyID int //company_id
   Company   Company
}

type Company struct {
   ID   int
   Name string
}
Has Many#
package main

import (
   "fmt"
   "gorm.io/gorm"
)

func HasMany() {
   //GlOBAl_DB.AutoMigrate(&CreditCard{})
   //GlOBAl_DB.AutoMigrate(&UserTest{})
   user1 := UserTest{}
   GlOBAl_DB.Create(&user1)
   GlOBAl_DB.Create(&CreditCard{
      Number: "11111",
      UserID: user1.ID,
   })
   var user []UserTest
   GlOBAl_DB.Model(UserTest{}).Preload("CreditCards").Find(&user)
   for _, user2 := range user {
      fmt.Println(user2)
   }
}

// User 有多张 CreditCard,UserID 是外键
type UserTest struct {
   gorm.Model
   CreditCards []CreditCard `gorm:"foreignKey:UserID"`
}

type CreditCard struct {
   gorm.Model
   Number string
   UserID uint
}
Many to Many#
package main

import (
   "fmt"
   "gorm.io/gorm"
)

func Many2Many() {
   //GlOBAl_DB.AutoMigrate(ManyUserTest{})

   //languages := []Language{}
   //languages = append(languages, Language{
   // Name: "Java",
   //})
   //languages = append(languages, Language{
   // Name: "Go",
   //})
   //user:=ManyUserTest{
   // Languages: languages,
   //}
   //GlOBAl_DB.Create(&user)
   var user ManyUserTest
   GlOBAl_DB.Preload("Languages").First(&user)
   for _, lan := range user.Languages {
      fmt.Println(lan)
   }
}

// User 拥有并属于多种 language,`user_languages` 是连接表
type ManyUserTest struct {
   gorm.Model
   Languages []Language `gorm:"many2many:user_languages;"`
}

type Language struct {
   gorm.Model
   Name string
}

微服务#

rpc#

protobuf 的使用#

定义 message#
syntax = "proto3";

package proto;

import "google/protobuf/empty.proto";
import "Test/grpc_test/proto/base.proto";
option go_package = "./;proto";

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply); //Greeter接口
  rpc Ping (google.protobuf.Empty) returns (Pong);
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}
定义方法#
service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply); //Greeter接口
  rpc Ping (google.protobuf.Empty) returns (Pong);
}
message 嵌套#
message HelloReply {
  string message = 1;
  repeated Result data = 2;
}

message Result {
  string name = 1;
  string url = 2; 
}
生成 proto-go 文件#

shell 指令

protoc --go_out=plugins=grpc:. helloworld.proto
定义枚举#
message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
  enum Corpus {
    UNIVERSAL = 0;
    WEB = 1;
    IMAGES = 2;
    LOCAL = 3;
    NEWS = 4;
    PRODUCTS = 5;
    VIDEO = 6;
  }
  Corpus corpus = 4;
}
protobuf 关键字#
repeated

repeated 在 message 的类型前面加这个关键字,就是数组

message Result {
  string url = 1;
  string title = 2;
  repeated string snippets = 3; //表示字符串数组/切片string[]
}
required
message SearchResponse {
  required Result results = 1; //表示必须传的参数
}
rpc (头部携带信息) metadata#

客户端

func main() {
   //stream流模式调用
   conn, _ := grpc.Dial("127.0.0.1:8080", grpc.WithTransportCredentials(insecure.NewCredentials()))
   defer func(conn *grpc.ClientConn) {
      err := conn.Close()
      if err != nil {
         panic(err)
      }
   }(conn)
   client := proto.NewGreeterClient(conn)
   //创建metadata
   md := metadata.New(map[string]string{
      "token": "666666666666666666666",
   })
   //将metadata写入写入到context上下文中
   ctx := metadata.NewOutgoingContext(context.Background(), md)
   //和客户端一起请求发过去
   r, _ := client.SayHello(ctx, &proto.HelloRequest{Name: "zwj"})
   fmt.Println(r.Message)
}

服务端

func (s *Server) SayHello(ctx context.Context, req *proto.HelloRequest) (*proto.HelloReply, error) {
   //如果metadata不存在
   if md, ok := metadata.FromIncomingContext(ctx); !ok {
      //抛异常
      fmt.Println("get metadata error")
   } else {
      //存在并取出
      if value, ok := md["token"]; ok {
         fmt.Println(value)
      }
   }

   return &proto.HelloReply{
      Message: "Hello" + req.Name,
   }, nil
}

实战#

拓展技巧#

异常处理#

如果遇到底层的异常,可以使用第三方异常处理包 pkg/errors 进行包装 wrap,

image-20220512220428303

然后直接返回即可,而不是每个错误产生都去打日志,这样会导致层层日志一直打印,重复打印,在上层调用函数展示错误的堆栈信息,和上下文信息等,上层可以使用 % v+,err 进行展示堆栈信息

image-20220512220628645

第一条的意思是:如果当前写的这个方法或者项目被很多其他的项目引用或者使用,最好直接返回错误本身,不要进行包装,因为有可能其他的项目调用本项目会产生错误,然后进行了 warp 包装,导致多重 wrap 嵌套报错堆栈信息,造成冗余

并发#

互斥锁#

互斥锁 Mutex 就提供两个方法 Lock 和 Unlock:进入临界区之前调用 Lock 方法,退出临界区的时候调用 Unlock 方法

  func(m *Mutex)Lock()
  func(m *Mutex)Unlock()

当一个 goroutine 通过调用 Lock 方法获得了这个锁的拥有权后, 其它请求锁的 goroutine 就会阻塞在 Lock 方法的调用上,直到锁被释放并且自己获取到了这个锁的拥有权。

实例#
func main() {
   var count = 0
   // 使用WaitGroup等待10个goroutine完成
   var wg sync.WaitGroup
   var lock sync.Mutex
   wg.Add(10)
   for i := 0; i < 10; i++ {
      go func() {
         defer wg.Done()
         // 对变量count执行10次加1
         for j := 0; j < 100000; j++ {
            lock.Lock()
            count++
            lock.Unlock()
         }
      }()
   }
   // 等待10个goroutine完成
   wg.Wait()
   fmt.Println(count)
}

可以使用 Google 提供的工具对代码进行检查,就有可能发现并发问题。

go run -race main.go
易错场景#
Lock/Unlock 不是成对出现

Lock/Unlock 没有成对出现,就意味着会出现死锁的情况,或者是因为 Unlock 一个未加锁的 Mutex 而导致 panic。

常见的有三种情况:

1. 代码中有太多的 if-else 分支,可能在某个分支中漏写了 Unlock;

2. 在重构的时候把 Unlock 给删除了;

3.Unlock 误写成了 Lock。

缺少 Lock 的场景,一般来说就是误操作删除了 Lock。 比如先前使用 Mutex 都是正常的,结果后来其他人重构代码的时候,由于对代码不熟悉,或者由于开发者的马虎,把 Lock 调用给删除了,或者注释掉了。比如下面的代码,mu.Lock () 一行代码被删除了,直接 Unlock 一个未加锁的 Mutex 会 panic:

func foo() {
    var mu sync.Mutex
    defer mu.Unlock()
    fmt.Println("hello world!")
}
Copy 已经使用的 Mutex

参数传递结构体是值拷贝,如果当前的结构体内部已经上锁,会导致函数的传递参数的结构体内部也是上锁的,就会出现问题

原因在于,Mutex 是一个有状态的对象,它的 state 字段记录这个锁的状态。如果要复制一个已经加锁的 Mutex 给一个新的变量,那么新的刚初始化的变量也被加锁了,这显然不符合期望,因为期望的是一个零值的 Mutex。

type Counter struct {
    sync.Mutex
    Count int
}
func main() {
    var c Counter
    c.Lock()
    defer c.Unlock()
    c.Count++
    foo(c) // 复制锁
}
// 这里Counter的参数是通过复制的方式传入的
func foo(c Counter) {
    c.Lock()
    defer c.Unlock()
    fmt.Println("in foo")
}
重入锁

当一个线程获取锁时,如果没有其它线程拥有这个锁,那么,这个线程就成功获取到这个锁。之后,如果其它线程再请求这个锁,就会处于阻塞等待的状态。但是,如果拥有这把锁的线程再请求这把锁的话,不会阻塞,而是成功返回,所以叫可重入锁(有时候也叫做递归锁)。只要你拥有这把锁,你可以可着劲儿地调用,比如通过递归实现一些算法,调用者不会阻塞或者死锁。Mutex 不是可重入的锁因为 Mutex 的实现中没有记录哪个 goroutine 拥有这把锁。理论上,任何 goroutine 都可以随意地 Unlock 这把锁,所以没办法计算重入条件

func foo(l sync.Locker) {
    fmt.Println("in foo")
    l.Lock()
    bar(l)
    l.Unlock()
}
func bar(l sync.Locker) {
    l.Lock()
    fmt.Println("in bar")
    l.Unlock()
}
func main() {
    l := &sync.Mutex{}
    foo(l)
}
死锁

死锁是两个或两个以上的进程(或线程,goroutine)在执行过程中,因争夺共享资源而处于一种互相等待的状态,如果没有外部干涉,它们都将无法推进下去,此时,我们称系统处于死锁状态或系统产生了死锁。

WaitGroup#


// 线程安全的计数器
type Counter struct {
    mu    sync.Mutex
    count uint64
}
// 对计数值加一
func (c *Counter) Incr() {
    c.mu.Lock()
    c.count++
    c.mu.Unlock()
}
// 获取当前的计数值
func (c *Counter) Count() uint64 {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count
}
// sleep 1秒,然后计数值加1
func worker(c *Counter, wg *sync.WaitGroup) {
    defer wg.Done()
    time.Sleep(time.Second)
    c.Incr()
}
func main() {
    var counter Counter
    
    var wg sync.WaitGroup
    wg.Add(10) // WaitGroup的值设置为10
    for i := 0; i < 10; i++ { // 启动10个goroutine执行加1任务
        go worker(&counter, &wg)
    }
    // 检查点,等待goroutine都完成任务
    wg.Wait()
    // 输出当前计数器的值
    fmt.Println(counter.Count())
}
内部结构实现#
type WaitGroup struct {
    // 避免复制使用的一个技巧,可以告诉vet工具违反了复制使用的规则
    noCopy noCopy
    // 64bit(8bytes)的值分成两段,高32bit是计数值,低32bit是waiter的计数
    // 另外32bit是用作信号量的
    // 因为64bit值的原子操作需要64bit对齐,但是32bit编译器不支持,所以数组中的元素在不同的架构中不一样,具体处理看下面的方法
    // 总之,会找到对齐的那64bit作为state,其余的32bit做信号量
    state1 [3]uint32
}
// 得到state的地址和信号量的地址
func (wg *WaitGroup) state() (statep *uint64, semap *uint32) {
    if uintptr(unsafe.Pointer(&wg.state1))%8 == 0 {
        // 如果地址是64bit对齐的,数组前两个元素做state,后一个元素做信号量
        return (*uint64)(unsafe.Pointer(&wg.state1)), &wg.state1[2]
    } else {
        // 如果地址是32bit对齐的,数组后两个元素用来做state,它可以用来做64bit的原子操作,第一个元素32bit用来做信号量
        return (*uint64)(unsafe.Pointer(&wg.state1[1])), &wg.state1[0]
    }
}
WaitGroup 的方法实现#
Add/Done 方法
func (wg *WaitGroup) Add(delta int) {
    statep, semap := wg.state()
    // 高32bit是计数值v,所以把delta左移32,增加到计数上
    state := atomic.AddUint64(statep, uint64(delta)<<32)
    v := int32(state >> 32) // 当前计数值
    w := uint32(state) // waiter count
    if v > 0 || w == 0 {
        return
    }
    // 如果计数值v为0并且waiter的数量w不为0,那么state的值就是waiter的数量
    // 将waiter的数量设置为0,因为计数值v也是0,所以它们俩的组合*statep直接设置为0即可。此时需要并唤醒所有的waiter
    *statep = 0
    for ; w != 0; w-- {
        runtime_Semrelease(semap, false, 0)
    }
}
// Done方法实际就是计数器减1
func (wg *WaitGroup) Done() {
    wg.Add(-1)
}
Wait 方法
func (wg *WaitGroup) Wait() {
    statep, semap := wg.state()
    
    for {
        state := atomic.LoadUint64(statep)
        v := int32(state >> 32) // 当前计数值
        w := uint32(state) // waiter的数量
        if v == 0 {
            // 如果计数值为0, 调用这个方法的goroutine不必再等待,继续执行它后面的逻辑即可
            return
        }
        // 否则把waiter数量加1。期间可能有并发调用Wait的情况,所以最外层使用了一个for循环
        if atomic.CompareAndSwapUint64(statep, state, state+1) {
            // 阻塞休眠等待
            runtime_Semacquire(semap)
            // 被唤醒,不再阻塞,返回
            return
        }
    }
}
常见问题#
计数器设置为负值 / 或调用 Done 次数大于 Add
func main() {
    var wg sync.WaitGroup
    wg.Add(10)
    wg.Add(-10)//将-10作为参数调用Add,计数值被设置为0
    wg.Add(-1)//将-1作为参数调用Add,如果加上-1计数值就会变为负数。这是不对的,所以会触发panic
}
func main() {
    var wg sync.WaitGroup
    wg.Add(1)
    wg.Done()
    wg.Done()
}
不期望的 Add 时机

必须等所有的 Add 方法调用之后再调用 Wait,否则就可能导致 panic 或者不期望的结果。

func main() {
    var wg sync.WaitGroup
    go dosomething(100, &wg) // 启动第一个goroutine
    go dosomething(110, &wg) // 启动第二个goroutine
    go dosomething(120, &wg) // 启动第三个goroutine
    go dosomething(130, &wg) // 启动第四个goroutine
    wg.Wait() // 主goroutine等待完成
    fmt.Println("Done")
}
func dosomething(millisecs time.Duration, wg *sync.WaitGroup) {
    duration := millisecs * time.Millisecond
    time.Sleep(duration) // 故意sleep一段时间
    wg.Add(1)
    fmt.Println("后台执行, duration:", duration)
    wg.Done()
}

原本想的是,等四个 goroutine 都执行完毕后输出 Done 的信息,但是它的错误之处在于,将 WaitGroup.Add 方法的调用放在了子 gorotuine 中。等主 goorutine 调用 Wait 的时候,因为四个任务 goroutine 一开始都休眠,所以可能 WaitGroup 的 Add 方法还没有被调用,WaitGroup 的计数还是 0,所以它并没有等待四个子 goroutine 执行完毕才继续执行,而是立刻执行了下一步。导致这个错误的原因是,没有遵循先完成所有的 Add 之后才 Wait。

正确写法


func main() {
    var wg sync.WaitGroup
    wg.Add(4) // 预先设定WaitGroup的计数值
    go dosomething(100, &wg) // 启动第一个goroutine
    go dosomething(110, &wg) // 启动第二个goroutine
    go dosomething(120, &wg) // 启动第三个goroutine
    go dosomething(130, &wg) // 启动第四个goroutine
    wg.Wait() // 主goroutine等待
    fmt.Println("Done")
}
func dosomething(millisecs time.Duration, wg *sync.WaitGroup) {
    duration := millisecs * time.Millisecond
    time.Sleep(duration)
    fmt.Println("后台执行, duration:", duration)
    wg.Done()
}

前一个 Wait 还没结束就重用 WaitGroup

因为 WaitGroup 是可以重用的。只要 WaitGroup 的计数值恢复到零值的状态,那么它就可以被看作是新创建的 WaitGroup,被重复使用。

实现线程安全的 map#

自己使用读写锁#

使用读写锁自己实现一个 map

type RWMap struct { // 一个读写锁保护的线程安全的map
    sync.RWMutex // 读写锁保护下面的map字段
    m map[int]int
}
// 新建一个RWMap
func NewRWMap(n int) *RWMap {
    return &RWMap{
        m: make(map[int]int, n),
    }
}
func (m *RWMap) Get(k int) (int, bool) { //从map中读取一个值
    m.RLock()
    defer m.RUnlock()
    v, existed := m.m[k] // 在锁的保护下从map中读取
    return v, existed
}
func (m *RWMap) Set(k int, v int) { // 设置一个键值对
    m.Lock()              // 锁保护
    defer m.Unlock()
    m.m[k] = v
}
func (m *RWMap) Delete(k int) { //删除一个键
    m.Lock()                   // 锁保护
    defer m.Unlock()
    delete(m.m, k)
}
func (m *RWMap) Len() int { // map的长度
    m.RLock()   // 锁保护
    defer m.RUnlock()
    return len(m.m)
}
func (m *RWMap) Each(f func(k, v int) bool) { // 遍历map
    m.RLock()             //遍历期间一直持有读锁
    defer m.RUnlock()
    for k, v := range m.m {
        if !f(k, v) {
            return
        }
    }
}
使用第三方的分片锁 map#

github/orcaman/concurrent-map

特定场景的 map#
sync.Map

1. 只会增长的缓存系统中,一个 key 只写入一次而被读很多次;

2. 多个 goroutine 为不相交的键集读、写和重写键值对。

Store 方法

Store 方法,它是用来设置一个键值对,或者更新一个键值对的。

func (m *Map) Store(key, value interface{}) {
    read, _ := m.read.Load().(readOnly)
    // 如果read字段包含这个项,说明是更新,cas更新项目的值即可
    if e, ok := read.m[key]; ok && e.tryStore(&value) {
        return
    }
    // read中不存在,或者cas更新失败,就需要加锁访问dirty了
    m.mu.Lock()
    read, _ = m.read.Load().(readOnly)
    if e, ok := read.m[key]; ok { // 双检查,看看read是否已经存在了
        if e.unexpungeLocked() {
            // 此项目先前已经被删除了,通过将它的值设置为nil,标记为unexpunged
            m.dirty[key] = e
        }
        e.storeLocked(&value) // 更新
    } else if e, ok := m.dirty[key]; ok { // 如果dirty中有此项
        e.storeLocked(&value) // 直接更新
    } else { // 否则就是一个新的key
        if !read.amended { //如果dirty为nil
            // 需要创建dirty对象,并且标记read的amended为true,
            // 说明有元素它不包含而dirty包含
            m.dirtyLocked()
            m.read.Store(readOnly{m: read.m, amended: true})
        }
        m.dirty[key] = newEntry(value) //将新值增加到dirty对象中
    }
    m.mu.Unlock()
}

可以看出,Store 既可以是新增元素,也可以是更新元素。如果运气好的话,更新的是已存在的未被删除的元素,直接更新即可,不会用到锁。如果运气不好,需要更新(重用)删除的对象、更新还未提升的 dirty 中的对象,或者新增加元素的时候就会使用到了锁,这个时候,性能就会下降。

所以从这一点来看,sync.Map 适合那些只会增长的缓存系统,可以进行更新,但是不要删除,并且不要频繁地增加新元素。

新加的元素需要放入到 dirty 中,如果 dirty 为 nil,那么需要从 read 字段中复制出来一个 dirty 对象:

func (m *Map) dirtyLocked() {
    if m.dirty != nil { // 如果dirty字段已经存在,不需要创建了
        return
    }
    read, _ := m.read.Load().(readOnly) // 获取read字段
    m.dirty = make(map[interface{}]*entry, len(read.m))
    for k, e := range read.m { // 遍历read字段
        if !e.tryExpungeLocked() { // 把非punged的键值对复制到dirty中
            m.dirty[k] = e
        }
    }
}

Load 方法

Load 方法用来读取一个 key 对应的值。它也是从 read 开始处理,一开始并不需要锁。


func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
    // 首先从read处理
    read, _ := m.read.Load().(readOnly)
    e, ok := read.m[key]
    if !ok && read.amended { // 如果不存在并且dirty不为nil(有新的元素)
        m.mu.Lock()
        // 双检查,看看read中现在是否存在此key
        read, _ = m.read.Load().(readOnly)
        e, ok = read.m[key]
        if !ok && read.amended {//依然不存在,并且dirty不为nil
            e, ok = m.dirty[key]// 从dirty中读取
            // 不管dirty中存不存在,miss数都加1
            m.missLocked()
        }
        m.mu.Unlock()
    }
    if !ok {
        return nil, false
    }
    return e.load() //返回读取的对象,e既可能是从read中获得的,也可能是从dirty中获得的
}

如果幸运的话,我们从 read 中读取到了这个 key 对应的值,那么就不需要加锁了,性能会非常好。但是,如果请求的 key 不存在或者是新加的,就需要加锁从 dirty 中读取。所以,读取不存在的 key 会因为加锁而导致性能下降,读取还没有提升的新值的情况下也会因为加锁性能下降。

其中,missLocked 增加 miss 的时候,如果 miss 数等于 dirty 长度,会将 dirty 提升为 read,并将 dirty 置空。

func (m *Map) missLocked() {
    m.misses++ // misses计数加一
    if m.misses < len(m.dirty) { // 如果没达到阈值(dirty字段的长度),返回
        return
    }
    m.read.Store(readOnly{m: m.dirty}) //把dirty字段的内存提升为read字段
    m.dirty = nil // 清空dirty
    m.misses = 0  // misses数重置为0
}

Delete 方法

sync.map 的第 3 个核心方法是 Delete 方法。 Delete 方法的核心改在了对 LoadAndDelete 中实现了。Delete 方法是先从 read 操作开始

func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {
    read, _ := m.read.Load().(readOnly)
    e, ok := read.m[key]
    if !ok && read.amended {
        m.mu.Lock()
        // 双检查
        read, _ = m.read.Load().(readOnly)
        e, ok = read.m[key]
        if !ok && read.amended {
            e, ok = m.dirty[key]
            // 这一行长坤在1.15中实现的时候忘记加上了,导致在特殊的场景下有些key总是没有被回收
            delete(m.dirty, key)
            // miss数加1
            m.missLocked()
        }
        m.mu.Unlock()
    }
    if ok {
        return e.delete()
    }
    return nil, false
}
func (m *Map) Delete(key interface{}) {
    m.LoadAndDelete(key)
}
func (e *entry) delete() (value interface{}, ok bool) {
    for {
        p := atomic.LoadPointer(&e.p)
        if p == nil || p == expunged {
            return nil, false
        }
        if atomic.CompareAndSwapPointer(&e.p, p, nil) {
            return *(*interface{})(p), true
        }
    }
}

ync.map 还有一些 LoadAndDelete、LoadOrStore、Range 等辅助方法,但是没有 Len 这样查询 sync.Map 的包含项目数量的方法,并且官方也不准备提供。如果你想得到 sync.Map 的项目数量的话,你可能不得不通过 Range 逐个计数。

总结

Go 内置的 map 类型使用起来很方便,但是它有一个非常致命的缺陷,那就是它存在着并发问题,所以如果有多个 goroutine 同时并发访问这个 map,就会导致程序崩溃。所以 Go 官方 Blog 很早就提供了一种加锁的方法,还有后来提供了适用特定场景的线程安全的 sync.Map,还有第三方实现的分片式的 map,这些方法都可以应用于并发访问的场景。

Go 精进之路#

第二章 项目结构,代码风格与标识符命名#

Go 项目结构#

直接参考 Kratos 的目录结构

image-20230308004830439

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。