Golang学习总结

一、基本语法

Golang 设计理念:一种事情有且只有一种方法完成

软件包安装

直接官网下载好,配置安装下环境变量即可

bash 复制代码
/etc/profile        # 在这个文件写入会对所有用户生效
~/.profile          # 在这个文件写入只会对当前用户生效

# 以上两个文件中任意选择一个
# 将 Go 的安装目录记录下,比如安装目录为 /home/CHAO/Document/go/bin
export GOROOT=/usr/local/go                 #这个表示go的安装目录
export PATH=$PATH:$GOROOT/bin               #声明环境变量
export GOPATH=/home/CHAO/Golang             #这个表示go的工作目录

既可以使用 go env 这个命令来设置 Go 的环境变量,也可以直接使用 Linux 的 export 命令来设置 go 的环境变量,因为 go 既会读自己的设置的环境变量,也会读系统级别的环境变量,所以都能识别到

go env -w 实际上是把配置写入到了配置文件 env,这个 env 文件的位置可以通过 os.UserConfigDir() 来查看

附:介绍几个 Linux 下的与环境变量相关的配置文件,实际的PATH环境变量在 /etc/environment

/etc/bash.bashrc 在 bash shell 打开时运行,修改该文件配置的环境变量将会影响所有用户使用的bash shell

/etc/profile 在系统启动后第一个用户登录时运行,并从 /etc/profile.d 目录的配置文件中搜集 shell 的设置,使用该文件配置的环境变量将应用于登录到系统的每一个用户

/etc/bash_completion.d 这个目录通常是存放 shell 自动补全命令的脚本,每打开一个终端就会执行一次这个目录下的所有文件

~/.bashrc 当用户登录时以及每次打开新的 shell 时该文件都将被读取,不推荐在这里配置用户专用的环境变量,因为每开一个shell,该文件都会被读取一次,效率肯定受影响。

~/.profile 当用户登录时执行,每个用户都可以使用该文件来配置专属于自己使用的 shell 信息

在这几个文件末尾输出文件名,可以发现初次登录机器打开 shell 执行次序如下

而重新打开一个新的终端显示如下,这两个文件每次打开新的终端都会被读取一次:

所以之后用户配置文件建议写在 ~/.profile 下,全局配置文件建议写在 /etc/profile

基础语法

声明变量的三种方式

go 复制代码
var foo int             //方式一
foo = 10

var foo int = 20        //方式二

foo := 30               //方式三

Golang 在函数体外不能有赋值语句

go 复制代码
var age int = 20        //正确

name := "Tom"       
//错误,这一句实际上有两句,var name string;  name = "Tom";  第二句为赋值语句

Go 语言禁止对常量取地址的操作;编译器不允许在类型变量之间进行隐式类型转换

Go中的 byte 和 rune 其实就是 uint8 和 int32 的类型别名,源代码在 builtin.go

go 复制代码
type byte = uint8
type rune = int32

//注意:
type rune = int32        //这是给类型起别名
type rune int32          //这是基于已有类型创建新类型

常量没法取指针是合理的,如果常量能取到指针,那就意味着可以修改,也就不能叫常量了

go 复制代码
const (
    str = "hello world"    // 这是常量,不能取地址
)

func main() {
    fmt.Println(pointer.String("hello wolrd"))    // 这是临时变量,可以取地址
}

Golang 中的返回值为指定值

go 复制代码
func f() (result int) {
    result = 3
    return 4
}

如果指定为 return 的返回值那么函数返回值就是其 return 的值
否则就是在函数中得到的 result 的值
浮点类型

Go 的浮点类型有固定的范围和字段长度,不受OS的影响

Go 的浮点类型默认声明为 float64 类型,通常情况下应该使用 float64,因为它比 float32 位更加精确

字符类型

Go 中没有专门的字符类型,如果要存储单个字符(字母),一般使用 byte 来保存;Go 的字符串是由 byte 组成的

Go 语言中的字符使用 UTF-8 编码 (其中英文字母占1个字节,汉字占用3个字节)

字符串就是由一串固定长度的字符连接起来的字符序列;Go 的字符串是由单个字节连接起来的,也就是说对于传统的字符串是由字符组成的,而 Go 中字符串却是由字节组成的

Golang 和 Java/C 不同,Go 在不同类型变量之间赋值时需要显示转换,也就是说 Go 中的数据类型不能自动转换

Go 中的字符串是不可变的,一旦初始化后便不能再重新赋值

字符串的两种表示形式:

1.双引号"":会识别出转义字符

2.反引号````````:以字符串的原生形式输出,包括换行和特殊字符,可以实现防止攻击,输出源代码等效果

字符串和数值型互相转化

数值转化成字符型利用 fmt 包和 strconv 包

go 复制代码
var num1 int = 99
var num2 float64 = 23.32
var b bool = true

1.方法一:使用 Sprintf, 字符串格式化输出
str1, str2, str3 := fmt.Sprintf("%d", num1), fmt.Sprintf("%f", num2), fmt.Sprintf("%t", b)

2.方法二:使用 FormatInt
str1 := strconv.FormatInt(int64(num1), 10)

// 'f'表示格式,10表示小数位保留10位,64代表这个小数是float64
str2 := strconv.FormatFloat(num2, 'f', 10, 64)       

str3 := strconv.FormatBool(b)

string 类型转换成基本数据类型

go 复制代码
var str string  = "true"
var b bool
b, _ = strconv.ParseBool(str)

var str2 string = "123456"
var n1 int64
n1, _ = strconv.ParseInt(str2, 10, 64)        //10代表十进制,64代表转换成int64

常见的值类型和引用类型

值类型:基本数据类型 int系列、float系列、bool、string、数组以及结构体等,通常存放在栈区

引用类型:指针、slice切片、map、管道chan、interface等,通常存放在堆区

算法运算符

++ 和 -- 只能独立使用,Go中只有后置++和--,没有前置++

go 复制代码
var i int = 3
var n int = i++                //这是错误的
i++
var n int =  i                //这是正确的
分支循环

Go 支持在 if 中,直接定义一个变量,比如下面

go 复制代码
if age := 18; age > 20 {
    fmt.Println("OK")
} else {
    fmt.Println("NO")
}

golang 使用简短方式声明变量,左侧必须要有一个新变量,变量也可以重复声明。

go 复制代码
func main() {
    test1 := 0
    test1, test2 := 1, 2        

    test1:= 3         //错误,因为左侧没有一个新变量
    test1 = 3         //正确
}
函数和包

Go 中的包:包的实质就是创建不同的文件夹,来存放程序文件

包的三大作用:

  • 区分相同名字的函数,变量和标识符
  • 当程序文件很多时,可以很好的管理项目
  • 控制函数,变量等访问范围,即作用域

打包基本语法:package 包名 引入一个包:import "包的路径"

Go 支持可变参数

go 复制代码
func sum(args ... int) int {
    ...
}

如果一个函数的形参列表中有可变参数,则可变参数需要放在形参列表后面

每一个源文件都可以包含一个 init 函数,该函数会在 main 函数之前执行,被 Go 运行框架调用,也就是说 init 会在 main 函数前被调用

通常可以在 init 函数中完成初始化工作

闭包就是一个函数和与其相关的引用环境组合的一个整体(实体)

defer语句

在函数中,程序员经常需要创建资源(比如:数据库了解、文件句柄、锁等),为了在函数执行完毕后及时的释放资源,Go 的设计者提供了defer(延时机制)

defer 和 recover 就可以对应理解为C++里面的 throw exception 和 catch

判断以下程序输出

go 复制代码
func f() {
    g()
    fmt.Println("A")
}

func g() {
    defer func() {
        if r := recover(); r != nil {  //捕捉到panic,则函数返回后f正常执行输出"A"
            fmt.Println("Recovered in f", r)
        }
        fmt.Println("Hi")  //有没有recover这个都会输出
    }()

    nums := []int{10,20,30}
    nums[3] = 40                //此处会panic,余下的部分不会执行
    fmt.Println("hello world")  //panic会导致程序被中止,但是在退出前,会先处理完当前协程上已经defer的任务,执行完成后再退出
}

func main() {
    f()
    fmt.Println("B")
}

输出:
Recovered in f ...
Hi
A
B

注意:panic发生后panic余下的部分都不会执行,只会执行defer函数,recover捕捉到panic后等
defer函数执行完毕返回再顺序执行其他函数
  1. recover 只有在 defer 语句中调用才会生效
  2. panic 只会对当前的 Goroutine 的 defer 有效,下面这种情况 panic 是不会被 recover 的
go 复制代码
func main() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err)
        }
    }()
    go func() {
        panic("nil pointer exception")
    }()

    time.Sleep(time.Second)
}

注:不是所有的 panic 都能被recover捕获到,像内存溢出、map并发读写、栈内存耗尽这些 panic 就不能被recover捕获到

参数类型

值类型:基本数据类型 int 系列,float系列,bool,string,数组和结构体 struct

引用类型:指针、slice切片、map、管道 chan、interface 等都是引用类型

Golang定义结构体指针切片

go 复制代码
type Tag struct {
    Key   string `json:"key"`
    Value string `json:"value"`
}
p := []*Tag{
    {"hi", "ih"},    //不需要加 &Tag{"hi","ih"}
    {"wo", "ow"},
    {"rld", "dlr"},
}
字符串
go 复制代码
len(str)                                //1.统计字符串的长度(字节数)

n, err := strconv.Atoi("12")             //2.字符串转数字

str := strconv.Itoa(12345)               //3.数字转字符串

var bytes = []byte("hello go")           //4.字符串转byte

str := string([]byte{97,98,99})          //5.[]byte转字符串

str := strconv.FormatInt(123, 2)        //6.十进制转化成对应的二进制,八进制,十六进制,返回的是一个字符串

b := strings.Contains("seafood", "foo")   //7.查找子串是否在指定的字符串中

num := strings.Count("chesses", "e")        //8.判断字符串有几个指定的子串

b := strings.EqualFold("abc", "ABC")        //9.不区分大小写的字符串比较

n := strings.Index("NIL_abc", "abc")     //10.返回子串在字符串中出现的第一次位置,找不到返回-1

index := strings.LastIndex("go golang", "go")    //11.返回子串在字符串出现的最后一次位置

str := strings.Replace("go go hello", "go", "C++", -1)  //12.字符串替换,-1表示全部替换

strings.Split("hello,world,ok", ",")       //13.字符串拆分

strings.ToLower("Go"), ToUpper("Go")      //14.字符串大小写转换

strings.TrimSpace(" hi, world  ")          //15.去除字符串两边的空格

strings.Trim("! hello ! ", "! ")        //16.去除左右两边指定的字符串

strings.TrimLeft("! hello!", "!")        //17.去掉左边的指定字符串

 //18.判断是否以指定的字符串开头或者结尾
strings.HasPrefix("ftp://192.168...", "ftp")   HasSuffix("Hi.jpg", "jpg")       

字符串拼接的四种方法

go 复制代码
最推荐使用
func builderContact(n int, str string) string {
    var builder strings.Builder
    for i := 0; i < n; i++ {
        builder.WriteString(str)
    }
    return builder.String()
}

或者 
func ConcatStrings(ss ...string) string {
    var bff bytes.Buffer
    for _, s := range ss {
        bff.WriteString(s)
    }
    return bff.String()
}


//第二种
func plusConcat(n int, str string) string {
    s := ""
    for i := 0; i < n; i++ {
        s += str
    }

    return s
}

//第三种
func SprintfConcat(n int, str string) string {
    s := ""
    for i := 0; i < n; i++ {
        s = fmt.Sprintf("%s%s", s, str)
    }
    return s
}

//第四种
func preByteConcat(n int, str string) string {
    buf := make([]byte, 0, n*len(str))
    for i := 0; i < n; i++ {
        buf = append(buf, str...)
    }
    return string(buf)
}
错误处理

