【笔记】Go语言学习笔记

目录

概念

基础语法

并发编程

项目相关

报错案例

实战项目

概念

1.Go语言最主要的特性

  • 自动垃圾回收
  • 更丰富的内置类型
  • 函数多返回值
  • 错误处理
  • 匿名函数和闭包
  • 类型和接口
  • 并发编程
  • 反射
  • 语言交互性

2.Go语言很可能是第一个将代码风格强制统一的语言,

  • Go语言要求public的变量必须以大写字母开头,private变量则以小写字母开头,这种做法不仅免除了public、private关键字,更重要的是统一了命名风格
  • Go语言对{}应该怎么写进行了强制,比如以下风格是正确的:
    1
    2
    3
    if expression{
    ...
    }
    而以下风格是错误的:
    1
    2
    3
    4
    if expression
    {
    ...
    }

3.xorm是一个简单而强大的Go语言ORM库,经过它可使数据库操做很是简便。xorm的目标并非让你彻底不去学习SQL,咱们认为SQL并不会为ORM所替代,可是ORM将能够解决绝大部分的简单SQL需求。xorm支持两种风格的混用

  • orm是一种封装数据库操作的框架,使用它可以简化sql操作

4.Go的内存回收整体阶段:

  • Goff to Gmark,每次的gcstart都是满足gc_triger时由mallocgc触发,整个的启动过程是stop the world的,这个过程启动了所有的GC工作协程,进入GCMark状态使能写屏障,启动gcController。简单来说就是确定GCroot相关的goroutine。
  • Gmark,这个阶段是标记阶段,拿到准备好的goroutine来做标记,但是一开始就gopark当前的goroutine(上个阶段),直到被gccontroller的findRunnableGCWorker唤醒。
    唤醒后进入标记阶段,每个worker都去gc-work中拿节点(节点置黑),然后处理当前节点看有没有指针和没标记的对象,继续入队子节点(灰化节点),直到队列为空。
  • Gmarktermination,标记结束后调用gcMarkDone
  • Gsweep,具体的清除行为,有多个时机可以出发Gsweep,如果是并发清除的话,需要先回收未被标记的heap区,然后唤醒进行sweep的 goroutine。

3.main()函数不能带参数,也不能有返回值

4.go get -u xx的-u含义是,本地已经有一个名为xx的第三方库了,如果直接执行go get xx的话会什么都不执行,因为编译器会识别本地已经安装了所以直接跳过,而加上-u则会去拉取xx的最新版本

5.Go相比其他语言的优势

  • 直接编译成二进制(和c/c++一样),没有虚拟化损失
  • 一次编码适用于多种平台(和java一样)
  • 超强并发支持能力与并发易用性

基础语法

1.字符串修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
s2 := "白萝卜"

# rune为utf8类型主要用于汉字等
s3 := []rune(s2)

# 单独字符修改
s3[0] = '红'

# 把rune切片强悍强制转换成字符串
fmt.Println(s3)

# 把单个字符加入字符串
s3 += string(h)

# 单独字符h为int32类型
# 字符串为string类型

2.for循环

1
2
3
4
5
for _, c := range s{
...
}

# _返回索引,c返回字符

3.定义数组

1
2
3
4
5
6
7
8
9
10
var a [3]string = [3]string{"nihao", "jhj", "ir"} 
var q [3]int = [3]int{1, 2, 3}
var r [3]int = [3]int{1, 2}

q := [3]int{1, 2, 3}

a := [2]int{1, 2}
b := [...]int{1, 2}
c := [2]int{1, 3}
fmt.Println(a == b, a == c, b == c)

4.多维切片

1
2
3
4
5
# 声明一个二维整型切片并赋值
slice := [][]int{{10}, {100, 200}}

# 为第一个切片追加值为 20 的元素
slice[0] = append(slice[0], 20)

5.多重返回值

  • 定义
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    func getName()(firstName, middleName, lastName, nickName string){ 
    return "May", "M", "Chen", "Babe"
    }

    # 与上方等价
    func getName()(firstName, middleName, lastName, nickName string){
    firstName = "May"
    middleName = "M"
    lastName = "Chen"
    nickName = "Babe"
    return
    }
  • 调用
    1
    2
    3
    4
    fn, mn, ln, nn := getName() 

    # 只需要lastName就用这个形式
    _, _, ln, _ := getName()

6.=是赋值,:=是声明变量并赋值,注意函数外和const常量都必须用var xx的形式,不能用:=

1
2
3
4
5
6
7
8
//使用=赋值之前必须使用先var声明例如:
var a
a=100
var b = 100
var c int = 100

//:= 是声明并赋值,并且系统自动推断类型,不需要var关键字
d := 100

7.函数的写法

1
2
3
4
func test(x, y int, s string) (int, string) {
n := x + y
return n, fmt.Sprintf(s, n)
}
  • 使用关键字func定义函数
  • 参数类型要在参数名后面
  • 当两个或多个连续的函数命名参数是同一类型,则只需写最后一个参数的参数类型
  • 多返回值必须用括号,单返回值可以不用,如果没有返回值的话可以直接不写,注意只有没有返回值的情况下才能不写返回值类型

8.按地址传递的函数写法

  • 定义
    1
    2
    3
    4
    5
    6
    func swap(x, y *int) {
    var temp int
    temp = *a
    *a = *b
    *b = temp
    }
  • 使用
    1
    2
    3
    a := 10
    b := 1
    swap(&a, &b)
  • 即不可以像c++一样函数只管用引用格式接受就行了,而是main()里面传地址然后函数用指针形式接受

9.nil是引用类型的默认值

10.常用占位符

  • %v:变量的默认格式
  • %p:变量的地址,十六进制表示,前缀0x
  • %d:变量的十进制表示

11.for循环的写法

  • C++风格写法
    1
    2
    3
    for i := 0; i < 10; i++ {
    sum += i
    }
  • py风格写法
    1
    2
    3
    4
    5
    6
    var temple = []string{token, timestamp, nonce}
    var codeString string
    for _, v := range temple { //_是下标,v是元素
    codeString += v
    }
    //最终codeString是token+timestamp+nonce

12.if else写法

1
2
3
4
5
if codeString == signature {
c.Writer.Write([]byte(echostr))
} else {
log.Println("api验证失败")
}

13.(函数也是一模一样)在Go中名字是以大写字母开头的表示该变量就是已导出的,可以在main中用包名.变量名访问,否则不行

  • 大写的变量对于同一个包的其他go文件而言也是可见的,即使这个大写变量是在局部定义的,它也相当于一个全局变量

14.类型转换示例,Go不支持不同类型隐式转换

  • int转string,strconv.Itoa
    1
    func Itoa(i int) string
  • string转int,strconv.Atoi
    1
    func Atoi(s string) (int, error)
  • 数值类型转换
    1
    2
    3
    4
    d1 := 24.1
    i := int(d1)
    f := float(i)
    u := uint(f)
  • 字符串转time类型
    1
    2
    3
    4
    tmp, _ := time.LoadLocation("Asia/Shanghai") //加时区
    tmpTime, err := time.ParseInLocation("2006-01-02 15:04:05", tmpStr, tmp)

    //tmpStr是string类型,tmpTime是time类型
  • time类型转时间戳
    1
    2
    //startTime是time类型,startUnix是int64类型的时间戳
    startUnix := startTime.Unix()
  • 时间戳转time类型
    1
    2
    //upInt是int64类型的时间戳,uptime是time类型
    upTime := time.Unix(upInt, 0)

15.切片

  • 切片并不存储任何数据,切片就像数组的引用,修改切片会影响数组
  • 下列代码表示b包含a中下标为1、2、3的元素
    1
    b := a[1:4]
  • 切片下界默认值为0,上界默认值是切片对象的总长度
  • 切片和数组的重要区别
    1
    2
    3
    s := []int{2, 3, 5, 7, 11, 13} //此时的s是数组,长度固定,只能另外定义一个切片来对其操作

    s := []int{2, 3, 5, 7, 11, 13} //此时的s是切片,即后续完全可以用s=s[2:]这样来改变长度
  • 切片的长度lens()是该切片实际含有的元素数目,容量caps()是该切片的第一个元素到底层数组的末尾元素之间的元素数目
  • 切片的零值是nil
  • 声明一个空切片
    1
    var s []int
  • 用make创建切片
    1
    2
    a := make([]int, 5, 10)  //a是一个长度为5元素值为0的int数组的切片,预设cap是10

  • 二维数组用切片的切片来实现
  • 往切片中追加元素
    1
    2
    3
    4
    5
    var temple []int
    for i := 0; i < 10; i++ {
    fmt.Println(i)
    temple = append(temple, i*10)
    }

16.映射

  • 定义
    1
    var m map[键类型]值类型
  • 使用
    1
    fmt.Println(m[键名1])
  • 删除某个键值对
    1
    delete(m, 键名1)
  • 检测某个键是否存在
    1
    2
    3
    v, ok := m[键名1]
    //若存在,则v是值,ok为true
    //若不存在,则v是0,ok为false

17.闭包

  • Go函数可以是一个闭包,闭包是一个函数值,它引用了其函数体之外的变量
  • 示例,函数adder返回一个闭包,每个闭包都被绑定在其各自的sum 变量上
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    func adder() func(int) int {
    sum := 0
    return func(x int) int {
    sum += x
    return sum
    }
    }
    func main() {
    pos, neg := adder(), adder()
    for i := 0; i < 10; i++ {
    fmt.Println(
    pos(i),
    neg(-2*i),
    )
    }
    }

18.go中没有类,只有结构体所以结构体方法要如下

  • 定义
    1
    2
    3
    4
    5
    6
    7
    type Circle struct {
    radius float64
    }

    func (c Circle) getArea() float64 { //(c Circle)表示该方法是Circle的方法
    return 3.14 * c.radius * c.radius //c.radius 即为 Circle 类型对象中的属性
    }
  • 使用
    1
    2
    3
    4
    5
    func main() {
    var c1 Circle
    c1.radius = 10.00
    fmt.Println( c1.getArea())
    }

19.批量定义常量用如下形式

1
2
3
4
5
6
7
8
const (
USER_NAME = "root"
PASS_WORD = "root"
HOST = "127.0.0.1"
PORT = "3306"
DATABASE = "demo"
CHARSET = "utf8"
)

20.反射

  • 结构体使用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    type OrderList struct {
    Id int `json:"id"` //json:"id"就是tag
    OddNum string `json:"odd_num"` //json:"odd_num"就是tag
    }

    //声明多个结构体要在type后加括号
    type(
    OrderList1 struct {
    Id int `json:"id"` //json:"id"就是tag
    OddNum string `json:"odd_num"` //json:"odd_num"就是tag
    }
    OrderList2 struct {
    Id int `json:"id"` //json:"id"就是tag
    OddNum string `json:"odd_num"` //json:"odd_num"就是tag
    }

    )

21.panic的用法

  • 调用panic()后会打印里面的信息,然后终止程序运行
    1
    panic()

22.error也可以作为返回类型,nil可以作为它的值

23.interface{} 就是一个空接口,所有类型都实现了这个接口,所以它可以代表所有类型,所以使用空接口实现可以保存任意值的字典。

  • 使用例子
    1
    2
    3
    4
    data := map[string]interface{} {
    "cal_detail" : calDetail,
    "cal_adjust_date":calAdjust.Date,
    }
  • 一个带有switch的经典案例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    func main() {
    m := make(map[string]interface{})
    m["int"] = 123
    m["string"] = "hello"
    m["bool"] = true

    for _, v := range m {
    switch v.(type) {
    case string:
    fmt.Println(v, "is string")
    case int:
    fmt.Println(v, "is int")
    default:
    fmt.Println(v, "is other")
    }
    }
    fmt.Println(m)
    }
  • 将interface{}转换成string或int类型
    1
    2
    3
    4
    //a是interface{}类型

    str_a := a.(string)
    int_a := a.(int)

24.用断言判断类型,主要用于判断interface{}类型