Go 追求简洁,所以不支持 try...catch...finally 这种处理

Go 中引入的处理方式是:defer panic recover

这几个异常的使用场景可以这么简单描述:Go 中可以抛出一个 panic 异常,然后在 defer 语句中通过 recover 捕获这个异常,然后正常处理

数组与切片

初始化数组的几种方式

go 复制代码
var intArr [3]int = [3]int{10, 20, 30}                //1.方法一

var intArr = [3]int{10, 20, 30}                       //2.方法二

var intArr = [...]int{10, 20, 30}                     //3.方法三

var intArr = [...]int{1:80, 0:90, 2:99}               //4.方法四

// 前面的 var intArr 全部可以替换成 intArr := 

切片是数组的一个引用,因此切片是引用类型,在进行传递时,遵守引用的机制

切片的长度是可以变化的,因此切片是一个可以动态变化的数组

slice 是一个引用类型,从底层来说是一个数据结构(struct 结构体)

go 复制代码
type slice struct {
    array unsafe.Pointer // 被引用的数组中的起始元素地址,之前版本是 ptr *[]T
    len int
    cap int
}

slice 的使用,slice也可以称为动态数组 源码在 ($GOROOT/src/runtime/slice.go)

1.使用数组创建一个切片

2.使用 make 来创建切片

3.定义切片并直接指定数组

go 复制代码
1.使用数组初始化
var intArr [5]int = [...]int{10, 20, 30, 40, 50}
slice := intArr[1:3]

2.使用 make 创建切片,但是make底层也会创建一个数组,由切片在底层创建一个数组
var slice []int = make([]int, 4, 10)        //4表示切片大小,10表示容量

3.定义切片直接指定数组
var strSlice []string = []string{"Tom", "Jack", "Mary"}  
//注意与数组的区别,数组后面的"[]"是要有数值的,而切片的"[]"要求是空的

4.对切片添加元素
slice = append(slice, 99)
//切片的扩容机制,append的时候,如果长度增加后超过容量,则将容量增加两倍

切片定义后,还不能使用,因为本身是一个空的,需要让其引用到一个数组,或者 make 一个空间供切片使用

slice 可以看成一个 struct 结构体,其中有数据成员存储指向的元素,存储数组大小以及容量

切片操作并不复制切片指向的元素,创建一个新的切片会复用原来切片的底层数组,因此切片操作是非常高效的。下面的例子展示了这个过程

最终 nums 为 [1,2,3,4,50]

string 底层是一个 byte 数组,因此 string 也可以进行切片处理;string 是不可变的,也就是不能通过 str[0] = 'z' 方式来修改字符串

如果需要修改字符串,需要先将 string -> []byte 或者 []rune,修改后再重新转化成字符串

byte 就是 uint8 类型,rune 就是 uint32 类型

go 复制代码
string 底层是一个byte数组,因此string也可以进行切片处理
str := "hello,world"
slice := str[6:]

对字符串进行间接修改
str := "hello"

如果字符含有中文则转化成byte会溢出,此时转化成rune即可, arr := []rune(str)
arr := []byte(str)
arr[0] = 'W'
str = string(arr)       //将byte[]转化成string
例1:基于数组创建切片
arr := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
fmt.Println(arr[:3:5][:4]) // [1 2 3 4]
fmt.Println(arr[:3:5][:8]) // panic: runtime error: slice bounds out of rang

arr[:3:5] 基于 arr 创建一个 slice, len 是 3, cap 是 5; 然后再在这个 slice 的基础上分别创建一个 len = 4len = 8 的 slice.

前者运行正常, 后者因超出 cap = 5 范围而 panic, 尽管后者实际想要的内存并没有超出 arr 数组范围

例2:go语言中切片当作函数传递
go 复制代码
func Change(s []int){
    s[0]=11
    s[1]=22
}
func main(){
    slice:=[]int{1,2,3,4,5}
    Change(slice)
    fmt.Println(slice)        //打印结果为 {11,22,3,4,5}
}

----
func Add(s []int){
    s = append(s,6,7,8)
}
func main(){
    slice:=[]int{1,2,3,4,5}
    Add(slice)
    fmt.Println(slice)        //结果不变,照样为 {1,2,3,4,5}
}

因为 append 扩容使得地址发生了变化,所以不是指向原来的切片也就导致了并不是在原来的切片上面增加了元素

Add函数里面用了append( )函数,在调用函数时,在栈区里面把1 2 3 添加到 a 里面然后重新分配了地址,而原来的 s 切片还是指向原来地址,根本没有变,所以在 main 函数里面打印出 s 还是原来的,不会改变

解决方案如下,将修改后的地址传递回去即可

go 复制代码
func Add(s []int) (b []int) {
    b = append(s, 6,7,8)
    return 
}

func main() {
    slice := []int{1,2,3,4,5}
    slice = Add(slice)
    fmt.Println(slice)                //结果为 {1,2,3,4,5,6,7,8}
}
例3:对二维数据排序
go 复制代码
//将二维数据按照首元素重排
nums := [][]int{{2, 3}, {1, 4}, {8, 9}, {7, 10}}

//方法一
sort.Slice(nums, func(i, j int) bool {
    return nums[i][0] < nums[j][0]
})

//方法二:实现sort.Interface接口,只要实现了这个接口,就可以调用sort包中的函数进行排序
sort.Sort(ints(nums))

type ints [][]int

func (s ints) Len() int           { return len(s) }
func (s ints) Less(i, j int) bool { return s[i][0] < s[j][0] }
func (s ints) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
例4: 不要直接将一个slice复制给另外一个slice

比如我现在需要声明两个2x2的二维切片

go 复制代码
nums1 := make([][]int, 2)
for i := 0; i < 2; i++ {
    nums1[i] = make([]int, 2)
}
nums2 := nums1
// var nums2 [][]int    或者这种写法,这样这个nums2切片会复用nums1切片的底层数组
// nums2 = nums1
nums1[0][0] = 10
fmt.Println(nums2)  千万注意这里输出的是[[10, 0],[0,0]]

哈希

区分是否内外排序的依据就是判断数据是否全部加载进内存中,如果全部加载进内存中就是内排序

map 在使用之前一定要 make , map 的 key 是不能重复的,如果重复了,就以最新的 key-value 为准

map 的 key-value 是无序的,make 可用来给 map 申请空间(make无论是用在切片还是map中都是用来申请内存空间的)

map 的三种声明方式

go 复制代码
//1.第一种方式
var mp map[string]string
mp = make(map[string]string, 10)

//2.第二种方式
mp := make(map[string]string)    //后面这个容量可以不预先指定

//3.第三种方式
mp := map[string]string {
    "one" : "CPP",
    "two" : "Java",
    "three": "Python",
}

//删除 map 中的某个元素
delete(mp, "1")

Go 中没有一个专门的方法针对 map 中的 key 进行排序

Go 中的 map 默认是无序的,注意也不是按照添加的顺序存放的,你每次遍历可能得到的结果都不一样

Go 中的 map 排序先是将 key 进行排序,之后根据 key 值遍历输出即可

golang 对 map 排序输出

go 复制代码
// 1.先将mp的key放入到切片中
// 2.对切片排序
// 3.遍历切片,然后按照key来输出map的值
mp := make(map[int]int, 10)
mp[1] = 100
mp[3] = 23
mp[2] = 383

var keys []int
for k, _ := range mp {
    keys = append(keys, k)
}

sort.Ints(keys)
fmt.Println(keys)

for _, k := range keys {
    fmt.Printf("map[%v] = %v\n", k, mp[k])
}

Go中的map不支持并发的读写,只支持并发的读,原因是保证大多数场景下的查询效率

下面这种对map的并发读写是错误的

go 复制代码
aa := make(map[int]int)

go func() {
    for {
        aa[0] = 5
    }
}()

go func() {
    for {
        _ = aa[1]
    }
}()

time.Sleep(time.Second)

Go 不支持泛型,比如 C++ 里面的 max 和 min 方法,Go 中只有比较 float64 的 Min 和 Max 方法

go 复制代码
math.Min(float64, float64) float64
math.Max(float64, float64) float64

另外为了保持 Go 简洁干净,Go 不支持泛型,所以既然有了对比 float64 数据类型的 max/min 方法,在同一个 math 包中就无法支持同名的但是参数不同的方法,也就是下面的方法无法存在同一个包中

go 复制代码
math.Max(float64, float64) float64
math.Max(int64, int64) int64

利用空接口/结构体 + map 来实现集合 set

go 复制代码
type empty struct{}  
type t interface{}    //这里key用的是空接口类型,所以任何元素都可以放进这个集合
type set map[t]empty

func (s set) has(item t) bool {
    _, exists := s[item]
    return exists
}

func (s set) insert(item t) {
    s[item] = empty{}
}

func (s set) delete(item t) {
    delete(s, item)
}

func (s set) len() int {
    return len(s)
}

踩坑:

go 复制代码
type Student struct {
    Name   string `json:"name"`
    Age    int    `json:"age"`
    IsBool bool   `json:"is_boy,omitempty"`
    Data   map[string]string
}

stu := Student{
    Name:   "zhu",
    Age:    18,
    IsBool: false,
}

stu.Data["hello"] = "world"  //报错,这里map没有初始化,实际上 stu.Data 为 nil

stu初始化时改成下面这种即可
stu := Student{
    Name:   "zhu",
    Age:    18,
    IsBool: false,
    Data:   map[string]string{},
}

面向对象编程

  • Go 也支持面向对象编程 (OOP),但是和传统的面向对象编程还是有区别,并不是纯粹的面向对象语言;所以我们说 Golang 支持面向对象编程特性是比较准确的
  • Golang 没有类 (class),Go 语言的结构体 (struct) 和其他编程语言的类有同等地位
  • Golang 面向对象编程非常简洁,去掉了传统 OOP 语言的继承、方法重载、构造函数和析构函数、隐藏的 this 指针等
  • Golang 仍然有面向对象编程的继承,封装和多态特性,只是实现的方式和其他OOP不一样,比如继承是通过匿名字段来实现的
  • Golang 面向编程很优雅,OOP 本身就是语言类型系统 (type system) 的一部分,通过 interface 接口,耦合性低,也非常灵活

使用 slice 和 map 一定要先初始化后才能使用

go 复制代码
nums := []int{}  或者 nums := make([]int, 10)
mp := map[int]string{}  或者 mp := make(map[int]string, 10)

结构体是用户单独定义的类型,和其他类型进行转换时需要有完全相同的字段 (名字,个数和类型)

结构体进行 type 重新定义 (相当于取别名),golang 认为是新的数据类型,但是相互间可以强转

struct 的每个字段上,可以写上一个 tag,这个 tag 可以通过反射机制获取,常见的使用场景就是序列化和反序列化

golang 多重继承

如果一个 struct 嵌套了多个匿名结构体,那么该结构体可以直接访问嵌套的匿名结构体的字段和方法,从而实现了多重继承

但是为了保证代码的简洁性,建议大家尽量不要使用多重继承

接口(interface),其本质是一个指针

interface 类型可以定义一组方法,但是这些不需要实现;并且 interface 不能包含任何变量,到某个自定义类型(比如 struct)要使用的时候,再根据具体情况把这些方法写出来

  • 接口里面的方法都没有方法体,即接口的方法都是没有实现的方法;接口体现了程序设计的多态和高内聚低耦合的思想
  • Golang 的接口,不需要显示的定义;只要一个变量,含有接口类型的所有方法,那么这个变量就实现这个接口,因此 golang 中没有 implement 这样的关键字
  • 在 Go 中,一个自定义类型需要将某个接口的所有方法都实现,我们说这个自定义类型实现了这个接口
  • 一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋值给接口类型
  • 只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型
  • 一个自定义类型又可有多个接口
  • Golang 接口中不能有任何变量
  • 一个接口(比如 A 接口),可以继承多个别的接口(比如 B,C 接口),这时如果要实现A接口,也必须将 B,C 接口的方法全部实现,这也称之为接口嵌套;但是接口不能递归嵌套

如在 io package 中

go 复制代码
type ReadWriter interface {
    Reader    //Reader和Writer都是接口类型
    Writer    
}

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

// 所以上面的 ReadWriter 接口就等价于
type ReadWriter interface {
    Read(p []byte) (n int, err error)
    Write(p []byte) (n int, err error)
}

接口的类型断言

go 复制代码
//interface 是万能数据类型
//类型断言机制
func myFunc(arg interface{}) {
    value, ok = arg.(string)        //判断arg的类型
}
结构体内嵌结构体的方法调用

需要注意看清内嵌的结构体是否是一个单独的成员变量

在 Gin 框架中,Engine 结构体内嵌一个RouterGroup结构体

go 复制代码
type Engine struct {
    RouterGroup           写法1:这样写的话Engine结构体可以直接调用RouterGroup结构体实现的方法(写法1兼顾写法2)
    router RouterGroup    写法2:这样写的话必须要使用Engine.router来调用RouterGroup实现的方法
    ...    
}

type RouterGroup struct {
    ...
}

func (group RouterGroup) GET(relativePath string) string {
    return relativePath
}

func main() {
    r := Engine{}
    fmt.Println(r.GET("/user"))    //Engine结构体可以直接调用RouterGroup实现的方法
    fmt.Println(r.RouterGroup.GET("/user")) //也可以像写法2那样使用结构体成员来调用
}

结构体内嵌接口的方法调用

go 复制代码
type People struct {
    Stu            //内嵌一个接口
    Name string
}

type Stu interface {
    Print()
}

type Fresh struct {
}

func (s Fresh) Print() {
    fmt.Println("hello world")
}

func main(){
    Jack := Fresh{}    
    peo := People{Stu: Jack, Name: "jack"}    //只要结构体实现了这个接口,就可以赋值
    peo.Print()       
    peo.Stu.Print()     //这两种方法都可以调用接口中定义的方法
}

只要结构体中任意一个成员对象实现了某个接口,就可以将这个结构体赋值给这个接口

go 复制代码
type Animal struct {
    Cat
    Dog
}

type Cat struct {
    Name string
    Age  int
}

type Dog struct {
}

func (dog Dog) Speak() {    
    fmt.Println("Hi, I'm a Dog")
}

type Ani interface {  //Dog成员实现了Ani这个接口,因此可以将Animal结构体赋值给Ani接口
    Speak()
}

func main() {
    ani := Animal{}
    var aniInterface Ani
    aniInterface = ani
    aniInterface.Speak()
}

Go 协程(Goroutine)和 channel

GMP : goroutine 协程 P :processor 处理器 M : thread 线程

Go 协程介绍:

  • 主线程是一个物理线程,直接作用在 CPU 上,是重量级的,非常耗费 CPU 资源
  • 协程是从主线程开启的,是轻量级的线程,是逻辑态,对资源消耗相对小
  • Go 的协程机制是重要的特点,可以轻松的开启上万个协程;其他编程语言的并发机制一般是基于线程的,开启过多的线程,资源耗费非常大

空的 channel 参看这篇文章

调度器的设计策略

1.复用线程:work stealing 机制 hand off 机制

2.利用并行

3.抢占

4.全局 G 队列

channel 用于给 Go 协程通信

go 复制代码
make(chan Type)                
make(chan Type, capacity)      // 这两种写法等价

channel <- value        // 发送value到channel中
<- channel              // 接收并将其丢弃
x := <- channel         // 从channel中接收数据,并赋值给x
x, ok := <- channel     
// 功能同上,并检查 channel 是否关闭, ok为true表示channel没有关闭,为false表示channel关闭

channel 用于各个 goroutine 之间进行通信

  • channel 本质是一个队列,数据是先进先出
  • 线程安全,多个 goroutine 访问时,不需要加锁,就是说 channel 本身就是线程安全的
  • channel 是有类型的,一个 string 的 channel 只能存放 string 类型数据
  • channel 是引用类型,必须初始化后才能写入数据,即 make 后才能使用,给管道写入数据时,不能超过其容量

channel 特点:当 channel 满时,再向里面写数据,就会阻塞;为空时,从里面取数据也会阻塞

channel 不像文件那样经常需要关闭,只有当你确实没有发送任何数据时,或者你想显示结束 range 循环之类的,采取关闭 channel

关闭 channel 后,无法向 channel 再写入数据(引发 panic 错误后导致接收立即返回零值)

关闭 channel 后,可以继续从 channel 接收数据

对于 nil channel , 无论收发都会被阻塞

go 复制代码
var chanInt chan<- int                  // 声明为只读
chanInt := make(chan int, 3)

var chanInt <- chan int                 // 声明为只写

使用 select 来解决从管道取数据的阻塞问题

有缓冲和无缓冲的 channel

单流程下一个 Go 协程只能监控一个 channel 的状态,select 可以完成监控多个 channel 的状态

Q:select 是随机的还是顺序的?

  • select 语句不使用 default 分支时,处于阻塞状态直到其中一个 channel 的收/发操作准备就绪(或者channel关闭或者缓冲区有值),如果同时有多个 channel 的收/发操作准备就绪(或者 channel 关闭)则随机选择其中一个
  • select 语句使用 default 分支时,处于非阻塞状态,从所有准备就绪(或者 channel 关闭或者缓冲区有值)的channel 中随机选择其中一个,如果没有则执行 default 分支

无缓冲的 channel 是指在接收前没有能力保存任何值的通道

这种类型的通道要求发送 goroutine 和接收 goroutine 同时准备好,才能完成发送和接收操作. 如果两个 goroutine 都没有同时准备好,通道会导致先执行发送或接收操作的 goroutine 阻塞等待

无缓冲的 channel 一次只能传输一个数据

  • 如果只有读端,没有写端;那么读端阻塞
  • 如果只有写端,没有读端;那么写端阻塞

有缓冲的 channel 直到缓冲区被填满后写端才阻塞,缓冲区被读空后读端才会阻塞

go 复制代码
ch := make(chan int)        // 无缓冲通道

通过 channel 实现同步和数据交互

go 复制代码
func person1(ch chan int) {
    fmt.Println("hello")
    ch <- 666 //给管道写入数据
}

func person2(ch chan int) {
    <-ch //从管道读取数据,如果管道为空便会阻塞
    fmt.Println("world")
}

func main() {
    ch := make(chan int)
    go person1(ch)
    go person2(ch)

    for {
    }
}

channel 支持 for range 的方式进行遍历,需要注意两个细节

a. 在遍历时,如果 channel 没有关闭,则会出现 deadlock 的错误

b. 在遍历时,如果 channel 已经关闭,则会正常遍历数据,遍历完后,就会退出遍历

go 复制代码
func main() {
   // 遍历管道
   intChan2 := make(chan int, 10)
   for i := 0; i < 10; i++ {
      intChan2 <- i * 2     // 放入100个数据到管道
   }
   // 关闭channel就表示不能再向里面写入数据了,但是照样可以读取channel剩余的数据
   close(intChan2)          // 这个不能放在for range后面,放在后面就阻塞死锁了
   for v := range intChan2 {
      fmt.Println("v=", v)
   }
}
例1:一个协程唤醒所有阻塞的协程

三个协程调用 wait() 等待,另一个协程调用 Broadcast 唤醒所有等待的协程

go 复制代码
var done = false

func read(name string, c *sync.Cond) {
    c.L.Lock()
    for !done {
        c.Wait()
    }
    log.Println(name, "starts reading")
    c.L.Unlock()
}

func write(name string, c *sync.Cond) {
    log.Println(name, "starts writing")

    time.Sleep(time.Second)      //模拟耗时,并等待前面的三个read协程都执行到 Wait()
    c.L.Lock()
    done = true
    c.L.Unlock()

    log.Println(name, "wakes all")
    c.Broadcast()
}

func main() {
    cond := sync.NewCond(&sync.Mutex{})

    go read("read1", cond)
    go read("read2", cond)
    go read("read3", cond)

    write("writer", cond)

    time.Sleep(time.Second * 3)
}

done 即互斥锁需要保护的条件变量

read()调用wait()等待通知,直到 done 为 true

write()接受数据,接收完成后,将 done 置为 true,调用broadcast()通知所有等待的线程

例2:Gosched 函数让出时间片让子协程先执行
go 复制代码
go func() {
    for i := 0; i < 5; i++ {
        fmt.Println("go")
    }
}()

for i := 0; i < 2; i++ {
    runtime.Gosched()
    fmt.Println("hello")
}

Goexit 函数将立即终止当前的 goroutine 执行, 调度器确保所有已注册 defer 延迟调用被执行

GOMAXPROCES 用来设置可以并行计算的CPU核数的最大值,并返回之前的值

例3:三个协程轮流打印数字1,2,3

方法一:使用 channel

go 复制代码
ch1, ch2, ch3 := make(chan bool), make(chan bool), make(chan bool)

Exit := make(chan int)

go func(a int) {
    for i := 0; i < 3; i++ {
        <-ch1
        fmt.Println(a)
        ch2 <- true
    }
 // 由于我们定义的是无缓冲的channel,打印完最后一个3后其实还往ch1写入了一个值
 // 不加 "<-ch1" 则这个1号协程打印完最后一个1就结束了
 // 那么ch1就因为最后还写入一个值,但是没有协程来读这个值从而导致永久阻塞了
    <-ch1
}(1)

go func(b int) {
    for i := 0; i < 3; i++ {
        <-ch2
        fmt.Println(b)
        ch3 <- true
    }
}(2)

go func(c int) {
    defer close(Exit) // 打印完关闭Exit channel
    for i := 0; i < 3; i++ {
        <-ch3
        fmt.Println(c)
        ch1 <- true
    }
}(3)

ch1 <- true

<-Exit //让main协程阻塞不退出

方法二:使用 WaitGroup

go 复制代码
var wg sync.WaitGroup

wg.Add(3)

ch1, ch2, ch3 := make(chan bool), make(chan bool), make(chan bool)

go func(a int) {
    defer wg.Done()
    for i := 0; i < 3; i++ { 
        <-ch1
        fmt.Println(a)
        ch2 <- true
    }
    <-ch1
}(1)

go func(b int) {
    defer wg.Done()
    for i := 0; i < 3; i++ {
        <-ch2
        fmt.Println(b)
        ch3 <- true
    }
}(2)

go func(c int) {
    defer wg.Done()
    for i := 0; i < 3; i++ {
        <-ch3
        fmt.Println(c)
        ch1 <- true
    }
}(3)

ch1 <- true
wg.Wait()
例4:启动一个 Goroutine 执行定时任务
go 复制代码
ticker := time.NewTicker(time.Second * 2)
done := make(chan bool)
go func() {
    for {
        select {
        case <-done:
            return
        case t := <-ticker.C:
            fmt.Println("Tick at", t)
        }
    }
}()
<-done

接口(interface)

interface {} 空接口,称为万能类型,空接口没有任何方法,因此任何一个数据类型都可以赋值给空接口

go 复制代码
func myFunc(arg interface{}) {
    fmt.Println("myFunc is called...", arg)

    val, ok := arg.(string)  //类型断言,如果是string类型则ok返回true,否则返回false
    if !ok {
        fmt.Println("arg is not string")
    } else {
        fmt.Printf("%T\n", val)
    }
}

func main() {
    book := Book{"Golang"}

    myFunc(book)
    myFunc(100)
    myFunc("abc")
    myFunc(3.14)
}