1
value, ok = x.(T) //若x是T类型,则value为x的值,ok为true,否则ok为false,value仍为x的值

25.Go的字符串操作

  • 截取字符串
    1
    s2 := s1[start:end] //若start为空则默认为0,若end为空则默认为s1.len()-1
  • 找到某个字符的最后出现下标
    1
    index := strings.LastIndex("/", dirName)
  • 判断s是否包含子字符串substr
    1
    flag := strings.Contains(s, substr)
  • 返回s中子字符串substr出现的下标,没有则返回-1(Contains方法就是调用Index实现的)
    1
    index := strings.Index(s, substr)
  • 如果后缀是”\r\n”那么就去掉后缀
    1
    str = strings.TrimSuffix(str, "\r\n")

26.对字符串用特定字符串分割

1
2
3
4
str := "1223-6"
strTmp := strings.Split(str, "-")
//strTmp[0]为"1223"
//strTmp[1]为"6"

27.map的一些使用事项

  • 必须用memoryOne := map[string]bool{}这样初始化声明后,才能用memoryOne[“001”] = false来添加值,不能仅仅只是var
  • 判断key是否存在
    1
    2
    3
    _, ok := memoryOne["001"]

    # 存在的话ok会是true,不存在的话ok会是false

28.因为go可以实现多重赋值,所以交换i,j的功能可以由以下一行代码实现

1
i, j = j, i 

29.const声明的常量可以不加类型,且常量右边应该是编译期就能获取的值(运算符里只有>>和<<)

30.atoi是一个自增常量,在遇到下一个const的时候会自动置0,不然每出现一次下一次它的值就为+1

31.go的bool类型不支持自动类型转换,必须是一个逻辑值

32.float32等价于c的float,float64等价于c的double

33.不能直接用str[1]=’x’这样的方式去修改一个string的值

  • 以byte遍历,byte的本质就是一个8位的二进制数字,String转byte[],就是先转ASCII码,然后再转成二进制
    1
    2
    3
    for i := 0; i < len(str); i++ {
    ch := str[i] // 依据下标取字符串中的字符,类型为byte fmt.Println(i, ch)
    }
  • 以rune遍历
    1
    2
    3
    for i, ch := range str {
    fmt.Println(i, ch) //ch的类型为rune
    }

34.往一个切片末尾添加另一个切片所有元素的方法

1
2
3
4
mySlice = append(mySlice, mySlice2...) //注意如果第二个参数是切片而不是值的话,一定要加上...

//等同于
mySlice = append(mySlice, 8, 9, 10)

35.基于老切片生成新切片时,可以超过老切片的len,只要不超过老切片的cap就不会报错

36.goto跳转语句

  • 用xx:声明
  • 用goto xx跳转

37.不定参数,只能在函数参数列表里出现,并且要是最后一个参数(下面的args实际类型时[]int)

1
2
3
4
5
6
7
8
9
10
11
func main() {
box(1,2,1,3,1)
return
}

func box(args... int){
other(args...) //传到另一个参数
for _, arg := range args{
fmt.Println(arg)
}
}

38.切片就相当于vector,基于数组生成的切片其实是把数组复制了一份,更改切片的值并不影响数组

  • 切片同vector原理一样也有指针、长度len、容量cap(用make初始化的时候cap=len)
  • 用var temple []int = []int{1,2}这样没指定长度生成的数组也是切片
  • 切片在传参进函数的时候,默认是按引用传递

39.defer语句的调用是遵照先进后出的原则,即最后一个defer语句将最先被执行

39.匿名函数由一个不带函数名的函数声明和函数体组成

1
2
3
func(a, b int, z float64) bool {
return a*b < int(z)
}

40.如何在运行时检查变量类型

  • 类型开关(Type Switch)是在运行时检查变量类型的最佳方式。类型开关按类型而不是值来评估变量。每个Switch 至少包含一个case用作条件语句,如果没有一个case 为真,则执行default

41.cap()函数的适用范围

  • 数组
  • 切片
  • 带缓冲的channel

42.Go中的new是将一个该类型的空间置零,返回值是指针类型

  • 即在new的时候就已经分配内存了,不用等到定义时才分配内存
    1
    c := new(int)

43.三个打印函数

  • Printf()是标准输出,一般是屏幕,也可以重定向。
  • Sprintf()是把格式化字符串输出到指定的字符串中。
  • Fprintf()是把格式化字符串输出到文件中

44.slice的底层原理

  • 切片是基于数组实现的,它的底层是数组,它自己本身非常小,可以理解为对底层数组的抽象。因为基于数组实现,所以它的底层的内存是连续分配的,效率非常高,还可以通过索引获得数据
  • 扩容的时候也是和vector一样,重新开辟内存然后把旧值复制过来再销毁旧内存
  • 扩容策略
    • 首先判断,如果新申请容量大于2倍的旧容量,最终容量就是新申请的容量
    • 否则判断,如果旧切片的长度小于1024,则最终容量就是旧容量的两倍
    • 否则判断,如果旧切片长度大于等于1024,则最终容量从旧容量开始循环增加原来的1/4, 直到最终容量大于等于新申请的容量
    • 如果最终容量计算值溢出,则最终容量就是新申请容量

45.go中默认按值传递和按地址传递的类型

  • 按值传递:int、string、struct(属于非引用类型),在函数中就无法修改原内容数据
  • 按地址传递:指针、map、slice、chan(属于引用类型),在函数中可以修改原内容数据

46.map的底层原理

  • Golang中map的底层实现是一个散列表,因此实现map的过程实际上就是实现散表的过程。在这个散列表中,主要出现的结构体有两个,一个叫hmap,一个叫bmap,通常叫其bucket。
  • 扩容机制
    • 双倍扩容:扩容采取了一种称为“渐进式”的方式,原有的key并不会一次性搬迁完毕,每次最多只会搬迁2 个bucket
    • 等量扩容:重新排列,极端情况下,重新排列也解决不了,map存储就会蜕变成链表,性能大大降低,此时哈希因子hash0的设置,可以降低此类极端场景的发生
  • 查找
    • Go语言中map采用的是哈希查找表,由一个key通过哈希函数得到哈希值,64位系统中就生成一个64bit的哈希值,由这个哈希值将key对应存到不同的桶(bucket)中,当有多个哈希映射到相同的的桶中时,使用链表解决哈希冲突
    • 细节:key经过hash后共64位,根据hmap中B的值,计算它到底要落在哪个桶时,桶的数量为2^B,如B=5,那么用64位最后5位表示第几号桶,在用hash值的高8位确定在bucket中的存储位置,当前bmap中的bucket未找到,则查询对应的overflow bucket,对应位置有数据则对比完整的哈希值,确定是否是要查找的数据。如果当前map处于数据搬移状态,则优先从老buckets查找。

46.slice类型

  • 本质是对底层数组的一段内容的引用
  • 一个slice包含数组、长度(len)、容量(cap)三个元素
  • 两个slice类型,如果用=的话,那么就会共用底层数组。不想共用的话就要用make分别声明后,再用copy(目标, 原数据)来拷贝数据

47.channel

  • 一个channel包含长度和类型两个元素
  • 读 v:=<-ch
  • 写 ch<-v
  • 关闭 close(ch)
  • 长度为0的表示不带buffer的channel(读优先于写),不为0表示带buffer的channel(写优先于读,存在数据拷贝的操作)

48.interface

  • 一个interface没有值,它只能用来确认实现了哪些方法,然后这些方法的定义也不包含
  • 实质上就是一个n个接口声明放在一起的结构

49.go的context是用于并发用途的,它本质也是一个接口

50.http/net等标准库都是服务端接受到一个请求时,都会新建一个协程去执行它

51.go成员函数可以有以下两种写法

  • 1
    2
    3
    func (u user) xx(){
    }
    //这种对值的修改就只能局限于在user这个结构体里面
  • 指针
    1
    2
    3
    func (u *user) xx(){
    }
    //这种对值的修改就没有限制

52.以下这种类型转换其实就是断言

1
var b = a.(int)
  • 以下这种方式会返回a的类型名
    1
    string str = a.(type)

测试

  1. Go Convey 是什么?一般用来做什么?
  • 是一个支持Golang的单元测试框架
  • 能够自动监控文件修改并启动测试,并可以将测试结果实时输出到Web界面
  • 提供了丰富的断言简化测试用例的编写

一些库的使用

go-commons-pool库,用于实现连接池

  • 引入
    1
    2
    //在你的结构体内定义这个成员
    peerConnection *pool.ObjectPool
  • 实现
    • 新建一个cluster_pool.go文件
    • 里面写一个factory类去实现pool.PooledObject接口,实现MakeObject和DestroyObject方法即可
  • 操作
    • 定义
      1
      xx.peerConnection, err = MakeObject(xx)
    • 从连接池里拿一个连接
      1
      object := pool.BorrowObject(context.Background())
    • 将拿的连接还回去
      1
      pool.returnObject(context.Background(), client)

bufio库

1.从buffer里面读一行以’\n’为换行表示的[]byte

1
2
var bufReader *bufio.Reader
msg, err = bufReader.ReadBytes('\n')

2.从buffer里面取内容,直到填满msg

1
2
msg = make([]byte, 2)
_, err := io.ReadFull(bufReader, msg)

http库

1.应用demo

  • 发送Get请求(接受一个返回的json)
    1
    2
    3
    4
    5
    6
    7
    8
    type responseGet struct {
    AccessToken string `json:"access_token"`
    ExpiresIn int `json:"expires_in"`
    }
    var getRes responseGet
    responseGet, _ := http.Get(urlGet) //此处responseGet是*http.response类型

    json.NewDecoder(responseGet.Body).Decode(&getRes) //用结构体getRes来对应存Get返回的数据
  • 发送带json的Post请求
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    data := make(map[string]interface{})
    data["path"] = request.Path
    data["scene"] = "a"
    bytesData, _ := json.Marshal(data) //构造成json格式
    reader := bytes.NewReader(bytesData) //构造新reader对象
    requestPost, err := http.NewRequest("POST", urlPost, reader) //构造request对象,此处requestPost是*http.Request类型
    requestPost.Header.Set("Content-Type", "application/json;charset=UTF-8") //设置请求头

    client := http.Client{} //设置client为发起请求的客户端
    resp, err := client.Do(requestPost) //发起请求,此处resp是*http.response类型

    respBytes, err := ioutil.ReadAll(resp.Body) //将响应的数据读出来,respBytes是二进制数组

2.*http.request的数据处理

  • 新增cookie
    1
    2
    cookieToken := http.Cookie{Name:"token", Value:token, Path:"/"}
    req.AddCookie(cookieToken)
  • 读取cookie
    1
    token, err := req.Cookie("token")
  • 从header里取数据
    1
    2
    # req是*http.Request类型
    token := req.Header.Get("Authorization")
  • get请求里取传入的参数
    1
    param, err := url.ParseQuery(req.URL.RawQuery)

3.http接口的写法

1
2
3
4
5
6
7
8
9
10
11
func main(){
http.HandleFunc("/get_one", )
err := http.ListenAndServe(":12345", nil)
if err != nil{
log.Fatal("Err:", err)
}
}

func xx(rw http.ResponseWriter, req *http.request){
//处理逻辑
}

5.*http.response的读数据方式

  • 常规处理
    1
    2
    3
    4
    # resp是*http.response类型
    respBytes, err := ioutil.ReadAll(resp.Body)
    respString = string(respBytes)
    fmt.Println(respString)
  • 直接通过json转成结构体实例
    1
    2
    # resp是*http.response类型,getRes是满足json格式要求的结构体的实例
    json.NewDecoder(resp.Body).Decode(&getRes)

time库

1.time.sleep()默认单位是纳秒,即1000,000,000才等于休眠1秒

2.time.time类型的变量x可以用x.Format(“2006-01-02 15:04:05”)来转换成符合格式的字符串