接口嵌套

以 io 包中的 ReadWriter 接口为例

go 复制代码
type ReadWriter interface {
    Reader
    Writer
}

type Reader interface {
    Read(p []byte) (n int, err error)
}
type Writer interface {
    Write(p []byte) (n int, err error)
}

所以接口 ReadWriter 就等价于
type ReadWriter interface {
    Read(p []byte) (n int, err error)
    Write(p []byte) (n int, err error)
}

利用下划线_ 可以判断结构体是否实现了接口,未实现的话编译通不过

go 复制代码
var _ Foo = Dog{}        // 用来检测判断结构体是否实现了接口
var _ Foo = &Dog{}       // 用来判断结构体指针是否实现了接口

type Foo interface {
    Say()
}

type Dog struct {
}

func (dog Dog) Say() {        //不实现这个方法则上面的Foo编译通不过
    fmt.Println("hello")
}
为什么GO语言不允许为 T 和 *T 定义同名方法?

首先需要明确一点,编译器会为接受者为值类型的方法生成接受者为指针类型的方法,这就是所谓的包装方法,生成这个包装方法主要是为了支持接口

对于接口来讲,在调用指针接受者方法时,传递地址是非常方便的(平台确定了指针的大小就确定了),不需要关心数据的类型;但是如果通过接口来调用值接受者方法,就需要通过接口中的 data 指针把数据拷贝到栈上,由于编译阶段并不能明确接口背后的具体类型,所以编译器不能生成相关的指令完成拷贝,所以接口是不能直接调用值接收者方法的,于是就需要包装方法。

既然编译器会为值接收者的方法生成指针接收者的方法,那么如果我自定义了一个与值接收者同名的指针接收者方法,这就很可能与编译器生成的包装方法重复,于是编译器干脆禁止定义 T 和 *T 的同名方法。

类型断言是怎么实现的?

相对于 int、sting、slice、map 等这些具体类型;接口和空接口则称为抽象类型

类型断言作用于接口值之上,可以是空接口或者非空接口,而断言的目标类型可以是具体类型或非空接口类型,这样便组合出了四种类型断言:

空接口.(具体类型)

go 复制代码
var e interface{}
r, ok := e.(*os.File)    只需要判断空接口e的_type是否指向*os.File的类型元数据即可

非空接口.(具体类型)

go 复制代码
var rw io.ReadWriter
f, _ := os.Open("print.txt")
rw = f
r, ok := rw.(*os.File)

程序中用的 itab 结构体都会被缓存起来,所以这里只需要判断 rw 中的
成员 tab 是否指向 itab 结构体即可

空接口.(非空接口)

go 复制代码
var e interface{}
f := "eggo"
e = f
rw, ok := e.(io.ReadWriter)

判断e是否实现了io.ReadWriter中的所有方法,由于现在e的类型元数据指向的是string type,将会
导致断言失败

非空接口.(非空接口)

go 复制代码
var w io.Writer
f, _ := os.Open("test.txt")
w = f
rw, ok := w.(io.ReadWriter)

判断w是否实现了io.ReadWriter,现在w指向的是 *os.File 的类型元数据,之后判断这个类型
元数据是否实现了io.ReadWriter接口的所有方法即可

总之:类型断言的关键就是明确接口的动态类型以及这个动态类型实现了哪些方法

接口的源码

非空接口类型

暂时无法在飞书文档外展示此内容

go 复制代码
// 源码在 runtime/runtime2.go中

// 非空接口
type iface struct {
    tab  *itab               // 存储接口的类型信息
    data unsafe.Pointer      // 指向接口的地址(实际类型指针)
}

type itab struct {
    inter *interfacetype     // 描述接口的类型
    _type *_type             // 描述接口的动态类型元数据
    hash  uint32 // hash是从上面的动态类型元数据拷贝过来的哈希值,用于类型断言时快速判断目标类型是否和接口中的类型一致
    _     [4]byte
    fun   [1]uintptr     // fun[0]==0 意味着未实现任何方法
// 存储接口的方法地址,这里存储的是第一个方法的指针,如果有更多的方法则由于在内存空间连续存储;从汇编角度通过增加地址就能获取这些函数指针(这些方法是按照函数名称的字典序来排列的)
}

type interfacetype struct {
    typ     _type            // 描述Go语言中各种类型的结构体
    pkgpath name             // 记录接口的包名
    mhdr    []imethod        // 接口定义的方法列表
}

// _type是Go语言中所有类型的公共描述
type _type struct {
    size       uintptr  // 该类型所占的字节数
    ptrdata    uintptr  
    hash       uint32
    tflag      tflag
    align      uint8    // 对齐
    fieldAlign uint8    // 嵌入结构体时对齐
    kind       uint8    // 类型的种类,如bool、int、float、string、struct等
    equal func(unsafe.Pointer, unsafe.Pointer) bool
    gcdata    *byte
    str       nameOff
    ptrToThis typeOff
}

空接口类型

go 复制代码
// 空接口
type eface struct {
    _type *_type
    data  unsafe.Pointer
}

_type 称为类型元数据,像类型名称,大小,对齐边界,是否为自定义类型等信息,是每个类型元数据都要记录的;

可以从类型元数据解释下面这两种写法的区别

go 复制代码
type MyType1 = int32
type MyType2 int32

MyType1 这种写法是给类型 int32 起别名,实际上 MyType1 和 int32 会关联到同一个类型元数据,属于同一种类型;rune 和 int32 就是这样的关系

而 MyType2 是基于已有类型创建新类型,MyType2 会自立门户,拥有自己的类型元数据,即使 MyType2 相对于 int32 来说没有做任何改变,但是它们两个的类型元数据也已经不同了

判断下面程序的输出
go 复制代码
func Foo(x interface{}) {
    if x == nil {
        fmt.Println("empty interface")
        return
    }

    fmt.Println("non-empty interface")
}

type myinterface interface {
    Fun()
}

//var x interface{} = nil    
var x myinterface = nil   //接口类型是无效的方法接收者
var y *int = nil          
Foo(x)    //1.输出 empty interface
Foo(y)    //2.输出 non-empty interface, data为nil,但是type指向的是ptrtype(runtime中的type.go)

答:Go 的 nil 只能赋值给指针或者接口类型变量;无论是非空接口还是空接口类型,都存在一个 _type 和 data 类型的成员;

  • 当 type 和 data 都为 nil 时,整个接口就为 nil
  • 当 data 为 nil,但是 type 不为 nil 时,这时接口就不为 nil
ABCD 哪一行存在错误?
go 复制代码
type S struct{}

func f(x interface{}) {
}

func g(x *interface{}) {
}

func main(){
    s := S{}
    p := &s
    f(s)    A
    g(s)    B
    f(p)    C
    g(p)    D
}

答:BD错误,Go是强类型语言,接口类型可以看作是所有golang其他数据类型的父类;所以函数 f 可以传入任何类型,但是函数 g 只能传入 *interface{} 的参数

反射 (reflect)

  1. 反射可以在运行时动态的获取变量的各种信息,比如变量的类型(type),类别(kind)
  2. 如果是结构体变量,还可以获取到结构体本身的信息(包括结构体的字段,方法)
  3. 通过反射,可以修改变量的值,可以调用关联的方法
  4. Go 语言反射获取的函数类型只跟参数和返回值有关
go 复制代码
func reflectTest(b interface{}) {
    rVal := reflect.ValueOf(b)
    
    rVal.Elem().SetInt(20)
    //上面这一行就相当于
    //var num = 10
    //var b *int = &num
    //*b = 3
}

func main() {
    var num int = 10;
    reflectTest(&num)    //这里传递的是指针类型
    fmt.Println(num)
}

反射:使用反射来遍历结构体的字段,调用结构体的方法,并获取结构体标签的值

go 复制代码
type Monster struct {
    Name        string        `json:"name"`
    Age         int           `json:"monster_age"`
    Score       float32
    Sex         string                
}

func (s Monster) Print() {
    fmt.Println("---start---")
    fmt.Println(s)
    fmt.Println("---end----")
}

func(s Monster) GetSum(n1, n2 int) int {
    return n1 + n2
}

func(s Monster) Set(name string, age int, score float32, sex string) {
    s.Name = name
    s.Age = age
    s.Score = score
    s.Sex = sex
}

func TestStruct(a interface{}) {

    typ := reflect.TypeOf(a)          //获取reflect.Type类型
    val := reflect.ValueOf(a)         //获取reflect.Value类型
    kd := val.Kind()                  //获取a对应的类别

    //如果传入的不是struct,则退出
    if kd != reflect.Struct {
        fmt.Println("expect struct")
        return
    }

    //获取该结构体有几个字段
    num := val.NumField()
    fmt.Printf("struct has %d fields\n", num)

    //遍历结构体所有字段
    for i := 0; i < num; i++ {
        fmt.Printf("Field %d: val = %v\t", i, val.Field(i))

        //获取到struct标签,注意需要通过reflect.Type来获取tag标签的值
        tagVal := typ.Field(i).Tag.Get("json")

        //如果该字段没有Tag标签就不显示
        if tagVal != "" {
            fmt.Printf("Field %d: tag = %v\n", i, tagVal)
        }
    }
    fmt.Println()
    
    //获取到结构体有几个方法
    numOfMethod := val.NumMethod()
    fmt.Printf("struct has %d methods\n", numOfMethod)

    //Method()表示获取该方法,Call表示调用该方法
    //获取到第二个方法并调用,方法的顺序是按照函数名ASCII码的顺序排列的
    //所以调用的函数顺序默认为: GetSum(), Print(), Set() 三个方法
    val.Method(1).Call(nil)                                                

    //利用反射调用方法
    var params []reflect.Value
    params = append(params, reflect.ValueOf(10), reflect.ValueOf(40))
    res := val.Method(0).Call(params)           //传入的参数是 []reflect.Value
    fmt.Println("res =", res[0].Int())          //返回的结果是 []reflect.Value

}

func main() {
    var a Monster = Monster{
        Name: "Tom",
        Age: 24,
        Score: 99.99,
    }

    TestStruct(a)
}

使用反射来调用结构体内的方法

go 复制代码
type Service struct{}

func (s *Service) Add(a, b int) int {
    return a + b
}

func CallWithInterface(rcvr interface{}) {
    // 按照方法的名字来调用
    v := reflect.ValueOf(rcvr).MethodByName("Add").Call([]reflect.Value{reflect.ValueOf(10), reflect.ValueOf(20)})

    // 这里返回结果的类型一定要判断下,类型不对容易panic
    if res, ok := v[0].Interface().(int); ok {
        fmt.Println(res)
    }

}

func main() {
    svc := &Service{}
    CallWithInterface(svc)
}

结构体的序列化和反序列化

golang 中的结构体标签的使用

go 复制代码
type Movie struct {
    Title string `json:"title_json" yaml:"title_yaml"`
    Year  int    `json:"year_json" yaml:"year_yaml"`
    Price int    `json:"rmb_json" yaml:"price_yaml,omitempty"`
}

//Json序列化
func main() {
    movie := Movie{"Wind", 2000, 10}

    // 编码的过程, 结构体 -> json
    jsonStr, err := json.Marshal(movie)
    if err != nil {
        panic(err)
    }

    // 需要使用 Printf 不能使用 Println
    // fmt.Println(jsonStr)
    fmt.Printf("%s\n", jsonStr)

    // 解码的过程, jsonStr -> 结构体
    myMovie := Movie{}
    err = json.Unmarshal(jsonStr, &myMovie)
    if err != nil {
        panic(err)
    }

    fmt.Printf("%v\n", myMovie)
}