1
2
3
4
5
# time1是值为2022-01-02 15:04:05的time.time类型变量
str1 := time1.Format("2006-01-02 15:04:05")
str2 := time1.Format("2006-01-02")

# 此时str1和str2都是string类型,其中str1="2022-01-02 15:04:05",str2="2022-01-02"

3.time类型进行加减

1
2
3
4
h, _ := time.ParseDuration("+1h")
m, _ := time.ParseDuration("-10m")
time1.Add(h) //加1小时
time1.Add(m) //减10分钟

4.time类型进行比较

1
2
3
time1.Before(time2) //如果time1早于time2返回true,否则返回false
time1.After(time2) //如果time1晚于time2返回true,否则返回false
time1.Equl(time2) //如果time1等于time2返回true,否则返回false

5.time.time类型的加减

1
2
rightTime := data.Add(time.Duration(10)*time.Second)
# 表示rightTime是data的时间+10s

rand库

1.生成随机数

  • rand.Intn(n)和rand.Int() % n都会生成一个0到n-1的随机int数
  • rand.perm(n)会生成一个以0到n-1为元素的随机排序列表

2.用时间作随机数种子时,需要休眠1纳秒来避免程序运行过快导致的重复

1
2
time.Sleep(1)
rand.Seed(time.Now().UnixNano())

3.打乱一个数组

1
2
3
rand.Shuffle(len(arr), func(i, j int) {
arr[i], arr[j] = arr[j], arr[i]
})

base64库

1.如何将二进制数组转换成base64表示的字符串(一般用于图片和文件)

1
2
3
sEnc := base64.StdEncoding.EncodeToString(respBytes)
//respBytes是二进制数组,sEnc就是base64编码的string类型
//respBytes, _ := ioutil.ReadFile("myzipfile.zip")也可以这样产生respBytes
  • 解码base64
    1
    sDec, _ := b64.StdEncoding.DecodeString(sEnc)

sort库

1.将一个[]xx数组进行排序步骤

  • 将数组声明成一个类型
    1
    type tar []xx
  • 为这个类型写三个子方法Len(),Less(),Swap()
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    func (t tar) Len() int{
    return len(tar)
    }

    func (t tar) Less(i int, j int) bool{
    return t[i] < t[j]
    }

    func (t tar) Swap(i int, j int) {
    t[i], t[j] = t[j], t[i]
    return
    }
  • 使用排序
    1
    2
    var element tar
    sort.Sort(element)

2.简单排序用法

1
2
3
4
var intervals = [][]float64{{999.0,1},{726.1,3},{1000,2.2}}

sort.Slice(intervals, func(i,j int) bool {return intervals[i][1] < intervals[j][1]})
# 结果会把intervals排序成按第二个元素升序顺序

3.内置的sort函数

  • sort.Ints([]int)
  • 快速查找,下面demo为找到第一个大于等于target的下标
    1
    2
    3
    4
    target := 2
    idx := sort.Search(len(m.nodeHashes), func(i int) bool {
    return m.nodeHashes[i] >= target
    })

log库

  • 在日志里打印err信息并终止程序
    1
    2
    3
    if err != nil{
    log.Fatal("Err:", err)
    }

sync库

1.sync.map的使用

  • range(),需要传进去一个匿名函数,用来对每一个k-v施加操作,注意这个遍历开始的位置都是随机的
    1
    2
    3
    4
    5
    //range里面传方法表示对每个k-v执行一般这个方法,返回true时就继续执行,直到返回false或者遍历结束,这里用来获取sync.map的k-v数
    dict.m.Range(func(key, val interface{}) bool {
    cnt++
    return true
    })

xlsx库

1.引包

1
github.com/tealeg/xlsx

接口

1.如果两个接口有相同的方法列表,那么他们就是等价的,可以相互赋值。如果接口A的方法列表是接口B的方法列表的子集,那么接口B可以赋值给接口A,B多余的方法会被忽略。

2.接口查询是否成功,要在运行期才能够确定

并发编程

1.在调用函数前加一个go关键字就是并发去执行该函数(使用的是协程)

  • 当被调用的函数返回时,这个goroutine也自动结束了。需要注意的是,如果这个函数有返回值,那么这个返回值会被丢弃
    1
    2
    3
    4
    5
    6
    7
    8
    func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
    wg.Add(1) // 启动一个goroutine就登记+1
    go hello(&wg)
    }
    wg.Wait() // 等待所有登记的goroutine都结束,不等待的话一旦main的goroutine退出,会把所有的子goroutine退出
    }

2.chan类型的使用方法

  • 声明,表示resultChan是容量为2的chan int类型
    1
    resultChan := make(chan int, 2)
  • 使用
    • 传值进去
      1
      2
      3
      4
      sum := 1
      resultChan <- sum
      sum++
      resultChan <- sum
    • 取出来
      1
      2
      3
      var sum1, sum2 int
      sum1, sum2 = <- resultChan, <- resultChan
      //因为管道是类似栈一样的先进后出,所以sum1=2,sum2=1

3.停止一个goroutine

1
2
3
4
5
6
7
8
9
10
11
quit = make(chan bool, 1)

go box()

quit <- true

func box(){
tmp := false;
tmp <- quit
if(case) return;
}

4.Go 当中同步锁有什么特点?作用是什么

  • 当一个Goroutine获得了Mutex后,其他Goroutine就只能乖乖的等待,除非该Goroutine释放了该Mutex
  • RWMutex在读锁占用的情况下,会阻止写,但不阻止读RWMutex。在写锁占用情况下,会阻止任何其他Goroutine(无论读和写)进来,整个锁相当于由该Goroutine独占(即读锁占用,只可以读,写锁占用,读和写都不可以)
  • 同步锁的作用是保证资源在使用时的独有性,不会因为并发而导致数据错乱,保证系统的稳定性

5.Go 语言当中Channel(通道)有什么特点,需要注意什么?

  • Go语言中,不要通过共享内存来通信,而要通过通信来实现内存共享。Go的CSP并发模型,中文可以叫做通信顺序进程,是通过goroutine和channel来实现的。channel收发遵循先进先出FIFO的原则
  • 如果给一个nil 的channel 发送数据,会造成永远阻塞
  • 如果从一个nil 的channel 中接收数据,也会造成永久阻塞
  • 给一个已经关闭的channel 发送数据,会引起panic
  • 从一个已经关闭的channel 接收数据,如果缓冲区中为空,则返回一个零值

6.Go 语言当中Channel 缓冲有什么特点?

  • 无缓冲的channel是同步的
  • 有缓冲的channel是非同步的

7.goroutine为什么会这么快?

  • Go 语言标准库提供的所有系统调用操作(当然也包括所有同步IO 操作),都会出让CPU 给其他goroutine。这让轻量级线程的切换管理不依赖于系统的线程和进程,也不依赖于CPU的核心数量

5.当只是用go关键字启动协程,然后不加等待语句主函数就结束的话,协程实际上并没有执行,因为当main()函数返回时,程序退出,且程序并不等待其他goroutine(非主goroutine)结束

6.使用共享内存+锁的方法并发

1
2
3
4
5
6
func Count(lock *sync.Mutex){
lock.Lock()
counter++
fmt.Println(counter)
lock.Unlock()
}

7.<- chan类型在管道里没有数据时,会一直阻塞等待直到有数据。chan <- 写入后,会一直阻塞等待数据被读出去

8.channel的使用

  • 声明
    1
    var ch chan int
  • 声明及定义
    1
    ch := make(chan int)
  • 写入,向channel写入数据通常会导致程序阻塞,直到有其他goroutine从这个channel中读取数据
    1
    ch <- value
  • 读取,如果channel之前没有写入数据,那么从channel中读取数据也会导致程序阻塞,直到channel中被写入数据为止
    1
    value := <-ch 

9.select的使用,类似于switch

1
2
3
4
5
6
7
8
9
10
select{
case <- chan1:
//分支1:如果chan1有数据并且成功读到数据,就进行这个分支(注意这个成功读到的数据会被忽略)
case chan2 <- 1:
//分支2:如果成功向chan2写入数据,则进行该分支
default:
//如果以上都没成功,就进行这个分支
}

//如果case1和case2都满足的话,会去随机执行一个

10.带缓冲的channel

  • 定义及声明,在make时加上第二个参数即可
    1
    2
    c := make(chan int, 1024) 
    //即使没有读取方,写入方也可以一直往channel里写入,在缓冲区被填完之前都不会阻塞。
  • 读取全部缓冲用以下for range形式
    1
    2
    3
    for i := range c {
    fmt.Println("Received:", i)
    }

11.超时机制利用到了select只要一个case为真就往下执行和time库

1
2
3
4
5
6
7
8
9
10
11
12
timeout := make(chan bool, 1)
go func(){
time.Sleep(1e9)
timeout <- true
}

select{
case <-ch:
case <-timeout:
}

//意味当等待1秒后,ch里还是没数据就执行timeout分支

12.单向channel

  • 声明
    1
    2
    var ch2 chan<- float64// ch2是单向channel,只用于写float64数据
    var ch3 <-chan int // ch3是单向channel,只用于读取int数据
  • 基于正常channel初始化成单向
    1
    2
    3
    ch4 := make(chan int) 
    ch5 := <-chan int(ch4) // ch5就是一个单向的读取channel
    ch6 := chan<- int(ch4) // ch6 是一个单向的写入channel

13.关闭channel

  • 直接close
    1
    close(ch) 
  • 用于检测channel是否关闭,同map一样
    1
    x, ok := <-ch 

14.设置使得goroutine可以利用多个cpu核心

  • 获取当前环境cpu数量
    1
    a := runtime.NumCPU()
  • 设置使用a个cpu核心
    1
    runtime.GOMAXPROCS(a)

15.锁

  • sync.Mutex。一个goroutine获得Mutex后,其他goroutine就直到阻塞等待它释放后才能执行
    1
    2
    3
    c := &sync.Mutex{}
    c.Lock()
    defer c.Unlock()
  • sync.RWMutex。读锁被占用后只允许读不允许写,写锁被占用后读写都不允许
    1
    2
    3
    b := &sync.RWMutex{}
    b.Lock()
    defer b.Unlock()

16.全局唯一操作

  • sync.One类型
    1
    2
    3
    var a sync.Once

    a.Do(box()) //表示无论多少个goroutine运行到此行,box()全局中永远只被执行一次

17.ring buffer环形缓冲区实现

  • channel 中使用了ring buffer(环形缓冲区) 来缓存写入的数据。ring buffer 有很多好处,而且非常适合用来实现FIFO 式的固定长度队列
  • recvx 指向最早被读取的数据,sendx 指向再次写入时插入的位置

18.锁Mutex的几种形态

  • mutexLocked —表示互斥锁的锁定状态;
  • mutexWoken —表示从正常模式被从唤醒;
  • mutexStarving —当前的互斥锁进入饥饿状态;
  • waitersCount —当前互斥锁上等待的Goroutine 个数;

19.Mutex正常模式(性能较好)和饥饿模式

  • 正常模式是不公平的,所有等待锁的goroutine按照FIFO(先进先出)顺序等待。但是最新请求的goroutine更容易抢占锁,因为它正在CPU上执行
  • 饥饿模式是公平的,直接由unlock把锁交给等待队列中排在第一位的goroutine ,同时新进来的goroutine不会参与抢锁也不会进入自旋状态,会直接进入等待队列的尾部
    • 饥饿模式的触发条件:当一个goroutine等待锁时间超过1毫秒时,或者当前队列只剩下一个goroutine的时候

20.允许自旋的条件

  • Mutex不处于饥饿模式
  • 当前自旋次数小于最大自旋次数(一般为4)
  • cpu核心数大于1,且有空闲的核心
  • 当前运行的goroutine挂载的核的待运行队列为空,即不存在有别的goroutine等着当前goroutine执行完成