//Yaml序列化也类似
func main() {
    movie := Movie{
        Title: "Wind",
        Year:  2000,
    }

    yamlStr, err := yaml.Marshal(movie)
    if err != nil {
        panic(err)
    }
    fmt.Printf("%s", yamlStr)

    myMovie := Movie{}
    err = yaml.Unmarshal(yamlStr, &myMovie)
    if err != nil {
        panic(err)
    }
    fmt.Printf("%v\n", myMovie)
}

添加 omitempty 标签则在序列化的时候,struct中不初始化该属性时,序列化就忽略该属性

go 复制代码
type Teststrut struct {
    Name  string `json:"name"`
    Age   int    `json:"age"`
    IsBoy bool   `json:"is_boy,omitempty"` 
    //添加了 "omitempty" 标签则在序列化的时候,不初始化该属性时,就不返回该属性
}

obj := Teststrut{Name: "xiaoming", Age: 18}
s1, _ := json.Marshal(obj) 
输出为:{"name":"xiaoming","age":18}

不加上 omitempty 标签进行序列化时未初始化属性会给定一个默认值
输出为 {"name":"xiaoming","age":18,"is_boy":false}

Q:json包使用的时候,结构体里面的变量不加 tag 时能不能正常转成 json 里面的字段?

A: 如果变量首字母小写,则为 private , 无论如何转化不了,因为取不到反射信息

如果变量首字母大写

  • 不加 tag, 可以正常转成 json 里面的字段,其 json 字段名与结构体内字段原名一致
  • 加了 tag, 从 struct 转化成 json 的时候,json 的字段名就是 tag 里的字段名,原来 struct 里面的字段名没有用了
go 复制代码
type Stu struct {
    name    string
    sister  string `json:"sis"`    //这里编译阶段提醒报错
    Brother string
    Parent  string `json:"parent"`
}

j := Stu{
    name:    "Nero",
    sister:  "Nero_sister",
    Brother: "Nero_brother",
    Parent:  "Nero_parent",
}
jsonInfo, _ := json.Marshal(j)
fmt.Printf("%v\n", string(jsonInfo))

输出: {"Brother":"Nero_brother","parent":"Nero_parent"}

注意:结构体如果指定了序列化的字段名,则对应的yaml文件中的字段要对应相同才能反序列化成功,如果未指定序列化字段,则默认为结构体变量名的全小写形式

go 复制代码
type Student struct {
    Id   int    `yaml:"id_yaml"`    //如果未指定序列化字段,则默认为字段的全小写形式,如"id"
    Name string `yaml:"name_yaml"`
}

func main() {
    // 反序列化
    yamlString := `
       id_yaml: 1        
       name_yaml: Alice
    `
    //注意上面9~11行开头不能使用tab,必须全部使用空格,yaml可能识别不出tab
    stu := Student{}
    err := yaml.Unmarshal([]byte(yamlString), &stu)
    if err != nil {
        panic(err)
    }

    fmt.Println(stu)
}

Golang 测试

通过 go tool nm 可以查看该文件中实现了哪些函数,nm 会输出 OBJ 文件中定义或使用到的符号信息,通过 grep 命令过滤代码段符号对应的T标识,即可查看文件中实现的函数

Golang中的 *_test.go 全都是测试文件,其中的 BenchMark 测试要写在这个文件里面

Golang 的 recover 函数只能在 defer 函数中直接调用,不能通过另外的函数间接调用,这是语言实现层面的要求,不满足要求的 recover 调用,不会有任何效果

go 复制代码
// 正确:直接调用
func A() {
    defer B()
    panic("panicA")
}

func B() {
    p := recover()
    fmt.Println(p)
}

// 错误:间接调用
func A() {
    defer B()
    panic("panicA")
}

func B() {
    C()
}

func C() {
    p := recover()
    fmt.Println(p)
}

Go Module

Go Modules 是 Go 语言的依赖解决方案,发布于Go1.11,成长于Go1.12,丰富于Go1.13,正式于Go1.14推荐在生产中使用

Go Modules 目前集成在 Go 的工具链中,只要安装了 Go,自然而然也就可以使用 Go Modules 了,而 Go Modules 的出现也解决了 Go1.11 前的几个常见争议问题:

  • Go 语言长久以来的依赖管理问题
  • "淘汰" 现有的 GOPATH 的使用模式
  • 统一社区的其他的依赖管理工具(提供迁移功能)

GOMODULE 的用法

bash 复制代码
go mod init github.com/aceld/moudle_test    #初始化,这决定了以后其他人怎么导入这个包
go get #下载对应的包(这个下载的包实际上是在$GOPATH/bin/pkg/mod/下)

Go 语言提供了 GO111MOUDLE 这个环境变量来作为 Go Moudle 的开关

bash 复制代码
go env -w GO111MODULE=on   
启用 GO111MODULE

go env -w GOPROXY=https://goproxy.cn,direct

GOPROXY:这个环境变量主要用于设置 Go 模块代理(Go moudle proxy),其作用是用于使 Go 在后续拉取模块版本时直接通过镜像站点来快速拉取,其实就是项目的第三方依赖库的下载地址,后面的 direct 参数表示指示 Go 回源到模块版本的源地址去抓取

GOSUMDB:用来校验拉取的第三方库是否是完整的,如果设置了 GOPROXY 就不用设置这个参数了

注:只要是镜像或者什么其他下载地址,全部换成 国内的镜像源

使用 Go Module 初始化非本地项目不要求一定在

$GOPATH/src 目录下,但是如果使用本地包还是需要在 /src 目录下

go 复制代码
//model包
var Name = "hello world"

//main包
import (
    "fmt"
    "model"
)
func main() {
    fmt.Println(model.Name)
}
这种情况如果 
go env -w GO111MODULE=on 则无法使用 GOPATH 下的包

附:GO111MODULE

  • =off,Go 命令会忽略 go.mod 文件,使用 GOPATH 搜索包
  • =on,Go 命令会使用 go.mod 下的路径搜索包(以后全部用 on)
  • =auto, Go 命令会先搜索是否存在 go.mod 文件,不存在再使用 GOPATH 搜索包
bash 复制代码
go mod 命令
go mod init         #生成 go.mod 文件
go mod download     #下载 go.mod 文件中指明的所有依赖
go mod tidy         #整理现有的依赖
go mod graph        #查看现有的依赖结构
go mod vendor       #到处项目所有的依赖到 vendor 目录
go mod verify       #校验一个模块是否被篡改过
go mod why          #查看为什么需要依赖某个模块

总结:在本地开发项目时,不同的包都要求是在

$GOPATH/src 下,这样才能应用,但是有的时候需要引用 Github 上的包,这时就有两种方式

1、直接下载下来放在 /src 目录下面,这样当然可以引用,但是十分繁琐,而不好管理

2、使用 GoMoudle,利用 go get 命令直接下载好需要的包(默认是在

$GOPATH/src/pkg/下面),这样的好处是项目路径不必局限于一定要在 src/ 目录下,可以在任何目录,因为其会在 pkg/ 目录下找我们下载好的包,这样操作方便也好管理

注:首次使用 go get 拉取包时,默认是拉取的tag的最新的包,如果没有tag,则会去拉取主分支的包;

拉取成功后就会在当前目录下的go.mod文件中存在拉取的包名,每次使用 go get 不加指定分支的名就更新的是当前引用的这个包;而使用 go get 加分支名就会更新这个 go.mod 中的包

总结:

  • 不开启 Go Module,则包的查找路径为 GOROOT,GOPATH/src
  • 开启了 Go Module,则包的查找路径为 GOROOT, GOPATH/pkg/mod 以及 当前目录

Go删除本地包缓存并更新版本:go clean -modcache && rm go.sum && go mod tidy

go clean -modcache 会将本地的 $GOPATH/pkg/mod 里面的文件都清理掉

二、项目实战

同属于一个包的文件不需要 import, Go 会自动链接

网络编程有两种:

  • TCP socket 编程,是网络编程的主流
  • b/s 结构的 http 编程,我们使用浏览器去访问服务器时,使用的就是 http 协议

如果把 IP 地址比做一个房子,那么端口就是出入这间房子的门,端口是通过端口号来标记的,端口号只有整数

0号是保留端口

1 - 1024 是固定端口,即被某些程序固定使用,程序员不要使用这些固定端口

如:21端口是 ftp协议,22是 SSH 远程登录协议,23 telent协议,25 smtp协议

1025 - 65535 是动态端口,程序员可以使用

Web 编程

Web 服务端模型

两种 HTTP 请求方法:GET 和 POST

在客户机和服务器之间进行请求-响应时,两种最常被用到的方法是:GET 和 POST

  • GET - 从指定的资源请求数据。
  • POST - 向指定的资源提交要被处理的数据。

PUT请求:如果两个请求相同,后一个请求会把前一个请求覆盖掉。(所以PUT用来更新资源)

Post请求:后一个请求不会把第一个请求覆盖掉。(所以Post用来增加资源)

总结:HTTP四大方法,Get Put Post Delete 分别对应查改增删

HTTP 服务流程:Client -> Requsts ->router -> hander ->Resposne ->Client

  • Request:用户的请求信息,主要用来解析,包括post,get
  • router:路由根据 url 找到对应的 hander 并执行
  • hander:处理请求和生成返回信息的函数
  • response:服务器返回给客户端的信息

HTTP 请求模拟:浏览器 (只能模拟get请求)、命令行 curl (可模拟任何请求)

可以利用 postman 进行端口测试

Web 服务器也被称为 HTTP 服务器

所谓 HTTP 服务器,主要在于接受 client 的 request,并向 client 返回 response

创建一个 HTTP 服务,大致需要两个过程,

  • 首先需要注册路由,即提供 url 模式和 handler 函数的映射
  • 其次就是实例化一个 server 对象,并开启对客户端的监听

我们的浏览器其实就是一个发送和接受 HTTP 协议数据的客户端,我们平时通过浏览器访问网页其实就是从网站的服务器接受 HTTP 数据,然后浏览器会按照 HTML、CSS 等规则将网页渲染展示出来(其实手机客户端内部也是通过浏览器实现的)

一个典型的 HTTP 服务应该如下图所示

一个正常的 HTTP request 请求(请求报文),大概是下面这样子:

bash 复制代码
GET /api HTTP/1.1            请求行
Host: 127.0.0.1:8888         请求头
User-Agent: curl/7.64.1
Accept: */*
空行

Wireshark 抓包显示结果

请求头通知服务器有关客户端请求的信息,典型的请求头有:

User-Agent 请求的浏览器类型
Host 请求的主机名
Accept 客户端可识别的响应内容类型, */* 代表可接受所有类型

HTTP 的 response(响应报文)大概是下面这样子:

可使用 curl -I 查看响应报文的头部信息

bash 复制代码
HTTP/1.1 200 OK            响应行
Content-Length: 5          响应头
Content-Type: text/html; charset=utf-8
空行

Wireshark 抓包显示结果

典型的响应头内容有:

bash 复制代码
Content-Length 响应 Body 的字节大小
Content-Type 表示响应 Body 中的内容类型

Http Server

一个简单的HTTP Server代码实例如下:

go 复制代码
func HelloHandler(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("/hello http"))
}

func main() {
    http.HandleFunc("/", HelloHandler)
    http.ListenAndServe("127.0.0.1:8000", nil)
}

上面的大致过程为:

  1. 注册处理器到一个hash表中,可以通过键值路由匹配
  2. 注册完之后就是开启循环监听,每监听到一个链接就会创建一个Goroutine
  3. 在创建好的Goroutine里面会循环的等待接收请求数据,然后根据请求的地址去处理路由表中匹配对应的处理器,然后将请求交给处理器处理

step1. 注册处理

HandleFunc函数会一直调用到 ServeMux 的 Handle 方法中