21.读写锁RWMutex的原理

  • 通过记录readerCount 读锁的数量来进行控制,当有一个写锁的时候,会将读锁数量设置为负数1<<30。目的是让新进入的读锁等待之前的写锁释放通知读锁。同样的当有写锁进行抢占时,也会等待之前的读锁都释放完毕,才会开始进行后续的操作。而等写锁释放完之后,会将值重新加上1<<30,并通知刚才新进入的读锁
  • 属于单写多读锁,适用于读多写少的情景
  • 解锁方式属于饥饿模式
  • 一个goroutine可以解Unlock()另一个goroutine的Lock()
  • 变量的0值是一个未锁定状态的互斥锁

22.map类型在大并发情况下是不安全的,在同时读和同时写的时候会退出程序,所以必须加锁

23.对某个结构体使用锁来实现并发安全的方法

  • 定义的时候加上锁成员
    1
    2
    3
    4
    type xx struct{
    sourceArray map[int]interface{}
    sync.RWMutex //用*sync.RWMutex的话,会在初始化的时候创建锁,用sync.RWMutex只会在使用的时候创建锁,这两个方式在具体使用的时候没有区别
    }
  • 使用的时候
    1
    2
    3
    4
    5
    func (e *xx) update(){
    e.Lock() //如果只读的话用e.RLock(),也可以写成e.RWMutex.Lock()效果一模一样
    defer e.Unlock() //e.RLock()对应的用e.RUnlock()
    //业务代码
    }
  • ps:实例化结构体的时候*sync.RWMutex不需要赋值

反射

1.简单获取struct里的变量名和类型及值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Bird struct {
Name string
LifeTime int
}

func main() {
sparrow := &Bird{"sparrow", 3}
s := reflect.ValueOf(sparrow).Elem()
typeName := s.Type()

for i := 0; i < s.NumField(); i++{
f := s.Field(i)
fmt.Println("***")
fmt.Println(i)
fmt.Println(typeName.Field(i).Name)
fmt.Println(f.Type())
fmt.Println(f.Interface())
fmt.Println("***")
}
}

编译相关

1.go run会直接执行编译、链接和运行而不产生中间文件

1
go run main.go

2.go build会执行编译产生一个main.exe文件

1
go buil main.go

项目相关

1.Go语言的项目有三个三个文件夹

  • src文件夹:自己写的代码,第三方库的源代码
  • bin文件夹:产生的二进制可执行文件
  • pkg文件夹:生成的中间缓存文件

2.Go的目录

  • GOROOT指的是go的安装目录
  • GOPATH是添加到系统的环境变量
    • 值可以是一个目录的路径,也可以是包含多个目录的路径,每个目录代表Go的一个工作区

3.新建一个Go项目步骤

  • 新建Go项目
  • 点击右上角“Add Configuration”
  • 再弹出窗口选择+,然后选择“Go Build”
  • 配置中Run kind选择Directory,其他的配置项作用如下(一般不用改)
    • 名称:为本条配置信息的名称,可以自定义,也可以使用系统默认的值;
    • Directory:用来设置 main 包所在的目录,不能为空;
    • Output directory:用来设置编译后生成的可执行文件的存放目录,可以为空,为空时默认不生成可执行文件;
    • Working directory:用来设置程序的运行目录,可以与“Directory”的设置相同,但是不能为空。

4.一个hello world程序

1
2
3
4
5
6
7
package main

import "fmt"

func main(){
fmt.Println("Hello World!")
}

5.安装第三方包,在命令行输入以下命令(注意每个新项目要重新安装一遍)

  • 例如要import的包名是”github.com/taoshihan1991/imaptool/controller”
    1
    go get github.com/taoshihan1991/imaptool/controller

6.导入本地写好的包步骤

  • 已知条件
    • 主体项目名为Capt_Project(package main,主函数要是func main())
    • 有一个包的目录为Capt_Project\captcha-client,其内有三个.go文件(package captcha_client)
    • 按照约定,包名与导入路径的最后一个元素一致。例如,”Capt_Project\captcha-client” 包中的源码均以package captcha_client语句开始。
  • 在main中使用包的导入语句是
    1
    2
    3
    import (
    "Captcha_Project/captcha-client"
    )

7.把项目部署到linux上

  • cmd控制台切换到项目分别执行以下命令
    1
    2
    3
    4
    set GOARCH=amd64
    set GOOS=linux
    go build main.go
    //会在根目录得到一个test文件
  • 把test文件放到linux里用运行即可
    1
    ./test
  • 依赖文件的上传方法

8.用以下形式导入的话,尽管包里是package router,但也要视作是package router2

1
router2 "zhangshuai/router"

9.用json文件给结构体赋值

  • 引入json解析包,直接写就行不用下载
    1
    "encoding/json"
  • 定义结构体
    1
    2
    3
    4
    type Student struct {
    Name string `json:"name"`
    Age int `json:"age"`
    }
  • 读json文件并赋值
    1
    2
    3
    4
    5
    6
    7
    //已知json文件里有{"name": "小明", "age": 123}的信息
    dir, err := os.Getwd() //dir是项目绝对路径
    s := new(Student)
    file, err := os.Open("json文件相对项目路径")
    confByte, err := ioutil.ReadAll(file)
    json.Unmarshal(confByte, s)
    fmt.Println(s) //会打印出&{小明 123}
  • 实战案例
    • json文件
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      {
      "app_port": "8082",
      "app_secret": "FPlikJRgY82DpQ8tTbEngmJ0INREDjlw",
      "mysql": {
      "driver": "mysql",
      "user": "root",
      "pass_word": "123456",
      "host": "127.0.0.1",
      "port": "5726",
      "db_name": "aoyi",
      "charset": "utf8mb4",
      "show_sql": true,
      "parseTime": "true",
      "loc": "Asia/Shanghai"
      }
      }
    • 结构体定义
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      type (
      Config struct {
      AppPort string `json:"app_port"`
      AppSecret string `json:"app_secret"`
      TemplateIdExperience string `json:"template_id_experience"`
      TemplateIdTK string `json:"template_id_tk"`
      MySql MySql
      }
      MySql struct {
      Driver string `json:"driver"`
      User string `json:"user"`
      PassWord string `json:"pass_word"`
      Host string `json:"host"`
      Port string `json:"port"`
      DbName string `json:"db_name"`
      Charset string `json:"charset"`
      ShowSql bool `json:"show_sql"`
      ParseTime string `json:"parse_time"`
      Loc string `json:"loc"`
      }
      )
    • 使用json赋值
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      path := "json文件绝对路径"
      config := new(Config)

      file,err := os.Open(path)
      if err != nil {
      panic("打开配置文件错误")
      }

      confByte, err := ioutil.ReadAll(file)
      if err != nil {
      panic("读取配置文件错误")
      }

      err = json.Unmarshal(confByte,config)
      if err != nil {
      panic("解析配置文件错误")
      }
      //此时的实例config内部就已经全部是json的对应值了