go 复制代码
func (mux *ServeMux) Handle(pattern string, handler Handler) {
    mux.mu.Lock()
    defer mux.mu.Unlock()
    ...
    e := muxEntry{h: handler, pattern: pattern}
    mux.m[pattern] = e
    if pattern[len(pattern)-1] == '/' {
        mux.es = appendSorted(mux.es, e)
    }

    if pattern[0] != '/' {
        mux.hosts = true
    }
}

Handle 会根据路由作为 hash 表的键来保存 muxEntry 对象,muxEntry封装了 pattern 和 handler。如果路由表达式以'/'结尾,则将对应的muxEntry对象加入到[]muxEntry

hash 表是用于路由精确匹配,[]muxEntry用于部分匹配

这里注意一点:在 ServeMux.HandleFunc 里面会将自己注册的 HelloHandler 函数转换为 HandleFunc,这样做的目的就是为了全部抽象出来成 ServeHTTP 方法

go 复制代码
type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

从上面代码可见,调用 ServeHTTP 方法就是调用的 HandleFunc 方法

step2: 监听

监听是通过调用 ListenAndServe 函数,里面会调用 server.ListenAndServe

go 复制代码
func (srv *Server) ListenAndServe() error {
    if srv.shuttingDown() {
        return ErrServerClosed
    }
    addr := srv.Addr
    if addr == "" {
        addr = ":http"
    }
    //监听端口
    ln, err := net.Listen("tcp", addr)
    if err != nil {
        return err
    }
    //循环接收监听到的网络请求
    return srv.Serve(ln)
}

Server.Serve

go 复制代码
func (srv *Server) Serve(l net.Listener) error {
    ...
    baseCtx := context.Background()
    ctx := context.WithValue(baseCtx, ServerContextKey, srv)
    for {
        //接受listener过来的网络连接
        rw, err := l.Accept()
        ...
        tempDelay = 0
        c := srv.newConn(rw)
        c.setState(c.rwc, StateNew, runHooks) 
        
        //创建协程处理连接
        go c.serve(connCtx)
    }
}

Serve 这个方法里面会用一个循环去接收监听到的网络连接,然后创建协程处理连接。所以难免就会有一个问题,如果并发很高的话,可能会一次性创建太多协程,导致处理不过来的情况

step3: 处理请求

conn.serve

go 复制代码
func (c *conn) serve(ctx context.Context) {
    c.remoteAddr = c.rwc.RemoteAddr().String()
    ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
    ...
    // HTTP/1.x from here on.
    ctx, cancelCtx := context.WithCancel(ctx)
    c.cancelCtx = cancelCtx
    defer cancelCtx()

    c.r = &connReader{conn: c}
    c.bufr = newBufioReader(c.r)
    c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)

    for {
        w, err := c.readRequest(ctx)
        ...
        //根据请求路由调用出炉起处理请求
        serverHandler{c.server}.ServeHTTP(w, w.req)
        w.cancelCtx()
        if c.hijacked() {
            return
        }
        w.finishRequest()
        ...
    }
}

当一个连接建立之后,该连接中所有的请求都将在这个协程中进行处理,直到连接被关闭。在 for 循环里面会循环调用 readRequest 读取请求进行处理

请求处理是通过调用 ServeHTTP 进行的

go 复制代码
type serverHandler struct {
    srv *Server
}

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
    handler := sh.srv.Handler
    if handler == nil {
        handler = DefaultServeMux
    }
    if req.RequestURI == "*" && req.Method == "OPTIONS" {
        handler = globalOptionsHandler{}
    }
    handler.ServeHTTP(rw, req)
}

serverHandler 其实就是 Server 包装了一层。这里的 sh.srv.Handler参数实际上是传入的 ServeMux 实例,所以这里最后会调用到 ServeMux 的 ServeHTTP 方法

总结出来整个过程就是如下:

go 复制代码
func IndexHandler(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("/hello http"))
}

func main() {
    http.HandleFunc("/", IndexHandler)
    http.ListenAndServe("127.0.0.1:8000", nil)
}

对于 http.HandleFunc 调用
1.先调用 DefaultServeMux.HandleFunc(pattern, handler)
    type HandlerFunc func(ResponseWriter, *Request)
    func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
        f(w, r)
    }
使用 HandlerFunc 类型包装一个自定义路由的IndexHandler函数,这样做的目的就是让这个函数也实现
ServeHTTP 方法
2.往 DefaultServeMux 中的 map[string]muxEntry 增加对应的handler和路由规则

对于 http.ListenAndServer 调用
1.先实例化 Server,之后调用 server.ListenAndServe()
2.调用监听 ln, err := net.Listen("tcp", addr)
3.TCP实际是调用这个API  l, err = sl.listenTCP(ctx, la)
4.在Server函数中启动一个for循环,在循环中Accept请求
    for {
        rw, err := l.Accept()
        ...
        c := srv.newConn(rw)
        ...
        go c.serve(connCtx)
    }
5.对每一个请求实例化一个Conn,并且开启一个goroutine为这个请求进行服务
6.读取每个请求的内容 w, err := c.readRequest(ctx)
7.处理请求 serverHandler{c.server}.ServeHTTP(w, w.req)
在这个里面会判断 handler 是否为空,如果handler为空,则就将handler置为DefaultServeMux
    handler := sh.srv.Handler
    if handler == nil {
        handler = DefaultServeMux
    }
而这个 DefaultServeMux 中的路由是上面 http.HandleFunc 中添加的
8.之后调用 handler.ServeHTTP(rw, req) 处理对应的请求

注意上面第4步:Server 端接受一个新的TCP连接,创建一个新的协程用于读写该连接上的数据(标准库 net/http, thrift 等都是使用这种服务端模型)但是用协程这种方式在请求处理阻塞的情况下,容易引发 "协程爆炸",进而引发内存占用过多,调度延迟等问题

路由可以理解为用户的请求,而 Handler 可以理解为处理这个请求的相关逻辑

暂时无法在飞书文档外展示此内容

Gin 框架

Gin 最擅长的就是 API 接口的高并发,如果项目的规模不大,业务相对简单,可以考虑用 Gin 重写接口;Gin 框架允许开发者在处理请求的过程中,加入用户自己的钩子 (Hook) 函数。这个钩子函数就叫做中间件,中间件适合处理一些公共的业务逻辑,比如登陆认证、权限校验、数据分页、记录日志、耗时统计等

通俗的讲:中间件就是匹配路由前和匹配路由后的一系列操作

GET 请求URL后面的 ? 后面是 querystring 参数,就是我们需要查询的内容,其是 key=value 形式,多个 key-value 用 & 连接

如:https://www.sogou.com/web?name=程潇&age=18 表示 在路径 /web 下查询 name为程潇,并且age为18

Cookie 是什么?

  • HTTP是无状态协议,服务器不能记录浏览器的访问状态,也就是说服务器不能区分两次请求是否由同一个客户端发出
  • Cookie 就是解决HTTP协议无状态的方案之一,中文是小甜饼的意思
  • Cookie 实际上就是服务器保存在浏览器上的一段信息。浏览器有了 Cookie 之后,每次向服务器发送请求时都会同时将该 Cookie 信息发送给服务器,服务器收到请求后,就可以根据该信息处理请求
  • Cookie由服务器创建,并发送给浏览器,最终由浏览器保存

源码解析

go 复制代码
// StringToBytes converts string to byte slice without a memory allocation.
func StringToBytes(s string) []byte {
    return *(*[]byte)(unsafe.Pointer(
        &struct {
            string
            Cap int
        }{s, len(s)},
    ))
}

注意:这里指针也是值传递,原来的数据不会改变,也就是b还是一个切片只不过在函数内部给它强转了下
// BytesToString converts byte slice to string without a memory allocation.
func BytesToString(b []byte) string {
    return *(*string)(unsafe.Pointer(&b))    //将byte切片强制转换成string
}

go工具

同属于一个包的文件不需要 import, Go 会自动链接

  1. 一个Go项目在GOPATH下,会有如下三个目录
bash 复制代码
➜  Golang ls
bin  pkg  src
  1. bin 存放编译后的可执行文件 ,主要是与Go相关的工具,如
bash 复制代码
dlv         go语言调试工具
dodef       go语言代码定义和引用的跳转
go-outline  go语言文件大纲
  1. pkg 存放 go get 下来的包文件
  2. src 存放项目源文件 一般 bin 和 pkg 目录可以不创建,go命令会自动创建(如 go install),只需要创建 src目录即可

Golang 内置的工具

bash 复制代码
➜  Golang go tool
addr2line
asm
buildid
cgo
compile
covdata
cover
doc
fix
link
nm
objdump
pack
pprof
test2json
trace
vet
  • nm:查看符号表(与系统的nm命令有区别,go中的 nm 命令可分析.o文件,系统的 nm 命令只能分析二进制文件)
  • objdump:反汇编工具,分析二进制文件(等同于系统的 objdump 命令)
  • pprof:指标,性能分析工具
  • compile:代码汇编

三、技术细节

只要是含有 main 函数文件的包全部设置 package main

Go 中的 INT_MAX 和 INT_MIN

Go 中的 INT_MAX 和 INT_MIN 可以自己定义,也可直接使用 math 包中的 MaxInt

go 复制代码
const UINT_MIN uint = 0             //无符号最小值
const UINT_MAX = ^uint(0)           //无符号最大值, Golang中取反用的是'^'不是'~'

//有符号最小值和最大值
const INT_MAX = int(^uint(0) >> 1)  //根据二进制补码,第一位为0,其余为1
const INT_MIN = ^INT_MAX

// Go中如果没有指定int数据类型,则默认为int64
res := 0
fmt.Print(unsafe.Sizeof(res))        //输出为8
go 复制代码
// golang中默认的int是8字节的
num := int(0)               // 这时num占用8字节
num := int8(0)              // 这时num占1字节

num = ^int8(0)               // 结果为-1
// 1.num写成补码表示为 0 0 0 0 0 0 0 0
// 2.取反可得 1 1 1 1 1 1 1 1
// 3.变成原码表示为 1 0 0 0 0 0 0 1 => -1

从终端读取数据

三种读取方式总结:

go 复制代码
第一种:fmt.Scan
输入:读取以空白符分割的值返回到地址中进行修改,换行视为空白符 
返回值:错误处理,返回值中有一个 int 类型的值是返回正确的数量,有一个 err 是错误的原因
var a, b int
fmt.Scan(&a, &b)

第二种 fmt.Scanf
它与 Scan 比更加严谨,使用 format 读取空白符,在输入时也必须输入要输入的数据,适用场景学生输入账号,性别,而且必须要根据指定的方式输入,顺序也不可以改变
var a, b int
fmt.Scanf("%d:%d", &a, &b)        //输入一定要 11:22,中间的冒号不能少

第三种 fmt.Scanln
Scanln 类似 Scan,但它在遇到换行时才停止扫描,最后一个数据后面必须有换行或者到达结束位置
使用场景就是只要换行就结束;返回错误和Scan一样
var a, b int
fmt.Scanln(&a, &b)

以上三种方式一遇到空格便直接返回,都是以空格相隔

想从终端输入一个带空格的字符串 "hello world"

使用下面这三种方法发现都只能读取到空格前面的字符串 "hello"

go 复制代码
var str string
fmt.Scan(&str)
fmt.Scanf("%s", &str) 
fmt.Scanln(&str)                

使用第三方库即可

go 复制代码
reader := bufio.NewReader(os.Stdin)
res, _, err := reader.ReadLine()
if err != nil {
    fmt.Println("reader Readline err: ", err)
    return
}
str := string(res)

append 函数的用法

slice 是通过引用来传递,而不是通过值传递

go语言append函数是值传递,所以append函数在追加新元素到切片时,append会生成一个新切片,并且将原切片的值拷贝到新切片

  1. 如果append时数组还有多余容量,则新旧两个切片利用的都是同一个底层数组
  2. 如果append时数组没有多余容量,则会导致一个新的 slice 的产生,底层数组也会重新拷贝一份
go 复制代码
path = []int{1,2,3}
res = append(res, path)

path[0] = 10
res = append(res, path)                
//由于slice是通过引用来传递不是值传递,所以这时res结果为 [[10,2,3],[10,2,3]]

cp := make([]int, len(path))
copy(cp, path)
cp[0] = 10
res = append(res, cp)        //这时res结果为 [[1,2,3],[10,2,3]] 

// Leetcode第797题和第131题就是这个坑,回溯时一定要注意不要直接append原来的切片,一定要append原来切片的拷贝
res = append(res, path)     // path在后面可能会被更改,这就会导致res之前append进去的值会被更改

//方法一:
copy(cp, path)                // 1.创建一个副本
res = append(res, cp)         // 2.拷贝这个副本

//方法二:
res = append(res, append([]string{}, path...))   // 一步到位 

append 函数的用法注意(与其原始容量很有关系)

go 复制代码
var result = make([]int, 2, 100)  
var resultCap = make([]int, 2, 2) 

b := append(result, 1)     //由于result的容量很大,则在此基础上进行append还是复用的底层数组
c := append(resultCap, 3)   //而resultCap的容量有限,扩容会导致新的底层数组的产生

b[0] = 999    //切片b和result在底层的数组都是相同的,但是其长度是不同的      
c[0] = 999                        
fmt.Println(result, resultCap)  // 结果为[999, 0], [0, 0]

判断下面程序输出

go 复制代码
a := make([]int, 0, 5)
a = append(a, 1)
b := append(a, 2)    //这里并没有改变原切片a的值,因为返回的新切片是赋值给了b,不是a
c := append(a, 3)
fmt.Printf("%v\n", a)
fmt.Printf("%v\n", b)
fmt.Printf("%v\n", c)
注意:a,b,c三个切片都是复用的一个底层数组

golang中的方法

1.分析以下程序为什么能正常执行?

go 复制代码
type A struct {
    name string
}

func (a A) GetName() string {
    return a.name
}

func (pa *A) SetName() string {
    pa.name = "Hi! " + pa.name
    return pa.name
}

func main() {
    a := A{name: "eggo"}
    pa := &a
    
    fmt.Println(pa.GetName())
    fmt.Println(a.SetName())
}

答:编译期间,会把 pa.GetName() 这种方法调用转换成 (*pa).GetName(),也就等价于执行 A.GetName(*pa)。而 a.SetName()会被转换成 (&a).SetName(),也相当于执行(*A).SetName(&a)

总结:所以指针定义的结构体方法和值定义的结构体方法可以互相调用

2.编译器的语法糖

go 复制代码
type A struct {
    name string
}

func (a A) Name() string {
    a.name = "Hi! " + a.name
    return a.name
}

func NameOf(a A) string {
    a.name = "Hi " + a.name
    return a.name
}

func main() {
    a := A{name:"Eggo"}

    fmt.Println(a.Name())        // 1.编译器的语法糖,提供面向对象的语法
    fmt.Println(A.Name(a))       // 2.更贴近真实现实的写法,与普通函数调用没本质区别

    // golang 中的方法本质上就是普通函数,而接收者就是隐含的第一个参数
    t1 := reflect.TypeOf(A.Name)
    t2 := reflect.TypeOf(NameOfA)
    fmt.Println(t1 == t2)        // 输出true
}

Context

为什么需要 Context:在并发程序中,由于超时、取消操作或者一些异常情况,往往需要进行抢占操作或者中断后续操作

举个例子:在 Go http 包的 Server 中,每一个请求都有一个对应的 goroutine 去处理。请求处理函数通常会启动额外的 goroutine 用来访问后端服务,比如数据库和 RPC 服务,用来处理一个请求的 goroutine 通常需要访问一些与请求特定的数据,比如终端用户的身份认证信息、验证相关的token、请求的截止时间。 当一个请求被取消或超时时,服务端所有用来处理该请求的 goroutine 都应该迅速中断退出,然后系统才能释放这些 goroutine 占用的资源

context 的常用场景

  1. 上下文控制(下面这两个也属于上下文控制和交互)
  2. 一个请求对应多个 goroutine 之间的数据交互 (主要是指传递共享变量)
  3. 超时控制

根据官方文档的说法,该类型被设计用来在API 边界之间以及过程之间传递截止时间、取消信号及其他与请求相关的数据

Context 实际上是一个接口,其提供了四个方法

go 复制代码
type Context interface {
    //Deadline 返回 ctx 的截止时间,ok 为 false 表示没有设置。
    //达到截止时间的 ctx 会被自动 Cancel 掉; 
    Deadline() (deadline time.Time, ok bool)
    
    //如果当前 ctx 是可取消的,Done 返回一个chan 用来监听,否则返回 nil。
    //当ctx被Cancel时,返回的chan会同时被 close 掉,也就变成"有信号"状态 
    Done() <-chan struct{}
    
    //如果 ctx 已经被 canceled,Err 会返回执行 Cancel 时指定的error,否则返回nil;
    //也就是返回 Context 被取消的原因
    Err() error
    
    //Value 用来从 ctx 中根据指定的 key 提取对应的 value 
    Value(key interface{}) interface{}
}

Context 提供了四种 context,分别是普通 context, 可取消的 context, 超时 context 以及带值的 context

总结为一个接口,四种实现,六个函数

暂时无法在飞书文档外展示此内容

Background() 是所有 Context 的 root,不能被 cancel

go 复制代码
普通context,通常这样调用: ctx, cancel := context.WithCancel(context.Background())
func  WithCancel (parent Context) (ctx Context, cancel CancelFunc)
 
带超时的context,超时之后会自动close对象的Done,与调用CancelFunc的效果一样
WithDeadline 明确地设置一个指定的系统时钟时间,如果超过就触发超时
WithTimeout 设置一个相对的超时时间,也就是deadline设为timeout加上当前的系统时间
因为两者事实上都依赖于系统时钟,所以可能存在微小的误差,所以官方不推荐把超时间隔设置得太小
通常这样调用:ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
func  WithDeadline (parent Context, d time.Time) (Context, CancelFunc)
func  WithTimeout (parent Context, timeout time.Duration) (Context, CancelFunc)
 
带有值的context,没有CancelFunc,所以它只用于值的多goroutine之间传递和共享
通常这样调用:ctx := context.WithValue(context.Background(), "key", myValue)
func  WithValue (parent Context, key, val interface {}) Context

应用场景1:上下文控制

  1. 使用 Context 终止多个 Goroutine
go 复制代码
func worker(ctx context.Context, name string) {
    go func() {
        for {
            select {
            case <-ctx.Done():
                fmt.Println(name, "got the stop channel")
                return
            default:
                fmt.Println("still working")
                time.Sleep(time.Second)
            }
        }
    }()
}

func main(){
    //context.Background() 返回一个空的 Context, 这个空的 Context 一般用于
    //整个 Context 树根节点。然后使用这个 context.WithCancel(parent) 函数
    //创建一个可取消的子 Context,再当作参数传递给 goroutine 使用,这样就可以使用这个
    //子 Context 跟踪这个 goroutine
    ctx, cancel := context.WithCancel(context.Background()) //返回根节点
    
    //开启多个 goroutine,传入 ctx
    go worker(ctx, "node01")
    go worker(ctx, "node02")
    go worker(ctx, "node03")
    
    time.Sleep(2 * time.Second)
    fmt.Println("stop the goroutine")
    
    //停止掉所有的 goroutine,包括根节点及其下面的所有子节点(这个相当于一个总控开关)
    cancel()
    
    time.Sleep(2 * time.Second)
}
  1. 在HTTP中应用Context,来监听客户端取消请求的动作
go 复制代码
// Handler for example request
func exampleHandler(w http.ResponseWriter, req *http.Request) {

    fmt.Println("example handler started")

    // Accessing the context of the request
    context := req.Context()

    select {
    //这里等待十秒钟模拟服务端对客户端请求的处理动作
    case <-time.After(10 * time.Second):
        fmt.Fprintf(w, "example\n")
    //处理客户端的请求取消动作
    case <-context.Done():
        err := context.Err()
        fmt.Println("server:", err)
    }

    fmt.Println("example handler ended")
}

func main() {
    http.HandleFunc("/example", exampleHandler)
    http.ListenAndServe(":5000", nil)
}

curl localhost:/example 等待十秒会正常输出"example",但是如果中途客户端停止访问,
则会返回一个context.Err的错误

应用场景2:一个请求对应多个 goroutine 之间的数据交互

  1. 主 Goroutine 利用 Context 传递元数据给其子 Goroutine
go 复制代码
var key string = "name"
 
func run(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("任务%v结束退出\n", ctx.Value(key))
            return
        default:
            fmt.Printf("任务%v正在运行中\n", ctx.Value(key))
            time.Sleep(time.Second * 2)
        }
    }
}
 
func main() {
    //管理启动的协程
    ctx, cancel := context.WithCancel(context.Background())
    // 给ctx绑定键值,传递给 goroutine, 这个 key-value 就是 "name"-"监控"
    valueCtx := context.WithValue(ctx, key, "监控")

    // 开启goroutine,传入ctx
    go run(valueCtx)

    // 运行一段时间后停止
    time.Sleep(time.Second * 10)
    fmt.Println("停止任务")
    
    //使用context的cancel函数停止goroutine,从上面这几个例子可以看出,
    //cancel()起作用的方式其实就是关闭 context's Done channel
    cancel() 
    
    // 为了检测监控过是否停止,如果没有监控输出,表示停止
    time.Sleep(time.Second * 3)
}
  1. 中间件中的应用
go 复制代码
func main() {
    router := mux.NewRouter()
    router.Use(guidMiddleware)
    router.HandleFunc("/ishealthy", handleIsHealthy).Methods(http.MethodGet)
    http.ListenAndServe(":8080", router)
}

func handleIsHealthy(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    uuid := r.Context().Value("uuid")    // 这里使用ctx注入的key-value值
    log.Printf("[%v] Returning 200 - Healthy", uuid)
    w.Write([]byte("Healthy"))
}

func guidMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        uuid := uuid.New()
        //这里注入context的一个key-value值
        r = r.WithContext(context.WithValue(r.Context(), "uuid", uuid))
        next.ServeHTTP(w, r)
    })
}

应用场景3:超时控制

WithTimeout 实际上调用的还是 WithDeadline 函数,WithTimeout 会在指定的时间后自动关闭掉 Ctx.Done这个channel,但是观察这个函数的返回值可以发现其还是返回了一个cancel函数,这个cancel 函数是提供给用户手动关闭 Ctx.Done 这个 channel 的

go 复制代码
func coroutine(ctx context.Context, duration time.Duration, id int, wg *sync.WaitGroup) {
    for {
        select {
        case <-ctx.Done():
                fmt.Printf("协程 %d 退出\n", id)
                wg.Done()
                return
        case <-time.After(duration):
                fmt.Printf("消息来自协程 %d\n", id)
        }
    }
}
 
func main() {
    //使用 WaitGroup 等待所有的goroutine执行完毕,在收到<-ctx.Done()的终止信号后使wg中需要
    //等待的 goroutine 数量减一,因为 context 只负责取消 goroutine,不负责等待 goroutine 运行,
    //所以需要配合一点辅助手段管理启动的协程
    wg := &sync.WaitGroup{}
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()    //官方建议这里写一个 defer cancel()

    for i := 0; i < 3; i++ {
        wg.Add(1)
        go coroutine(ctx, 1*time.Second, i, wg)
    }
    wg.Wait()
}

Q: WithCancel() 和 WithTimeout() 可以通知多个goroutine, 如何实现的?