10.强制导入某个包即在前加个_即可,例如

1
_ "github.com/go-sql-driver/mysql"

11.go mod相关

  • 要记得先在Goland里勾选Enable Go modules integration
  • go.mod里存有一个项目的所有依赖,clone人家项目后要先在控制台用go mod tidy下载依赖
  • xx是go.mod里的module名
    1
    2
    3
    4
    import (
    "xx/internal/qyk_app_api/service/v1"
    "xx/internal/qyk_app_api/store"
    )

12.创建文件目录os.Mkdir、os.MkdirAll区别(os.ModePerm是固定写法,类似于一套参数)

  • 如果dir1或dir2不存在,Mkdir会报错,MkdirAll会创建dir1/dir2/
  • 如果dir已存在,Mkdir会报错,Mkdir会报错,MkdirAll会什么都不做
    1
    2
    os.Mkdir("dir1/dir2/dir3", os.ModePerm)              //创建目录   
    os.MkdirAll("dir1/dir2/dir3", os.ModePerm) //创建多级目录

13.os.Creat(fileName)表示创建某文件,注意fileName应该是fileName = 绝对路径path + 文件名name 的形式

14.defer可以将一个方法延迟到包裹该方法的方法返回时执行,在实际应用中可以用来处理关闭文件句柄等收尾操作

  • Go官方文档中对defer的执行时机做了阐述
    • 包裹defer的函数返回时
    • 包裹defer的函数执行到末尾时
    • 所在的goroutine发生panic时
      1
      2
      3
      4
      f, _ := os.Create(fileName)
      defer f.Close()

      //这样使用的好处是可以在open后马上写close,而避免拖到最后才写close,有时候会忘记关闭文件

15.Go中swap的写法

1
2
swap := reflect.Swapper(s) //s是任意类型的数组
swap(i, j) //表示把s中下标为i和下标为j的元素交换位置

16.序列化和反序列化

  • 序列化是把结构体或map转成json格式
  • 反序列化是把json数据赋给结构体或map
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    type Student struct {
    Name string `json:"name"` // 姓名
    Course []string `json:"course"` // 课程
    }

    func main() {
    //file是json文件的绝对路径
    confByte, err := ioutil.ReadAll(file)
    json.Unmarshal(confByte, s)
    stu := Student{
    "张三",
    []string{"语文", "数学", "音乐"},
    }
    data, err := json.Marshal(&stu) //将struct里的数据序列化为json格式
    err := json.Unmarshal(confByte, &stu) //把confByte里的json数据反序列化放到struct

    var map1 map[string]interface{} //使用一个空接口表示可以是任意类型
    map1 = make(map[string]interface{}) //map1是一个map[string]interface{}变量
    map1["name"] = "张三"
    map1["hobby"] = [...]string{"看书", "旅游", "学习"}
    data, err := json.Marshal(map1) //将map里的数据序列化为json格式
    err := json.Unmarshal(confByte, &map1) //把confByte里的json数据反序列化放到map
    }

17.tag的解释

1
2
3
type param struct {
StuNo string `form:"stu_no" json:"no" binding:"required"`
}
  • `form:”stu_no” json:”no” binding:”required”`这部分就是tag
    • form:”stu_no”表示
    • json:”no”表示
    • binding:”required”表示

18.常用的数据结构

  • []map[string]interface{}
    • 定义
      1
      list := make([]map[string]interface{}, 0)
    • 往里填数据
      1
      2
      3
      4
      list = append(list, map[string]interface{}{
      "id": qyk_order.Id,
      "stu_title": qyk_member.Title,
      })
  • []map[string]interface{}定义和填数据
    1
    2
    3
    4
    5
    6
    data := map[string]interface{}{
    "list": list,
    "total": count,
    "page": request.Page,
    "static": static,
    }

19.结构体的一些认识

  • 结构体指针:=&xx
    • 若xx是结构体类型名,则该新创建的一个结构体实例,并用该结构体指针指向它
    • 若xx是结构体实例,则该结构体指针指向该结构体实例
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      type param struct {
      BranchId int `form:"branch_id" json:"branch_id"`
      Page int `form:"page" json:"page"`
      Limit int `form:"limit" json:"limit"`
      }

      //request是结构体param的指针,下列语句表示新建一个param指针request并且给其中两个成员赋值
      request := &param{
      Limit: 10,
      Page: 1,
      }

      ctx.Bind(request) //表示把*gin.Context中post传过来的json数据按param的tag赋值给request
      //注意这里若Json中的Page和Limit有值,则会覆盖掉之前赋的Limit=10和Page=1
      //即在Bind之前的赋值相当于一个指定默认值
  • tag为json可以自动匹配json字段,tag为form可以自动匹配get的url参数字段,注意都需要以下bind操作
    1
    2
    request := &param{}
    err := ctx.Bind(request)

20.go解析json数据成员的规则如下

1
2
3
4
5
6
7
8
9
10
11
bool, for JSON booleans

float64, for JSON numbers

string, for JSON strings

[]interface{}, for JSON arrays

map[string]interface{}, for JSON objects

nil for JSON null

报错案例

1.“该版本的1%与您运行的windows版本不兼容”

  • 解决办法:第一行package xx改成

    1
    package main
  • 原理:run指定的go文件的package必须是main才可以运行,不是有main方法就可以的

    1. unrecognized import path
  • 解决办法:go mod tidy更新一下就行