WithCanel会生成一个新的CancelCtx, 并将这个新的CancelCtx标记为这个 parent ctx 的children

go 复制代码
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    if parent == nil {
        panic("cannot create context from nil parent")
    }

c := newCancelCtx(parent)
propagateCancel(parent, &c)
    return &c, func() { c.cancel( true , Canceled) }  可以看到CancelFunc实际是c.cancel的封装
}

func propagateCancel(parent Context, child canceler) {
    如果parent Ctx是 emptyCtx 则不标记父子关系
    done := parent.Done()
    if done == nil {
        return // parent is never canceled
    }
    
    ...

    如果是parent ctx是可取消的Ctx,则标记父子关系
    if p, ok := parentCancelCtx(parent); ok {
        p.mu.Lock()
        if p.err != nil {
            // parent has already been canceled
            child.cancel(false, p.err)
        } else {
            if p.children == nil {
                p.children = make(map[canceler]struct{})
            }
p.children[child] = struct {}{}  这里存入父子关系的逻辑
        }
        p.mu.Unlock()
    } else {
        ...
    }
}

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
    if err == nil {
        panic("context: internal error: missing cancel error")
    }
    ...
    
    这里就是将这个父节点下的所有子ctx全部取消掉,注意这里是直接是调用的cancel方法
    所有是一个递归函数,如果这个子ctx下面还有子ctx,则会递归全部进行cancel掉
    for child := range c.children {
        // NOTE: acquiring the child's lock while holding parent's lock.
        child.cancel(false, err)
    }
    
    ...

    if removeFromParent {
        removeChild(c.Context, c)  从父节点将子Ctx摘除
    }
}

观察到上面一个有趣的现象,就是如果调用的是 CancelFunc, 则执行的 cancel 函数的 removeFromParent 为 true,但是cancel内部递归调用的时候,child.cancel 的removeFromParent参数是false

这说明当 parentCtx 执行 cancel 函数时, 只会将其子节点从自己身上摘除,不会将子节点的子节点从子节点摘除

函数 panic 的输出是怎么来的 ?

panic(errors.New("hello")) 会输出 "hello" 字符串

实际上 panic 会去函数体找一个对应结构体的 Error 方法,其输出的字符串就是 Error 方法返回的字符串

go 复制代码
str := "s23"
num, err := strconv.Atoi(str)
if err != nil {
    panic(err)
}

对于上面字符串转换成数字的函数调用

其 panic 输出如上所示,这是因为在源码中的 Error 函数做了进一步包装

go中的error怎么比较相等

go 复制代码
func main() {
    s := "redigo: nil returned"
    err1 := errors.New(s)
    err2 := errors.New(s)
    if err1 == err2 {
            fmt.Println("err is equal")
    } else {
            fmt.Println("err is not equal")
    }
}
//输出 err is not equal

一个变量的两大数属性就是"类型+值", 所以任何变量在比较相等时,如果 "类型+值" 二者都相等,那么才算真正相等

而 errors.New 每次返回的是结构体errorString 变量的地址,每一次返回的都是新值,所以接口变量 err1 和 err2 存储的值并不相等,所以二者并不相等。

go 复制代码
// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
    return &errorString{text}
}

errors.Is:当多层调用的错误被一次次的包装起来,我们在调用链上游拿到的错误如何判断是否是底层的某个错误,Is 方法递归调用 Unwrap 并判断每一层的 err 是否相等,如果有任何一层的 err 和传入的目标错误相等则返回 true

errors.As: 这个和上面的 errors.Is 大体上是一样的,区别在于 Is 是严格判断相等,即两个 error 是否相等。 As 则是判断类型是否相同,并提取第一个符合目标类型的错误,用来统一处理某一类错误。

参看 errors的基本用法

其他

判断下面程序的输出

go 复制代码
slice := []int{0, 10, 20, 30}
mp := make(map[int]*int)

for k, v := range slice {
    mp[k] = &v           //这个 v 就是短变量(或者称为中间变量)
}

for k, v := range mp {
    fmt.Println(k, *v)
}

在上面的转换过程中,&v 实际上是对循环内部同一个短变量的取值,因此 mp 里面存储的其实都是同一个地址,这个地址中的值实际上是最后一次循环赋的值

暂时无法在飞书文档外展示此内容

所以传指针最后 map 里面的 value 都是同一个值 (就是这个短变量的地址值),但是传值最后结果就符合预期

三、Go 源码

为什么 golang 没有整数类型的大小比较函数 Min 和 Max

不要乱用 math.Minmath.Max,参看这篇文章

动态类型语言:如 Python、Ruby、PHP、以及 JavaScript 等

静态类型语言:如 Java、C 和 C++ 以及 Golang 等

动态类型语言不需要关心变量的类型,而静态类型语言一般有类型隐式转换

On a 64-bit platform, the Go float64 type can only represent integral values between 0 and ±2^53. When int64 values outside this range are converted to float64 values, the converted values don't always match the input values. Consider the following example:

go 复制代码
x := int64(1 << 60)
y := x + 127
fx, fy := float64(x), float64(y)

fmt.Printf("x: %f; y: %f\n", fx, fy)
fmt.Printf("x == y => %t\n", fx == fy) 

输出:
x: 1152921504606846976.000000; y: 1152921504606846976.000000
x == y => true

五、面试题

函数调用时参数的入栈顺序

答:

1、首先:在函数调用过程中,最先入栈的是调用函数处的下一条指令的地址,这样函数调用完成后返回到该地址继续执行,这个地址是很重要的,然后才是函数的参数,函数的内部局部变量。调用结束,将栈内容一一出栈,最后栈顶指向了开始存储的指令地址,主函数继续从这里开始运行。

2、其次:函数调用参数的入栈顺序和C语言支持变长参数有关,比如 printf 函数就支持变长参数,也即 void printf(const char *fmt,......),这个时候并不知道参数有多少个,如果从左向右入栈,那么 fmt 就在栈底,该参数的位置无法通过栈顶指针偏移来确定,因为不知道栈顶和栈底之间有多少个参数,大小是多少,无法确定,但是,如果从右向左入栈的话,fmt 参数就在栈顶的位置,通过这个固定的普通参数就可以通过偏移逐一寻址到后续参数的地址

总结:被调用函数是通过栈指针加上偏移值这样相对寻址的方式来定位到自己的参数和返回值的,这样由下至上正好先找到第一个参数,再找到第二个参数。所以参数和返回值采用由右至左的入栈顺序比较合适

127.0.0.1 和 localhost 和 本机IP三者的区别

答:环回地址 是主机用于向自身发送通信的一个特殊地址(也就是一个特殊的目的地址)

可以这么说:同一台主机上的两项服务若使用环回地址而非分配的主机地址,就可以绕开 TCP/IP 协议栈的下层。(也就是说:不用再通过什么链路层,物理层,以太网传出去了,而是可以直接在自己的网络层,运输层进行处理了)

IPv4 的环回地址为:127.0.0.0 到 127.255.255.255 都是环回地址(只是有两个特殊的保留),此地址中的任何地址都不会出现在网络中

网络号为127的地址根本就不是一个网络地址(因为产生的IP数据报就不会到达外部网络接口中,是不离开主机的包)

当操作系统初始化本机的 TCP/IP 协议栈时,设置协议栈本身的IP地址为 127.0.0.1(保留地址),并注入路由表。当IP层接收到目的地址为127.0.0.1(准确的说是:网络号为127的IP)的数据包时,不调用网卡驱动进行二次封装,而是立即转发到本机IP层进行处理,由于不涉及底层操作。因此,ping 127.0.0.1一般作为测试本机 TCP/IP 协议栈正常与否的判断之一

localhost 首先是一个域名(如同:www.baidu.com),也是本机地址,它可以被配置为任意的IP地址(也就是说,可以通过 hosts 这个文件进行更改的),不过通常情况下都指向:

IPv4:表示 127.0.0.1

查看 Hosts 文件位于 /etc/hosts

本机IP:我们可以理解为本机有三块网卡,一块网卡叫做 loopback(虚拟网卡),一块叫做 ethernet(有线网卡),一块叫做 wlan(你的无线网卡)

什么是逃逸分析

答:在 C 语言中,可以使用 mallocfree 手动在堆上分配和回收内存。Go 语言中,堆内存是通过垃圾回收机制自动管理的,无需开发者指定。那么,Go 编译器怎么知道某个变量需要分配在栈上,还是堆上呢?编译器决定内存分配位置的方式,就称之为逃逸分析(escape analysis)。逃逸分析由编译器完成,作用于编译阶段

函数传参何时传值何时传指针?

答:传值会拷贝整个对象,而传指针只会拷贝指针地址,指向的对象是同一个。传指针可以减少值的拷贝,但是会导致内存分配逃逸到堆中,增加垃圾回收(GC)的负担。在对象频繁创建和删除的场景下,传递指针导致的 GC 开销可能会严重影响性能。

一般情况下,对于需要修改原对象值,或占用内存比较大的结构体,选择传指针;对于只读的占用内存较小的结构体,直接传值能够获得更好的性能。

控制并发的两种方法:使用 WaitGroup、使用Context

WaitGroup 适用场景:把一个工作拆分成多个 worker 一起执行,且这些 worker 都是独立的,不相关的

go 复制代码
var wg sync.WaitGroup

wg.Add(2)

go func() {
    time.Sleep(2 * time.Second)
    fmt.Println("job 1 done")
    wg.Done()
}()

go func() {
    time.Sleep(1 * time.Second)
    fmt.Println("job 2 done")
    wg.Done()
}()

wg.Wait()  //等待所有的子协程完成之后才结束主协程
fmt.Println("All done")

但是使用 WaitGroup 不能主动停止 Goroutine 的执行,利用 channel + select 可以解决这个问题

go 复制代码
stop := make(chan bool)

go func() {
    for {
        select {
        case <-stop:
            fmt.Println("got the stop channel")
            return
        default:
            fmt.Println("still working")
            time.Sleep(time.Second)
        }
    }
}()

time.Sleep(5 * time.Second)
fmt.Println("stop the goroutine")
stop <- true
time.Sleep(5 * time.Second)

上面方式仍然存在缺点就是,怎么停止多个 Goroutine, 或者如果 Goroutine 中还有 Goroutine,怎么让其都停止执行,这就需要使用 Context

将上面改成使用 Context

go 复制代码
ctx, cancel := context.WithCancel(context.Background()) //返回根节点

go func() {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("got the stop channel")
            return
        default:
            fmt.Println("still working")
            time.Sleep(time.Second)
        }
    }
}()

time.Sleep(5 * time.Second)
fmt.Println("stop the goroutine")
cancel()
time.Sleep(5 * time.Second)
相关推荐
Swift社区1 小时前
在 Swift 中实现字符串分割问题:以字典中的单词构造句子
开发语言·ios·swift
没头脑的ht1 小时前
Swift内存访问冲突
开发语言·ios·swift
没头脑的ht1 小时前
Swift闭包的本质
开发语言·ios·swift
wjs20241 小时前
Swift 数组
开发语言
stm 学习ing2 小时前
FPGA 第十讲 避免latch的产生
c语言·开发语言·单片机·嵌入式硬件·fpga开发·fpga
Estar.Lee2 小时前
查手机号归属地免费API接口教程
android·网络·后端·网络协议·tcp/ip·oneapi
湫ccc3 小时前
《Python基础》之字符串格式化输出
开发语言·python
Red Red3 小时前
网安基础知识|IDS入侵检测系统|IPS入侵防御系统|堡垒机|VPN|EDR|CC防御|云安全-VDC/VPC|安全服务
网络·笔记·学习·安全·web安全
mqiqe4 小时前
Python MySQL通过Binlog 获取变更记录 恢复数据
开发语言·python·mysql