Golang学习笔记

  1. 基本语法
    1. 创建一个项目
    2. 变量和常量
      1. 声明变量
        1. 声明单一变量
        2. 声明多个变量
        3. 给变量初始化
        4. 声明并初始化
        5. 自动推导
        6. 简短变量声明
        7. 声明变量注意事项
        8. 匿名变量
      2. 声明常量
        1. 声明单一常量
        2. 声明多个常量
        3. iota使用
    3. 数据类型
      1. 整形
      2. 浮点型
      3. 复数
      4. 布尔值
      5. 字符串
        1. 多行字符串
        2. byte和rune类型
        3. 转义符
        4. 字符串操作
        5. 字符串修改
        6. 类型转换
        7. 其他操作
      6. 数组
        1. 普通声明加初始化
        2. 声明并初始化
        3. 自动推导长度
        4. 指定某位置的值
        5. 多维数组
        6. 数组遍历
      7. 切片
        1. 声明切片
        2. 其他声明方式
        3. 切片初始化
        4. 切片的长度、容量和底层数组的关系
          1. 获取切片长度和容量
          2. 切片与底层数组的关系
          3. 切片的扩容
        5. 切片的切片
        6. 切片的其他操作
          1. append
          2. copy
          3. 删除
          4. 遍历
          5. 比较
          6. 排序
      8. map
        1. 简单使用
        2. map的复杂嵌套
        3. map的一些操作
          1. 增、删、改
          2. 遍历map
      9. 指针
        1. 一般使用
        2. new
        3. make
      10. 函数
        1. 参数问题
        2. 匿名函数
        3. 闭包
        4. defer注意事项
        5. defer
          1. defer的简单使用
          2. 结合函数使用
          3. defer的注意事项
        6. 构造函数
        7. 方法
          1. 指针接收者
          2. 给任意类型添加方法
      11. 结构体
        1. 结构体的简单使用
        2. 匿名结构体
        3. 结构体的操作
          1. 结构体查值
          2. 修改结构体的值
        4. 结构体嵌套
          1. 省略字段名的结构体嵌套
          2. 利用结构体模拟继承
        5. 结构体补充
          1. 结构体内存布局
          2. 使用构造函数得到结构体
          3. 为结构体添加方法
          4. 匿名字段
          5. 结构体与json数据之间相互转换
      12. 接口
        1. 接口简单使用
        2. 使用值接收者实现方法和使用指针接收者实现方法
        3. 接口嵌套
        4. 接口在内存
        5. 空接口
        6. 接口类型断言
    4. 流程控制
      1. if
        1. if 的一种特殊写法
      2. for
        1. 跳转语句(break、goto、continue)
          1. break
          2. break + 便签
          3. continue
          4. continue + 标签
          5. goto
        2. for range
      3. switch
    5. 运算符
      1. 算术运算符
      2. 关系运算符
      3. 逻辑运算符
      4. 位运算符
      5. 赋值运算符
    6. 错误处理
    7. 包package
      1. 包的导入
      2. 包的别名
        1. “_”别名
        2. “.”别名
      3. 导入包时执行顺序
      4. 定义包
      5. go module
        1. go module准备工作
        2. go module使用
          1. 初始化
          2. 拉取了新的依赖
        3. go module命令
        4. 补充
    8. 一些关键字
      1. type
        1. 自定义别名
        2. 自定义类型
    9. 一些规则
      1. 可见性规则
    10. 文件操作
      1. 文件–读取
        1. 初始版本
        2. bufio
        3. ioutil
      2. 文件–写入
        1. 方法一:os.OpenFile()
        2. 方法二:bufio.NewWriter
        3. 方法三:ioutil.WriteFile
    11. 反射
      1. 获取类型TypeOf
        1. Name()和 Kind()
      2. 获取值ValueOf
        1. IsNil()和IsValid()和IsZero
      3. 修改值ValueOf+Elem()
        1. 一般类型的修改
        2. 结构体类型的修改
    12. 异步
      1. goroutine
        1. goroutine + sync.WaitGroup
        2. 补充
      2. channel
        1. 单向channel
        2. channel使用for循环取值
        3. channel使用make注意事项
        4. channel关闭后的注意事项
        5. channel状态总结
      3. select
        1. 互斥锁
        2. 读写互斥锁
      4. 其他
    13. 网络编程
      1. socket编程–TCP
        1. TCP服务端
        2. TCP客户端
        3. 注意事项
        4. 解决tcp粘包现象
      2. socket编程–UDP
        1. UDP服务端
        2. UDP客户端
      3. http编程(net/http)
        1. http服务端
          1. 一些函数
          2. 增加请求头
          3. 获得请求体
          4. 增加cookie
        2. 客户端
    14. 测试工具
    15. 数据库操作
      1. 链接数据库
        1. 下载第三方库
        2. 链接
      2. 操作数据
        1. 增、删、改数据
        2. 查询数据
          1. 查询一条数据
          2. 查询多条数据
      3. mysql预处理
  2. 进阶版本
    1. 内存对齐
  3. go标准库
    1. fmt
      1. fmt.Print
      2. fmt.Println
      3. fmt.Printf
    2. strings
      1. 开头与结尾
      2. 位置
      3. 替换
      4. 计数
      5. 重复
      6. 大小写
      7. 去除
      8. 分割
      9. 拼接
    3. bufio包
    4. time
  4. sync
  5. atomic

基本语法

创建一个项目

$GOPATH/src下创建一个项目
在main.go中

package main

import "fmt"

func main()  {
  fmt.Println("hello world!")

}

使用go build编译运行
注意事项:

语句只能在函数中,而声明变量/类型等可以在函数外

单行注释://
多行注释:/* 注释内容 */

变量和常量

声明变量

程序运行过程中的数据都是保存在内存中,我们想要在代码中操作某个数据时就需要去内存上找到这个变量,但是如果我们直接在代码中通过内存地址去操作变量的话,代码的可读性会非常差而且还容易出错,所以我们就利用变量将这个数据的内存地址保存起来,以后直接通过这个变量就能找到内存上对应的数据了。
Go语言中的变量需要声明后才能使用,同一作用域内不支持重复声明。 并且Go语言的变量声明后必须使用。
变量的格式:由字母、数字和_组成,并且只能以字母和_开头,建议写成驼峰式如studentName

声明单一变量

假如生命的变量只有一两个可以直接使用var关键字
格式: var 变量名 数据类型

var name string
var age int
声明多个变量

但假如要声明跟多个变量的话,我们不想每行都写上一个var,可以用以下方法解决

var (
name string
age int
)
给变量初始化

变量声明后会有一个默认值,称为零值.我们声明变量是为了存放我们需要的数据,所以要对变量进行初始化即赋值
一定要在函数中写

package main

import "fmt"

var (
  age  int
  name string
  isOk bool
)

func main() {
    age = 12
  name = "小猪"
  isOk = true
  fmt.Println()
  fmt.Printf("name %s", name)
  fmt.Println(isOk)

}
声明并初始化

在go中可以在声明变量的同时初始化变量
如下面写的:

  var age int = 12
自动推导

像上面写的,我们可以用一句话进行声明和初始化,但是我们已经把值给写在式子中了,也就是说编译器已经知道变量要存放的结果了,那么能不能不写数据的类型让编译器自动推导呢?答案是可以的.
我们只需要使用 =就能自动推导出变量的类型

var name = "xxx"
var age = 12

也可以这样

var name, age = "xxx", 12
简短变量声明

这应该是一种经常可以到的声明方式
使用:=进行声明并初始化:

name := "xxx"
声明变量注意事项

1 在go中非全局变量声明了就一定要使用,否则会报错
2 假如是全局变量就不会报错,但是加入全局变量在函数中被赋值了,但未使用,会报错
3 import导入的包未使用也会报错
4 :=var了在下一行=的一定要在函数中执行,不然会报错,全局只允许var a intvar a = 2

匿名变量

假如有这样的使用场景: 我调用一个函数,有两个返回值,但是,我只需要其中的一个值,由于在go中我我声明了并初始化了非全局变量,不使用这个变量的话就会报错.为了解决这个问题,就有可匿名变量
匿名变量是没有命名空间,不会分配内存的,所以 不存在重复声明的问题.
匿名变量使用下划线_表示
如下面这个例子

package main

import "fmt"

func main() {
  num1 := 12
  num2 := 13
  res, _ := add(num1, num2)
  fmt.Println(res)
}

func add(a int, b int) (int, string) {
  return a + b, "ok"
}

这个例子中包含函数的写法,下面有写.很明显add函数有两个返回值,但我只需要一个,所以使用了密名变量.

声明常量

相对于变量,常量是恒定不变的值,多用于定义程序运行期间不会改变的那些值。 常量的声明和变量声明非常类似,只是把var换成了const,常量在定义的时候必须赋值。

声明单一常量

const pi = 3.14
上面就声明了pi这个常量.在整个程序运行期间它的值不能再发生变化.

声明多个常量

与变量的方法类似,用以下的方法声明

const (
    pi = 3.1415
    e = 2.7182
)

注:const同时声明多个常量时,如果省略了值则表示和上面一行的值相同。 例如:

const (
    n1 = 100
    n2  // 100
    n3  // 100
)

//是注释

iota使用

iota是go的常量计数器,只能在常量的表达式中使用.
iota用两个关键点:

1、iota在const关键字出现时将被重置为0
2、const中每新增一行常量声明将使iota计数一次

如:

const (
    a1 = iota  // 0
    a2         // 1
    a3         // 2
    a4         // 3
  )

几个常见的iota示例:
这里展示一下iota例子,方便理解iota。

  1. 使用_跳过某些值

    const (
        n1 = iota // 0
        n2        //1
        _
        n4        //3
      )
    
  2. 多个iota定义在一行

    const (
        a, b = iota + 1, iota + 2 // 1, 2
        c, d                      // 2, 3
      )
    // 这里的c, d 就是iota + 1, iota + 2
    
  3. iota被隔断

    const (
        a1 = iota   // 0
        a2 = 10     // 10
        a3 = iota   // 2
        a4          // 3
      )
    
  4. 定义数量级

    const (
        _  = iota
        KB = 1 << (10 * iota)
        MB = 1 << (10 * iota)
        GB = 1 << (10 * iota)
        TB = 1 << (10 * iota)
        PB = 1 << (10 * iota)
      )
    

<<表示左移操作。如:1 << 10表示将1的二进制表示向左移10位,即1变成了10000000000(二进制),也就是1024(十进制)。

数据类型

这里简单说明go中的数据类型和对应的零值,以便于声明变量。

整形

零值:0

| 类型 | 说明 |
| – | – | – |
int | 32位操作系统上就是int32,64位操作系统上就是int64
uint | 32位操作系统上就是uint32,64位操作系统上就是uint64
uintptr | 无符号整型,用于存放一个指针
int8 | 有符号 8位整型 (-128127)
int16 | 有符号 16位整型 (-3276832767)
int32 | 有符号 32位整型 (-21474836482147483647)
int64 | 有符号 64位整型 (-92233720368547758089223372036854775807)
uint8 | 无符号 8位整型 (0255)
uint16 | 无符号 16位整型 (065535)
uint32 | 无符号 32位整型 (04294967295)
uint64 | 无符号 64位整型 (018446744073709551615)

例子: 声明一个8进制数?

package main

import "fmt"


func main(){
  i8 := int8(12)
  fmt.Printf("%T \n", i8)  // Printf()是格式化输出。%T占位符表示输出数据的类型
}

注:想了解fmt,请查看后面的go标准库  
进制声明
go中进制有2进制、8进制、10进制16进制,Go1.13版本之后引入了数字字面量语法,这样便于开发者以二进制、八进制或十六进制浮点数的格式定义数字,例如:
v := 0b00101, 代表二进制。 v := 0o777,代表八进制。 v := 0x12A,代表十六进制 。而且还允许我们用 _ 来分隔数字,比如说:v := 123_456 等于 123456。
进制转换
利用fmt库进行进制转换,如:

package main

import "fmt"


func main(){
  var n1 = 0b11  // 二进制
  var n2 = 0o10  // 八进制
  var n3 = 20    // 十进制
  var n4 = 0x1A  // 十六进制
  
  fmt.Printf("%d \n", n1)
  fmt.Printf("%d \n", n2)
  fmt.Printf("%b \n", n3)
  fmt.Printf("%d \n", n4)
}

浮点型

零值:0

Go语言只支持两种浮点型数:float32float64。注意:没有float

package main

import (
  "fmt"
  "math"
)

func main()  {
  f1 := float32(12.21)
  f2 := float64(32.23)
  fmt.Println(f1)
  fmt.Println(f2)
  fmt.Println(math.MaxFloat32)  // 查看最大float32
  fmt.Println(math.MaxFloat64)  // 查看最大float64
}

复数

零值:(0+0i)

complex64complex128

var c1 complex64
c1 = 1 + 2i
var c2 complex128
c2 = 2 + 3i
fmt.Println(c1)
fmt.Println(c2)

复数有实部和虚部,complex64的实部和虚部为32位,complex128的实部和虚部为64位。

布尔值

零值:false

布尔型数据只有truefalse

注意:

Go 语言中不允许将整型强制转换为布尔型.
布尔型无法参与数值运算,也无法与其他类型进行转换。

字符串

零值:””

Go语言中的字符串以原生数据类型出现,使用字符串就像使用其他原生数据类型(intboolfloat32float64 等)一样。 Go 语言里的字符串的内部实现使用UTF-8编码。
字符串的值为双引号(“)中的内容如:

s := "hello"

注:不能用单引号表示字符串,因为在go中,单引号括起来的叫字符
举个例子:

name := "小明"
n1 := '小'
n2 := '明'

很明显,字符就相当于把字符串拆开

多行字符串

我们上面写的都是单行字符串,有时候我们需要写多行字符串,那么我们可以是用反引号(Esc下面的那个键)把字符串括起来。
需要注意的是:
用反引号括起来后,转义不会生效,且会原封不动的把你定义的字符串报存下了,包括缩进、回车等。
如我这张截图:
多行字符串演示

byte和rune类型

byterune其实都是字节类型,不过,byte对应的是ascii,而rune对应的是utf8
在字符串那里,我们就说字节是以单引号括起来的。
假如我们写以下的代码

package main

import (
  "fmt"
)

func main()  {
  s1 := "hello world!\n落霞与孤鹜齐飞,秋水共长天一色。"
  for i, s := range s1{
    fmt.Println(i, s)
  }

}

这是一个循环遍历的一个代码,i是索引,s是字符串的元素–字符。输出结果:

0 104
1 101
2 108
3 108
4 111
5 32
6 119
7 111
8 114
9 108
10 100
11 33
12 10
13 33853
16 38686
19 19982
22 23396
25 40540
28 40784
31 39134
34 65292
37 31179
40 27700
43 20849
46 38271
49 22825
52 19968
55 33394
58 12290

所以,假如我们要打印出适合我们看的字符的话,需要用fmt.Printf()使用%c占位符。

特别提醒一下,byte其实就是uint8的别名、rune是int32的别名,至于什么是别名,以后会写的

转义符

上面我们说到了转义,我认为转义就是让普通字符变特殊和让特殊字符变普通

转义符 说明
\r 回车符(返回行首)
\n 换行符(直接跳到下一行的同列位置)
\t 制表符
\' 单引号
\" 双引号
\\ 反斜杠
这里还是要说一下,不能使用但引号括起字符串。
字符串操作

求长度:len(str)
拼接字符串:+fmt.Sprintf

字符串修改

由于在go语言中是不能直接修改字符串的,所以这里特别写一下。
因为go中的字符串的内存是已经分配好的,而且不能扩容。所以我们假如要修改已有的字符串的话,就要引入一个内存可变的数据类型–切片(后面的数据类型有)。切片就想python的list,是引用数据类型。
如:

package main

import (
  "fmt"
)

func main() {
  name1 := "凹凸慢"
  name1_s := []rune(name1)
  name1_s[2] = '曼'
  fmt.Println(string((name1_s)))

  name2 := "lczmx"
  name2_s := []byte(name2)
  name2_s[0] = 'L'
  fmt.Println(string((name2_s)))
}

注意事项:

英文和英文符号字符串对应的是byte,其他(如:中文)对应的是rune,写切片的元素是,不要写错了
修改时等号右边是字节,不是字符串!!!name1_s[2] = '曼',请注意是单引号。

类型转换

string –> 其他类型

使用strconv包,strconv包实现了基本数据类型和其字符串表示的相互转换。
有用的函数的签名:

签名 参数 说明
func ParseBool(str string) (value bool, err error) 传入string 解析成bool
func Atoi(s string) (i int, err error) 传入string 解析成int型,比ParseInt简单一点
func ParseInt(s string, base int, bitSize int) (i int64, err error) s:要解析的字符串;base:几进制;bitSize:多少位 解析成int64,可以强制转换
func ParseUint(s string, base int, bitSize int) (n uint64, err error) s:要解析的字符串;base:几进制;bitSize:多少位 解析成uint64,可以强制转换
func ParseFloat(s string, bitSize int) (f float64, err error) s:要解析的字符串;bitSize:多少位 解析成float64,可以强制转换
package main

import (
  "fmt"
  "strconv"
)

func main() {
  aInt := "123"
  bBool := "false"
  cFloat := "3.14"
  // bitSize:
  // 0, 8, 16, 32, and 64
  // int, int8, int16, int32, and int64.
  // 以上对应
  aV1, _ := strconv.ParseInt(aInt, 10, 0)
  aV2, _ := strconv.Atoi(aInt)
  fmt.Printf("%#v  %T\n", aV1, aV1)   // 123  int64

  fmt.Printf("%#v  %T\n", aV2, aV2)   //123  int

  bV, _ := strconv.ParseBool(bBool)
  fmt.Printf("%#v  %T\n", bV, bV)     //false  bool

  cV, _ := strconv.ParseFloat(cFloat, 64)
  fmt.Printf("%#v  %T\n", cV, cV)     //3.14  float64

}

其他类型 –> string

  1. strconv包

    签名 说明
    func Itoa(i int) string 传入int
    func FormatBool(b bool) string 传入bool
    func FormatInt(i int64, base int) string i:转换的数字(有符号);base:几进制
    func FormatUint(i uint64, base int) string i:转换的数字(无符号);base:几进制
    func FormatFloat(f float64, fmt byte, prec, bitSize int) string 有点长,见表格下面文字

    FormatFloat:
    bitSize表示f的来源类型(32:float32、64:float64),会据此进行舍入。
    fmt表示格式:’f’(-ddd.dddd)、’b’(-ddddp±ddd,指数为二进制)、’e’(-d.dddde±dd,十进制指数)、’E’(-d.ddddE±dd,十进制指数)、’g’(指数很大时用’e’格式,否则’f’格式)、’G’(指数很大时用’E’格式,否则’f’格式)。
    prec控制精度(排除指数部分):对’f’、’e’、’E’,它表示小数点后的数字个数;对’g’、’G’,它控制总的数字个数。如果prec 为-1,则代表使用最少数量的、但又必需的数字来表示f。

  2. fmt包
    TODO:完善fmt.Sxxx

其他操作

见go标准库strings[^1]
[^1]: 我会把一些单独分开来写

数组

数组是go语言中的一种复杂数据类型,在定义时要确定长度和存放元素的类型,且数组的长度不可改变。

普通声明加初始化
package main

import "fmt"

func main() {

  var a1 [2]int
  a1 = [2]int{1, 2}
  fmt.Println(a1)   // [1 2]

}

显然这种方法比较麻烦

声明并初始化
package main

import "fmt"

func main() {
  var a2 = [3]bool{true, false, true}  // [true false true]
  a3 := [3]int{}  // [0 0 0]
  a4 := [3]int{1, 3, 4}  //[1 3 4]
}

以上,a3表明:
数组不指定某个元素的值时,默认以零值补上

自动推导长度

使用...即可

package main

import "fmt"

func main() {
  var a2 = [...]bool{true, false, true}  // [true false true]
  a6 := [...]int{1, 3, 4, 5, 6, 6, 7}    //[1 3 4 5 6 6 7]
}
指定某位置的值

使用{index: val}的方式,即索引:值

package main

import "fmt"

func main() {

  // 索引为2的元素为1,索引为4的元素为1
  a5 := [5]int{2: 1, 4: 1}  // [0 0 1 0 1]
  // 索引为2的元素为1,索引为10的元素为2
  a7 := [...]int{2: 1, 10: 2}    //[0 0 1 0 0 0 0 0 0 0 2]
}
多维数组

多维数组实际上就是由数组组成的数据。其声明方式和初始化和其他类型的数组一样:

package main

import "fmt"

func main() {
    // 方式一
  var b [3][2]int
  b = [3][2]int{
    {1, 3},
    {2, 4},
    {5, 6},
  }
  fmt.Println(b)  // [[1 3] [2 4] [5 6]]
  // 方式二
  c := [2][2]int{{1, 3}, {2, 4}}
  fmt.Println(c)  // [[1 3] [2 4]]
}
数组遍历
package main

import "fmt"

func main() {

  a := [...]int{1, 3, 4, 5, 6, 6, 7}    //[1 3 4 5 6 6 7]
  // 方式一
  for i:=0; i<len(a);i++ {
    fmt.Println(i, a[i])

  }
  // 方式二
  for index, val := range a{
    fmt.Println(index, val)
  }
}

数组是值类型
[2]int[3]int 不是同一种数据类型
指定数组元素个数必须是常量
相同类型的数组才能比较,且只能比较== !=

切片

我们知道数组实际上是不能改变长度的,但是我们很多时候都要求一个可变长的数据类型来存储数据,这时就要引入切片了。
切片不是数组,指向底层的数组,切片有长度和容量两个属性(后面将会详解)。

声明切片

一般格式: var 变量名 [] 元素类型
很明显,在声明时切片比数组少了长度这个限制条件
比如:

package main

import "fmt"

func main()  {
  var a []int
  fmt.Println(a)  // 空的切片
}
其他声明方式
  1. 从数组中来
    既然说这是切片,那么数组的切片产生的数据就是切片类型,使用’[]’切片的规则:

    • 索引从0到最后一个,且索引没有负数
    • a[1:3]时,不会取最后一个(索引为3的元素)
    • 空缺时(如:a[ : 4]a[ 1: ]a[ : ]) ,表示从开头取、取到结尾、全部取
    • 长度=元素个数,容量=切片的第一个元素到底层数组的最后一个元素的长度(在长度与容量那里一并讲)
    package main
    
    import "fmt"
    
    func main()  {
      var b = [...]int{1,23,4,5,66,7}
      fmt.Println(b)
      c := b[:2]
      fmt.Println(c)
      fmt.Printf("%T\n", c)  // []int
    }
    
  2. 使用make创建
    make见指针部分
    格式:变量名 := make([]类型, 长度, 容量)

    容量可省略,省略时容量=长度

    package main
    
    import "fmt"
    
    func main() {
      // 这两种方式是一样的
      var c = make([]int, 5, 10)
      d := make([]int, 5, 10)
    }
    
切片初始化

直接将数组切片的,应该不需要初始化了吧。 |

package main

import "fmt"

func main() {
  // 方式一
  var a []int
  fmt.Println(a) // 空的切片
  a = []int{1, 3, 4, 5, 6}
  fmt.Println(a)

  // 方式二
  var b = []int{1, 2, 4, 5, 6, 7}
  fmt.Println(b)
  
  // 方式三
  var c = make([]int, 5, 10)
  d := make([]int, 5, 10)
  c = []int{1,3,4,5}
  d = []int{1,3,4,5}
  fmt.Println(c,d)
}
切片的长度、容量和底层数组的关系

切片的长度和容量对应着切片的两个指标:存了多少和能存多少

获取切片长度和容量
package main

import "fmt"

func main() {
  var b = []int{1, 2, 4, 5, 6, 7}
  fmt.Println("切片长度:", len(b))
  fmt.Println("切片容量:", cap(b))
}

假如运行上面这个代码,就会发现: 长度和容量都是6,其实这就是上面讲的“ 长度=元素个数,容量=切片的第一个元素到底层数组的最后一个元素的长度”。
这里借助两张从网上盗来的图
s1是对a数组的切片:
图一

此时,切片的长度为5,容量为8
图二
此时,切片的长度为3,容量为5

切片与底层数组的关系

切片实际上并不存储数据,而是使用底层数组来存储数据,切片本身只会指向底层数组(是连续的),因此假如底层数组发生改变时,切片也会发生改变。
如:

package main

import "fmt"

func main() {
  var b = [...]int{1, 2, 4, 5, 6, 7}
  c := b[2:]  
  fmt.Println(c) //  [4 5 6 7]
  b[2] = 2
  fmt.Println(c) //  [2 5 6 7]
}

可以看到,我根本没有直接修改切片c的值,只修改了数组b的值,但是切片c的值也跟着改变了。

切片的扩容

我们现在知道切片是可以扩容的,那么切片是怎么扩容的呢?
我们可以通过查看$GOROOT/src/runtime/slice.go源码,其中扩容相关代码如下:

newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
  newcap = cap
} else {
  if old.len < 1024 {
    newcap = doublecap
  } else {
    // Check 0 < newcap to detect overflow
    // and prevent an infinite loop.
    for 0 < newcap && newcap < cap {
      newcap += newcap / 4
    }
    // Set newcap to the requested cap when
    // the newcap calculation overflowed.
    if newcap <= 0 {
      newcap = cap
    }
  }
}

从上面的代码可以看出以下内容:

首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)。
否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap )
否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)
如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。

需要注意的是,切片扩容还会根据切片中元素的类型不同而做不同的处理,比如int和string类型的处理方式就不一样。

切片的切片
切片的其他操作
append

append即是追加,但需要注意的是,由于切片是 通过底层数组来存储的,所以假如追加后的内容超过了切片的容量,切片就会重新的开辟一片内存,用来存储数据,所以go语言规定:append后要用一个变量接收返回值(有可能重新分配了内存)。

package main

import "fmt"

func main() {
  var a = [...]int{1, 2, 4, 5, 6, 7}
  b := a[3:]       // [5 6 7]
  c := a[2:]       // [4 5 6 7]
  
  // 追加一个元素
  c = append(c, 1) // [4 5 6 7] ==> [4 5 6 7 1]
  fmt.Println(c)
  
  // 追加多个元素
  c = append(c, 1, 2) // [4 5 6 7 1] ==> [4 5 6 7 1 1 2]
  fmt.Println(c)
  
  // 追加一个切片, ...表示把切片拆开
  c = append(c, b...) // [4 5 6 7 1 1 2] ==> [4 5 6 7 1 1 2 5 6 7]
  fmt.Println(c)
  
}

注意:假如对只声明了没有初始化的切片使用append,会自动初始化

copy

格式copy(s1, s2) 表示:从s2复制到s1

package main

import "fmt"

func main() {
  a1 := []int{1, 2, 3, 4}
  b1 := []int{7, 8, 9}
  copy(a1, b1)
  fmt.Println(a1) // [7 8 9 4]

  a2 := []int{1, 2, 3, 4}
  b2 := []int{7, 8, 9}
  copy(b2, a2)
  fmt.Println(b2) // [1 2 3]

}

我理解的复制的过程是——对于copy(s1, s2): 从s2中的值替换s1中的值(从左到右),所以s1的长度决定了是否能够完全把s1中的值复制过来。

假如仅声明未初始化的切片长度和容量都为0,对它们使用copy是没有效果的

注意
使用copy时会重新分配内存,所以修改原来的底层数组时不会影响到copy后的切片。这与使用[:]获得有明显区别。

package main

import "fmt"

func main() {

  var a = []int{1, 2, 4, 5, 6, 7}
  b := a[:] // [5 6 7]
  c := make([]int,6)
  copy(c,a)
  a[1] = 22
  fmt.Println("a", a)    // a [1 22 4 5 6 7]
  fmt.Println("b", b)    // b [1 22 4 5 6 7]
  fmt.Println("c", c)    // c [1 2 4 5 6 7]


}
删除

切片没有删除,但是可以使用append达到删除的效果。


package main

import "fmt"

func main() {
  a := []int{1, 2, 3, 4, 2, 4, 5, 7, 78, 9}
  // 删除索引为4的元素(2)
  a = append(a[:4], a[5:]...)
  fmt.Println(a)  // [1 2 3 4 4 5 7 78 9]
}
遍历

切片的遍历与数组的一样,这里不做记录

比较

切片之间是不能比较的,我们不能使用==操作符来判断两个切片是否含有全部相等元素。 切片唯一合法的比较操作是和nil比较。 一个nil值的切片并没有底层数组,一个nil值的切片的长度和容量都是0。但是我们不能说一个长度和容量都是0的切片一定是nil,例如下面的示例:

  var s1 []int           // len(s1)=0  cap(s1)=0  没有初始化
  s2 := []int{}          // len(s2)=0  cap(s2)=0  []
  s3 := make([]int, 0)   // len(s3)=0  cap(s3)=0  []
  fmt.Println(s1 == nil)  // true
  fmt.Println(s2 == nil)  // false
  fmt.Println(s3 == nil)  // false

要判断一个切片是否是空的,要是用len(s) == 0来判断,不应该使用s == nil来判断

排序

TODO

map

map就类似于Python中的字典,是无序的,内部使用散列表(hash、基于key-value的数据结构,Go语言中的map是引用类型在使用前必须初始化(使用make)才能使用
使用map的格式: map[键类型]值类型

可以作为键的数据类型必须是支持==!=(值类型:intstring等支持,引用类型:切片、map不支持)
map只有len,没有cap,且我们在使用make之前要估量好大约要存多大的数据

简单使用

使用var + make或自动推导开辟内存:

package main

import "fmt"

func main() {
  // var + make
  var m1 = make(map[string]int, 10)
  m1["age"] = 18
  fmt.Println(m1)
  
  // 未初始化例子
  //var m2 map[string]int
  //m2["age"] = 18    // 报错  panic: assignment to entry in nil map
  
  // 或者,自动推导
  data := map[string]int{
    "a": 1,
    "b": 2,
  }
  fmt.Println(data)  // map[a:1 b:2]

}
map的复杂嵌套

在上面的例子中说明了,不对map进行初始化的话会报错!在多层嵌套时可能要对每一层都开辟内存,而有可能忽视掉某一层。
这里举两种类型:

  • 元素为map的切片([map[age:12] map[age:17]]
  • 值为切片的map(map[num:[1 2 3]]

看一下用代码怎么实现:

package main

import "fmt"

func main() {

  /*---------------------- 元素为map的切片 --------------------------*/
  var s = make([]map[string]int, 2, 20)
  // 此时切片已经有了内存,但是map没有内存
  // 为map开辟内存空间:
  s[0] = make(map[string]int) // 仅为第一个元素的map开辟内存
  // 操作
  s[0]["age"] = 12
  // s[1]["age"] = 12      // 会报错:panic: assignment to entry in nil map
  
  s[1] = make(map[string]int) // 为第二个元素的map开辟内存
  s[1]["age"] = 17            // 不报错
  fmt.Println(s)              // [map[age:12] map[age:17]]

  /*---------------------- 值为切片的map --------------------------*/
  var m = make(map[string][]int, 1) // 对map声明并初始化
  m["num"] = []int{1, 2, 3}         // 对切片的初始化
  fmt.Println(m)                    // map[num:[1 2 3]]

}
map的一些操作
增、删、改

对于增的前提:已经初始化,否者提示anic: assignment to entry in nil map

package main

import "fmt"

func main() {
  var m1 = make(map[string]int, 10)
  // 增
  m1["age"] = 18
  fmt.Println(m1)    // map[age:18]
  
  // 改
  m1["age"] = 20    // 有值时就改
  fmt.Println(m1)    // map[age:20]
  
  // 删
  delete(m1, "age")  // delete格式:delete(map, key)
  fmt.Println(m1)    // map[]

}

使用变量[key]取值

package main

import "fmt"

func main() {
  var m1 = make(map[string]int, 10)
  m1["age"] = 18
  fmt.Println(m1["age"])  // 18
  delete(m1, "age")       // 删除
  fmt.Println(m1["age"])  // 返回零值: 0

}

对于查不存在的键时,会返回值类型的零值(如:map[string]int, 返回0),但也有方法判断键是否存在

package main

import "fmt"

func main() {
  var m1 = make(map[string]int, 10)
  m1["age"] = 18
  fmt.Println(m1["age"])  // 18
  delete(m1, "age")   // 删除
  
  // 多一个变量ok接收,返回的是bool类型
  val, ok := m1["age"]
  if !ok{
    fmt.Println("没有这个键")
  }else{
    fmt.Println(val)
  }

}
遍历map

遍历使用的是for range,可以看控制流程部分的for range

指针

零值:nil

go语言的指针并没有c语言的指针那么麻烦,简单的说,go语言的指针只有两个用途:通过变量取地址、通过地址取变量。
这两个用途正好对应着两个符号:

  • &: 取地址符
  • *: 取变量符

图示看一下:
取地址图示

一般使用

使用指针时我们可以使用自动推导,亦可以自己声明类型:*数据类型

package main

import "fmt"

func main()  {
  // 方式一
  a := 2
  ap := &a
  fmt.Println(*ap)  // 2

  // 方式二
  var b int
  var bp *int
  b = 3
  bp = &b
  fmt.Println(*bp)  // 3
}

拿到了指针,就可以直接操作数据:

package main

import "fmt"

func main()  {
  a := 2
  ap := &a
  *ap = 100
  fmt.Println(a)    // 100
}

因此在函数中,假如传入的参数的类型是值类型时(会传入数据的副本),只需传入类型的指针即可修改数据。

new

使用new可以创建内存空间,返回一个指针

package main

import "fmt"

func main()  {
  var a = new(int)
  var b int      // 0
  fmt.Println(a)    // 0xc00001c170
  fmt.Println(b)    // 0
  a = &b
  fmt.Println(a)    // 0xc00001c178
}

实际上使用自动推导更加方便,而且使用var a *int这种方法也行,不同的是后者没有内存分配。

make

使用make也可创建内存空间,不同的是只用于slicemap以及chan的内存创建,且返回的是其对应的数据类型(在切片中也演示过)。
make函数的函数签名如下:

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

例子:

package main

import "fmt"

func main() {
  a := make([]int, 2)
  fmt.Println(a)  // [0 0]

}

函数

在我看来,用于实现具体功能的代码块。主要是把某些功能分离出来,有减少耦合度、便于可复用、便于维护的特点。
函数的结构:

fun 函数名(参数) (返回值){

具体代码

}

参数和返回值的书写方式很多,一下全列出来,它们之间和可以组合使用。
参数:

参数形式 说明
(a int, b string) 一般形式,使用a,b两个形参分别接收intstring类型的数据。
(a, b int) 同种类型可以省略
(a ...int) 不定长参数,a会变为一个切片,元素为传入值的拷贝

给个例子一看就明白:

package main

import "fmt"

func sum(a string, b, c int, d ...int) {
  fmt.Println(a)    // name
  fmt.Println(b)    // 10
  fmt.Println(c)    // 12
  fmt.Println(d)    // [1 2 3 4 56 7 8 98]

}

func main() {
  sum("name", 10, 12, 1, 2, 3, 4, 56, 7, 8, 98)
}

形参不使用不会报错

返回值

返回值形式 说明
没有时不用写包括括号
int 一个返回值不命名时,只需要写上类型即可
(a int) 命名返回值(确定返回值是哪个变量)
(int, int, string) 多个返回值时用括号括上
(a, b string) 命名+同类型

例子:

// 无返回值
func f1(a int) {

}

// 一个返回值
func f2(a, b int) int {
  return a + b
}

// 命名返回值
func f3(a, b int) (res int) {
  res = a + b
  return
}

// 多返回值
func f4(a, b int) (int, int, bool) {
  return a + b, a - b, true
}
// 多返回值 + 命名
func f5(a, b int) (sum, minus int,ok bool){
  sum = a + b
  minus = a - b
  ok = true
  return 

}

一定要注意:命名返回值后只需要写return即可

参数问题

这个参数问题不是形参的问题,而是传入的数据问题。前面多次提到数据类型分引用类型、值类型,那么引用类型、值类型有什么用呢?需要注意什么呢?

  1. 值类型

    有:int系列、float系列、boolstring、数组和结构体
    特点是:变量直接存储值,内存通常在栈中分配

  2. 引用类型

    有:指针、slice切片、管道channel、接口interfacemap、函数等
    特点是:变量存储的是一个地址,这个地址对应的空间里才是真正存储的值,内存通常在堆中分配

实际上:假如我对一个函数传入的参数是切片和int型,对他们进行修改,会发现:切片被修改了,而int未被修改,比如下面这个例子:

package main

import "fmt"

func change(s []int, i int) {
  s[0] = 100
  i = 200
}

func main() {
  a := []int{1, 2, 3, 4}
  b := 2
  fmt.Println("修改前", a)    //修改前 [1 2 3 4]
  fmt.Println("修改前", b)    //修改前 2

  change(a, b)
  fmt.Println("修改后", a)    //修改后 [100 2 3 4]
  fmt.Println("修改后", b)    //修改后 2

}

为了解释这一现象,我们需要明白在go语言中的一个传参规则:传入值类型时实际上传入的是值的副本、引用类型是内存地址的副本。也就是说,我们拿到的值类型是另一个数据,所以我们对它修改是没有作用的。那么假如一定要修改值类型呢?很简单:传入的值为值类型的指针,即可修改值类型。
比如:

package main

import "fmt"

func change(s []int, i *int) {
  s[0] = 100
  *i = 200
}

func main() {

  a := []int{1, 2, 3, 4}
  b := 2
  fmt.Println("修改前", a) //修改前 [1 2 3 4]
  fmt.Println("修改前", b) //修改前 2

  change(a, &b)
  fmt.Println("修改后", a) //修改后 [100 2 3 4]
  fmt.Println("修改后", b) //修改后 200

}

修改成功了!只需注意形参是指针类型,传值时传入地址即可。

匿名函数

由于在go语言中,函数不支持嵌。所以对一般函数来说我们是不能在函数中定义函数的,但也有一种函数例外–匿名函数。
例子:

package main

import "fmt"

func main() {
  // 形式一
  sum := func(a, b int) int {
    return a + b
  }
  res1 := sum(1, 2)
  fmt.Println(res1)
  // 形式二
  res2 := func(a, b int) int {
    return a + b
  }(1, 2)
  fmt.Println(res2)
}

除了没有函数名外,其他的都和普通的函数一样。

闭包

引用许式伟 的《Go语言编程》的概括:

闭包的概念:是可以包含自由(未绑定到特定对象)变量的代码块,这些变量不在这个代码块内或者任何全局上下文中定义,而是在定义代码块的环境中定义。要执行的代码块(由于自由变量包含在代码块中,所以这些自由变量以及它们引用的对象没有被释放)为自由变量提供绑定的计算环境(作用域)。
闭包的价值 : 闭包的价值在于可以作为函数对象或者匿名函数,对于类型系统而言,这意味着不仅要表示数据还要表示代码。支持闭包的多数语言都将函数作为第一级对象,就是说这些函数可以存储到变量中作为参数传递给其他函数,最重要的是能够被函数动态创建和返回。
Go语言中的闭包同样也会引用到函数外的变量。闭包的实现确保只要闭包还被使用,那么被闭包引用的变量会一直存在。

简单来说:闭包=函数+引用环境 。举几个例子说明一下它是怎么使用的。
这是简单的例子

package main

import "fmt"

func add(a int) func(c int) int {
  var b = 5
  return func(c int) int {
    return a + b + c
  }
}
func main()  {
  f := add(10)
  fmt.Println(f(1), f(2))    // 16 17
}

这是复杂一点的例子

package main

import "fmt"

func calc(a int) (func(b int) int, func(c int) int) {
  add := func(b int) int {
    return a + b
  }
  minus := func(c int) int {
    return a - c
  }
  return add, minus
}
func main() {
  f1, f2 := calc(10)
  fmt.Println(f1(1), f2(2)) // 11 8
}

总结一下:闭包其实就是在一个函数中返回一个或多个匿名函数,并在匿名函数中做一些列操作,而外层的函数只是给它提供环境的(即一些变量)

注意:
由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包

defer注意事项
  • 函数不支持:嵌套、重载(函数名不能重复)、默认参数
  • 无需声明原型
  • 可以作为一种类型使用:a := Func1a()
defer

defer是延迟调用,即走到deferdefer语句不会被执行,而是会在一个特定的时间调用,这个时间就是return的时候调用,下面来个图片解释一下:
defer调用图画个图真不容易
既然它有怎样的功能和调用时间,那么他的作用就有了:资源清理、文件关闭、解锁、记录时间…很多用途.
在列一下它的特点:

  • 即使发生严重错误(程序崩溃)也会执行(前提:defer在产生崩溃前定义)
  • 支持匿名函数
defer的简单使用
package main

import "fmt"

func main()  {
  defer fmt.Println("ok!")
  fmt.Println("start")
  //start
  //ok!
}
结合函数使用
package main

import "fmt"

func fd() {
  fmt.Println("defer1")
}
func main() {
  /* ---------------------- 一般函数 ----------------------*/
  
  defer fd()
  
  /* ---------------------- 匿名函数 ----------------------*/
  
  defer func() {fmt.Println("defer2")}()
  fmt.Println("start")
  // start
  // defer2
  // defer1
}

这里主要展示一下多个defer时的顺序和怎么是defer与函数结合起来
两种现象:

package main

import "fmt"

func main() {
  
  for i := 0; i < 3; i++ {
    defer func() { fmt.Println(i) }()

  }
  // 3
  // 3
  // 3
  for i := 0; i < 3; i++ {
    defer fmt.Println(i)
  }
  // 2
  // 1
  // 0

}

我的解释:
第一个: for在退出前会i++,此时i=3。而使用defer传入同一个i实际上是执行同一块内存地址,所以最后所有结果都是3
第二个:这是一般的情况,我们可以预料到的,不作说明了

defer的注意事项

使用匿名函数时参数传入即拷贝了,值类型的话后面修改对它无效、引用类型有效

这句话有点难理解,写一段代码就明白了:

package main

import "fmt"

func main() {
  num1, num2 := 1, 3
  sli := []int{1, 2, 3, 4}
  defer func(a, b int) { fmt.Println(a + b) }(num1, num2)
  num1 = 100
  // 结果 ==> 4

  defer func(s[]int) { fmt.Println(s[0] + s[1]) }(sli)
  sli[0] = 100
  // 结果  ==> 102
}

这种现象实际上也是引用类型和值类型在函数传值的规则造成的。

构造函数

本质上就是一个函数,不过在函数内部做数据的声明和初始化,在外部以函数的形式调用,有点像面向对象语言中的构造函数。
比如,构造一个结构体(下面章节有):

package main   
               
import "fmt"   

type people struct {
  name string
  age  int
}

// 构造函数
func newPeople(name string, age int) *people {

  return &people{
    name: name,
    age:  age,
  }
}
func main() {

  p1 := newPeople("小张", 21)
  p2 := newPeople("小李", 22)
  fmt.Println(p1)       // &{小张 21}
  fmt.Println(p2)       // &{小李 22}

}

构造函数以new开头(约定俗成的规定)
构造函数的返回值可以是指针,也可以是具体的数据,但假如数据比较大时,使用指针更能减少内存开销。

方法

Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于其他语言中的this或者 self,但在go语言中以接收者类型的小写命名。格式:

fun (名字 接收者类型) 方法名 (参数) [返回值]{
  // ....
}

除了多一个括号,其他的与函数无异。以结构体为例,为其增加方法:


package main

import "fmt"

type dog struct {
  name string
}

// 使用dog作为接收者,以d命名
func (d dog) wang(msg string) {
  fmt.Printf("%s叫了一句“%s”", d.name, msg)

}
func main() {

  d := dog{name: "cxk"}
  // 调用方法
  d.wang("太美") // cxk叫了一句“太美”

}
指针接收者

上面我们的例子是使用值接者,方法得到的是拷贝(对于值类型数据来说),假如我们是要取一些数据的话,没有一点问题,但假如我们需要修改里面的数据时–直接拉闸。不过还是有方法的,那就是使用值类型接收者。


package main

import "fmt"

type dog struct {
  name string
}

func (d *dog) changeName(name string)  {
  d.name = name

}
func main() {

  d := dog{name: "cxk"}
  // 使用了指针接收者也能直接 . 操作
  d.changeName("kk")
  fmt.Println(d.name)    // kk

}

什么时候用指针接收者: 数据大时、要修改数据时、前面已经用了指针接收者(为了保持一致)

给任意类型添加方法

上面举例用的都是结构体,其他类型的数据展示一下,其实都一样。注意使用type关键字

package main

func (s string) len() int {
  // 错误示范
  return len(s)
}

func main()  {
  ...
}

正确使用:

package main

import "fmt"

// 自定义一个类型
type myString string

// 定义方法
func (m myString) len() int {
  return len(m)
}

func main() {
  // 使用
  s := myString("hello word!")
  fmt.Println(s.len())  // 11
}

结构体

go语言中没有类这个概念,但有结构体,在一定程度上可以使用面向对象的方法。
结构:

struct{
  字段名1 字段类型
  字段名2 字段类型
}

注意:字段名唯一,不能重复

结构体的简单使用

有名的结构体在全局定义,使用type关键字(见一些关键字)定义。
格式:type 名字 struct{...}

package main

import "fmt"

type data struct {
  name string
  age int
  hobby []string
}

func main()  {
  // 方法一
  var d1 data
  d1.name = "cxk"
  d1.age = 25
  d1.hobby = []string{"唱", "跳", "rap", "篮球"}
  // 方法二
  var d2 = data{name:"cxk",age:25,hobby:[]string{"唱", "跳", "rap", "篮球"}}
  // 方法三
  d3 := data{"cxk", 25, []string{"唱", "跳", "rap", "篮球"}}
  fmt.Println(d1)    // {cxk 25 [唱 跳 rap 篮球]}
  fmt.Println(d2)    // {cxk 25 [唱 跳 rap 篮球]}
  fmt.Println(d3)    // {cxk 25 [唱 跳 rap 篮球]}
}

可以看出,初始话时可以指定结构体里面的字段对应的值,也可以根据顺序来确定值。

匿名结构体

和函数一样,结构体也有匿名的结构体,对于只在一个函数中使用的结构体,或许使用匿名的更能提高效率。使用:

package main

import "fmt"


func main() {
  // 声明并初始化
  data := struct {
    name, note string
    age        int
  }{name: "xxx", age: 22, note: "ss"}
  fmt.Println(data)      // {xxx ss 22}
  fmt.Println(data.name) // xxx

}
结构体的操作
结构体查值

通过.取值

package main

import "fmt"

type data struct {
  name  string
  age   int
  hobby []string
}

func main() {
  var d = data{name: "xxx", age: 25, hobby: []string{"唱", "跳", "rap", "篮球"}}
  // 取值
  fmt.Println(d.hobby)
}

修改结构体的值

一般修改

package main

import "fmt"

type data struct {
  name  string
  age   int
  hobby []string
}

func main() {

  var d = data{name: "xxx", age: 25, hobby: []string{"唱", "跳", "rap", "篮球"}}
  // 修改
  d.age = 9000

  fmt.Println(d)
}

在函数中修改:

package main

import "fmt"

type data struct {
  name  string
  age   int
  hobby []string
}

func main() {

  var d = data{name: "xxx", age: 25, hobby: []string{"唱", "跳", "rap", "篮球"}}
  d.age = 9000

  change(&d)
  fmt.Println(d)

}

func change(d *data) {
  // 方式一,普通的指针修改值的方法
  (*d).name = "yyy"
  // 方式二,这是一种语法糖,得到的是内存地址对应的值
  d.age = 9000
}
结构体嵌套

结构体是没有继承的,但是可以进行嵌套。首先讲一下有字段名的结构体嵌套:

package main

import "fmt"


type data struct {
  name string
  age int
  hobby []string
  fans struct{
    name string
  }
}

/* 或者
type fans  struct {
    name string
  }

type data struct {
  name string
  age int
  hobby []string
  fans fans
}
*/

func main() {
  // 初始化方法一,使用点初始化嵌套结构体
  d1 := data{name:"cxk",age:25,hobby:[]string{"唱", "跳", "rap", "篮球"}}
  d1.fans.name = "nc"
  fmt.Println(d1)
  // 初始化方法二,使用struct匿名结构体初始化
  d2 := data{"cxk", 25, []string{"唱", "跳", "rap", "篮球"}, struct{ name string }{name: "nc"}}
  fmt.Println(d2)
}
省略字段名的结构体嵌套

使用这种方式可以夸结构体使用.取值,前提是外层结构体没有该字段、内部结构体中 只有一个结构体有该字段


package main

import "fmt"

type fans  struct {
    name, note string
  }
type data struct {
  name  string
  age   int
  hobby []string
  // 直接写上fans这个结构体,省略其的字段名
  fans
}

func main() {
    var d = data{name: "xxx", age: 25, hobby: []string{"唱", "跳", "rap", "篮球"}, fans: struct{ name, note string }{name: "n", note: "ll"}}
  // 注意: 可以这样取值,只要是外层没有的可以直接取到外层的内容,也可以d.fans.note
  fmt.Println(d.note)

}
利用结构体模拟继承

涉及方法部分见函数 >> 方法


package main

import "fmt"

type animal struct {
  name string
}
type dog struct {
  name string
  animal
}

func (a animal) move() {
  fmt.Printf("%s移动了一下\n", a.name)
}

func (d dog) wang() {
  fmt.Printf("%s:汪汪汪~\n", d.name)
}

func main() {
  d := dog{"泰迪", struct{ name string }{name: "狗"}}
  d.move() // 狗移动了一下
  d.wang() // 泰迪:汪汪汪~

}

这个现象非常像类中的继承。

结构体补充
结构体内存布局

结构体占用一块连续的内存。

package main         
                     
import "fmt"         
                     
type test struct {
  a, b, c int8
}

func main() {
  t := test{a: int8(1), b: int8(2), c: int8(3)}
  fmt.Println(&t.a)       // 0xc000020160
  fmt.Println(&t.b)       // 0xc000020161
  fmt.Println(&t.c)       // 0xc000020162

}

假如是字段对应的是更为复杂的数据类型,会内存的值会跳跃性增长,见进阶版本 >> 内存对齐
空结构体是不占用空间的

var v struct{}
fmt.Println(unsafe.Sizeof(v))  // 0
使用构造函数得到结构体

函数 >> 构造函数

为结构体添加方法

函数 >> 方法

匿名字段

虽然不常用,但是结构体的字段名是可以省略的,取值时以类型取值。由于结构体字段不能重复,所以省略字段后结构体中一种类型的数据只能存一次,使用场景:数据简单时(个人觉得还不如有字段名的)
使用例子:

package main

import "fmt"

type data struct {
  int
  string
}
func main()  {
  d := data{10086, "中国移动"}
  fmt.Println(d.int)    // 10086
  fmt.Println(d.string)  // 中国移动
}
结构体与json数据之间相互转换
  1. 结构体 ==> Json
    步骤:

    使用json.Marshal,需要import "encoding/json"
    判断是否出错
    string[]byte强制转换为字符串

    注意:结构体的字段也遵循可见性规则,所以想要使用json包里的函数处理结构体,就需要把字段首个字母大写,假如需要输出的是一个小写的,可以使用反引号(`)包起来这样一段字符串:json:"我想要的字段名",注意冒号后面是没有空格的。

    package main
    
    import (
      "encoding/json"
      "fmt"
    
    )
    type data struct {
      Name string `json:"name"`
      Age int `json:"age"`
    }
    
    func main()  {
      d := data{Name:"张三", Age:22}
      msg, er := json.Marshal(d)
      if er != nil{
        fmt.Println("Marshal异常:",er)
        return
      }
      fmt.Printf("%#v\n", string(msg))   // "{\"name\":\"张三\",\"age\":22}"
    }
    
  2. Json ==> 结构体
    步骤:

    先定义一个结构体
    把字符串转化为[]byte
    使用json.Unmarshal()

    注意:json.Unmarshal()的参数是[]byte和对应数据类型的指针

    package main
    
    import (
      "encoding/json"
      "fmt"
    )
    
    
    type data struct {
      Name string `json:"name"`
      Age  int    `json:"age"`
    }
    
    func main() {
      var a = `{"Name":"xxx", "Age": 100}`
      // d1用于接收json.Unmarshal反序列化后的数据
      var d1 data
      // 传入[]byte和指针
      e := json.Unmarshal([]byte(a), &d1)
      if e != nil {
        fmt.Print("Unmarshal异常:\n", e)
        return
    
      }
      fmt.Printf("%#v\n", d1) // main.data{Name:"xxx", Age:100}
    }
    

接口

interface(接口)是一种类型。只要实现了接口的内所有方法,这个类型就可以是这个接口的类型。
在使用接口之前,我们想一想,为什么要使用接口?
之前所学,假如一个函数定义时:func test (a int){...},那么我们传值的时候只能传int型,但在实际中,我们需要的是一种可以插拔的开发方式,即不论你传入什么,我可以调用一个传入值的一个指定的方法即可。就好像,使用fmt.Println,无论传入int还是string都可以打印出来,这就是接口的作用。而且go不推荐面向对象编程,而是使用面向接口编程。
接口和类型的关系

一个接口可以有多个类型实现
一个类型可以实现多个接口

接口简单使用

使用:

type 接口名 interface {
  方法1()
  方法2()
}

// 后面实现方法1和方法2

例子:

package main

import "fmt"

type speaker interface {
  speak()
}
type dog struct{}
type cat struct{}

func (d dog) speak() {
  fmt.Println("汪汪汪~")
}
func (c cat) speak() {
  fmt.Println("喵喵喵~")
}

func da(speaker2 speaker) {
  speaker2.speak()
}

func main() {
  d := dog{}
  c := cat{}
  da(d)
  da(c)

}

例子中,调用da函数时传入的是dogcat结构体,但这两个都实现的speaker的所以方法,所以又是speaker类型。

使用值接收者实现方法和使用指针接收者实现方法

由于在实现方法时接收者有两种形式:值接收者和指针接收者。这两种接收方法都可以用来实现接口,但是实现完后的两者是有所差别的。


package main

import "fmt"

type animal interface {
  move()
}

type duck struct {
  name string
}
type dog struct {
  name string
}

// 这是使用值接收者完成接口
func (d duck) move() {
  fmt.Println(d.name, "move")
}

// 这是使用指针接收者完成接口
func (d *dog) move() {
  fmt.Println(d.name, "move")
}

func run(animal2 animal) {
  animal2.move()
}

func main() {
  // 值接收者使用
  var duck1 = duck{name: "丑小鸭"}
  var duck2 = &duck{"唐老鸭"}
  run(duck1)     // 丑小鸭 move
  run(duck2)     // 唐老鸭 move
  
  // 指针接收者使用
  var dog1 = dog{name: "二哈"}
  var dog2 = &dog{name: "泰迪"}
  // run(dog1)    // 报错:dog does not implement animal (move method has pointer receiver)
  fmt.Println(dog1) // {二哈}
  run(dog2)         // 泰迪 move

}

由于以后要操作数据是通常有修改等操作,而go又使用面向接口编程,所以在使用值接收者的时候要特别注意。

接口嵌套

假如a接口要实现m1方法,b接口要实现m2方法,而c接口要实现m1和m2两个方法,这时就可以把a和b嵌套在c中。


package main

import "fmt"

type mover interface {
  move()
}
type eater interface {
  eat()
}
type animal interface {
  mover
  eater
}

type duck  struct {
  name string
}

// 实现animal接口
func (d duck) move()  {
  fmt.Println("move~")

}
func (d duck) eat()  {
  fmt.Println("eat")

}
var (
  a animal
  e eater
  m mover
)

func main()  {
  d := duck{name: "丑小鸭"}
  a = d
  e = d
  m = d
  fmt.Println(a, e, m)

}

duck结构体实现了animal接口的同时,也实现了eatermover接口,因为animal嵌套了eatermover

接口在内存

假如运行下面代码:

package main

import "fmt"

type animal interface {
  move()
}
type dog struct {
  name string
}
type duck struct {
  name string
}

func (d dog) move()  {
  fmt.Printf(d.name, "move~")

}
func (d duck) move()  {
  fmt.Printf(d.name, "move~")

}
func main()  {
  var a animal
  fmt.Printf("type: %T  value: %v\n", a , a)  // type: <nil>  value: <nil>
  d := dog{name:"泰迪"}
  a = d
  fmt.Printf("type: %T  value: %v\n", a , a)  // type: main.dog  value: {泰迪}  d := dog{name:"泰迪"}
  duck1 := duck{name:"丑小鸭"}
  a = duck1
  fmt.Printf("type: %T  value: %v\n", a , a)  // type: main.dog  value: {泰迪}

}

a的变化过过程:
演示图

空接口


接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。
空接口类型的变量可以存储任意类型的变量。
空接口有两个应用,一:作为函数参数类型,二:作为map的值。

package main

import "fmt"

func f1(a interface{})  {
  fmt.Printf("%T\n", a)


}
var data map[string] interface{}

func main()  {
  f1("av")
  f1(123)
  f1(false)

  data = make( map[string] interface{}, 10)
  data["id"] = 123
  data["name"] = "cxk"
  data["hobby"] = [...]string{"c", "t", "rap"}
  fmt.Println(data)
}
接口类型断言

由于接口的可以存储任意类型的值,在实际使用中我们可能需要获取值得类型,以方便我们使用。
这个时候就可以使用类型断言,其语法格式:

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

假如使用一个变量接收:

func show(a interface{})  {
  t := a.(string)  // panic: interface conversion: interface {} is int, not string
  fmt.Printf(t)
}

假如我们传入的不是一个string,就会报错,因为t := a.(string)一句代码。同时go也有不报错的处理方式:

使用两个变量接收
有点像之前的map

func show(a interface{})  {

  t, ok := a.(string)
  if ok{
    fmt.Println(t)
  }else {
    fmt.Println("string")
  }

}

t, ok := a.(string)中的tok分别对应者a的值(ok为false时为断言类型的零值)和是否为断言类型。
使用switch
这里需要注意的是:.(type)而不是具体的类型,然后在case中写具体类型。


func show(a interface{}) {

  switch t := a.(type) {
  case string:
    fmt.Println("string", t)

  case int:
    fmt.Println("int", t)

  case bool:
    fmt.Println("bool", t)

  }

}

流程控制

在我们为引入流程控制之前,我们写的程序都是从上到下,一条路走到黑。而流程控制可以控制程序走其他的路,甚至可以再走一遍。

if

if即判断语句,控制是否要执行该分支的代码
基本格式:

if 条件一{
  <代码>
}else if 条件二{
  <代码>
  ....
}else{
  <代码>
}

其中else if的部分可以不要,作为只有两个选择的单分支来用。也可以使用多个else if作为进行多个分支的实现。

if中的{},不能随便换行,一下截个图画出必须同行的花括号

花括号提醒

图中的红色波浪线是Goland的提醒,是因为逻辑有问题。没有必要判断,这里是为了演示,才….
一般来说,花括号和if 、else 同行就对了

if 的一种特殊写法

有时候,我们只需要在if判断是才用到的某些变量,没有必要让其长时间占用内存,在go中可以在if开始时声明初始化变量。那么在退出时,go就会把变量销毁、回收内存。
写法:

package main

import "fmt"

func main()  {
  name := "lczmx"
  if age := 18; name == "lczmx" && age ==18{
    fmt.Println("true")
  }else {
    fmt.Println("false")
  }
  //fmt.Println(age)
}

假如,在if外面再使用if里面的变量时,就会报错,提示变量未声明。其实这个问题涉及作用域的问题,下面的for也有作用域的应用。

for

for循环可以控制程序重复执行某一段代码。
它的基本格式:

for 初始语句;条件表达式;结束语句{
    循环体语句
}

只有条件表达式为true,循环体语句就会一直重复运行
如:

package main

import "fmt"

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

这段代码会打印出0到9
除了这种格式外,还有3种for的变种

  1. 省略初始语句
    适用于使用for作用域之外的变量进行判断

    package main
    
    import "fmt"
    
    func main()  {
      age := 2
      for ; age < 10 ; age++{
        fmt.Println(age)
      }
      
    }
    

    需要注意的是:条件表达式之前的分号是不能删除的。

  2. 省略结束语句
    结束语句只用++--,即自增一或自减一,但是我们有时候需要自增2,甚至还有其他千奇百怪的需求。因此省略结束语句,把变量的改变过程写在循环体语句里,可以实现这些需求。如:

    package main
    
    import "fmt"
    
    func main() {
      name := "lczmx"
      length := len(name) // 取长度
      for length < 10 {
        length += 2     // 自增2
        fmt.Println(length)
      }
    }
    
  3. 条件表达式
    因为在go语言中,只有for,那么其他语言中的while,它怎么实现呢?其实就是只留一个for,把最后的条件表达式也去掉。

    package main
    
    import "fmt"
    
    func main() {
      score := 60
      for {
        if score > 80{
          break
        }
        fmt.Println(score)
        score += 10
      }
    }
    

    特别注意:一定要有跳出循环的判断,否者就会一直死循环
    这里用到了break跳出循环,除此外还有continuegotoreturnpanic,下面介绍前三个,后面两个以后会提到

跳转语句(break、goto、continue)

要说到这三个跳转语句,就必须提到另一个知识点:标签,标签就是几个字符加上冒号(如:LABEL:)。在我看来,标签就是一个标记点。当标签与跳转语句结合时,我们就可以控制程序跳到我们标记的那个点上,而不是直接跳到for外面的后面。
先说一下注意事项:

标签分大小写,定义而不用会报错
breakcontinue可以保存状态
goto不能保存状态

break

break是跳出当前层的循环,如:

package main

import "fmt"

func main() {
  score := 60
  for i := 1; i < 5; i++ {
    for {
      if score > 80 {
        break
      }
      fmt.Println("2: ",score)
      score += 10
    }
    fmt.Println("1: ", score)
  }
}

结果

2:  60
2:  70
2:  80
1:  90
1:  90
1:  90
1:  90

很明显,break只跳出了一层,这和其他语言一样。

break + 便签

break加上标签,那就发生了大大的不同:break可以跳出任意多层!它会跳到与便签同级的那一层。如下面代码:

package main

import "fmt"

func main() {
  label:
  for i := 1; i < 5; i++ {
    for j := 1; j < 5; j++{
      fmt.Println("2: j =",j)
      if i == 3 {
        fmt.Println("跳出")
        break label
      }
    }
    fmt.Println("1: i =", i)
  }
  fmt.Println("end")
}

结果:

2: j = 1
2: j = 2
2: j = 3
2: j = 4
1: i = 1
2: j = 1
2: j = 2
2: j = 3
2: j = 4
1: i = 2
2: j = 1
跳出
end

可以看到,打印了“跳出”后,直接打印“end”,此时break已经跳出了2层。

continue

continue就是跳到下面的代码,执行下一次循环,即continue不会跳出循环。

注意:标签一定要在for之前,否者break会找不到标签,报错”break label not defined”

package main

import "fmt"

func main() {
  for i := 1; i < 5; i++ {
    if  i == 2 {
        continue
      }
    fmt.Println("i =", i)
  }
}

结果

i = 1  // 跳过了2
i = 3
i = 4
continue + 标签

break类似,但是不会跳出循环,之后跳到标签所在的那一层

package main

import "fmt"

func main() {
  label:
  for i := 1; i < 4; i++ {
    for j := 1; j < 3; j++{
      fmt.Println("2: j =",j)
      if i == 2 {
        fmt.Println("跳过")
        continue label
      }
    }
    fmt.Println("1: i =", i)
  }
  fmt.Println("end")
}

结果

2: j = 1
2: j = 2
1: i = 1
2: j = 1
跳过
2: j = 1
2: j = 2
1: i = 3
end

break一样,continue的标签也要在for之前,否者会报错:”continue label not defined”

goto

goto必须要加标签名,因为goto是用于调整执行位置的,我们不指定位置的话,程序根本不知道往哪里跳。
使用:

package main

import "fmt"

func main() {
  for i := 1; i < 4; i++ {
    for j := 1; j < 3; j++{
      fmt.Println("2: j =",j)
      if i == 2 {
        fmt.Println("调整")
        goto label
      }
    }
    fmt.Println("1: i =", i)
  }
  label:  // 调整到这里
  fmt.Println("end")
}

结果

2: j = 1
2: j = 2
1: i = 1
2: j = 1
调整
end

注意:标签一定要在for之后,否者你会一直重复执行这一段代码!

for range

我之前在byte和rune类型那里用过一次for range,其实for range就是同时获取要遍历对象的索引和元素。

package main

import "fmt"

func main() {
  l := [4]string{"a","b","c","d"}  // 定义长度为4的数组,元素为字符串
  for i, v := range l{
    fmt.Println(i, v)
  }
}

结果

0 a
1 b
2 c
3 d

很明显,for range获取的就是索引和元素本身。假如我们使用for range遍历某些数据时,不需要索引的话,可以使用匿名变量_进行丢弃。
假如只想那索引或mapkey时,可以只使用一个变量接收:

package main

import "fmt"

func main() {
  l := [4]string{"a","b","c","d"}  // 定义长度为4的数组,元素为字符串
  for i := range l{
    fmt.Println(i)
  }
}

结果:

0
1
2
3

假如想for range中修改值:

package main

import "fmt"

func main() {
  a := []int{100, 2, 3, 4}
  fmt.Println("修改前", a) //修改前 [100 2 3 4]
  
  for i := range a {
    if i == 0 {
      a[i] = 1
    }
  }
  fmt.Println("修改后", a) //修改后 [1 2 3 4]

}

switch

switch 语句用于基于不同条件执行不同动作,每一个 case 分支都是唯一的,从上至下逐一测试,直到匹配为止。
switch 语句执行的过程从上至下,直到找到匹配项,匹配项后面也不需要再加 break
switch 默认情况下 case 最后自带 break 语句,匹配成功后就不会执行其他 case,如果我们需要执行后面的 case,可以使用 fallthrough
switch 可以的带一个default分支,没有以上都没有匹配到时执行,因此,switch只能有一个default分支
switch 匹配多个条件是用,&&bool时)隔开

switch主要有以下几种格式

  1. 格式一

    package main
    
    import "fmt"
    
    func main()  {
    
      finger := 3
      switch finger {
      case 1:
        fmt.Println("大拇指")
      case 2:
        fmt.Println("食指")
      case 3:
        fmt.Println("中指")
      case 4:
        fmt.Println("无名指")
      case 5:
        fmt.Println("小拇指")
      default:
        fmt.Println("无效的输入!")
    
    }
      
    }
    
  2. 格式二

    package main
    
    import "fmt"
    
    func main()  {
      switch n := 7; n {
      case 1, 3, 5, 7, 9:
        fmt.Println("奇数")
      case 2, 4, 6, 8:
        fmt.Println("偶数")
      default:
        fmt.Println(n)
      }
    }
    
  3. 格式三

    package main
    
    import "fmt"
    
    func main()  {
      age := 30
      switch {
      case age < 25:
        fmt.Println("好好学习吧")
      case age > 25 && age < 35:
        fmt.Println("好好工作吧")
      case age > 60:
        fmt.Println("好好享受吧")
      default:
        fmt.Println("活着真好")
      }
    }
    

运算符

算术运算符

运算符 说明
` `
- 相减
* 相乘
/ 相除
% 求余

注意:++(自增)和--(自减)在Go语言中是单独的语句,并不是运算符。是不能fmt.Println(i++)

关系运算符

运算符 说明
`== `
!= 两个值是否不相等
> 左边值是否大于右边值
>= 左边值是否大于等于右边值
`< `
<= 左边值是否小于等于右边

逻辑运算符

运算符 说明
&& 逻辑 AND 运算符。都是True才为True,否则为 False
`
! 逻辑 NOT 运算符。True变为 FalseTrue变为False

位运算符

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

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

赋值运算符

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

错误处理

go语言目前是没有异常处理的,但是可以理由defer的特性,做一些补救。
使用的是panic/recover模式
panic是一种在运行时的错误,不是编译时的错误(会编译不过去),也可以使用panic函数主动抛出错误。
panic格式:panic("错误信息")
recover是用来接收panic错误的,需要与defer结合,否者程序都崩溃了拿什么接收。

下面展示一下使用方法:

package main

import "fmt"

func add(a, b int) (res int) {
  defer func() {
    if err := recover(); err != nil {
      fmt.Println("捕捉到异常:", err)
      res = 0
    }
  }()
  res = a + b
  panic("搞事情")
  return
}

func main() {
  res := add(1, 2)
  fmt.Println(res)
}

结果:

捕捉到异常: 搞事情
0

defer要在产生panic之前的代码定义,否者是没用滴

包package

包是go语言代码的集合,一个包可以包含多个.go文件,在导入时,只需文件对应的包即可,无需把全部的.go文件都带上。
包可以是内置的如:fmtjson等,也可以是我们自定义的main包,需要注意的是,go语言中一个程序必须有一个main包和main包内有一个main函数,其他包不需要定义main函数。文件名与包无关。

包的导入

在导入包之时,我们要注意两个:

  • 包的路径

    包的路径是从GOPATH/src开始算

  • 我需要的标识符是否可见

    导如包时遵循可见性规则(即大写才外界可见)

  

举一个例子

假如有这样一个文件GOPATH/src/github.com/lczmx/studygo/calc/calc.go内容如下:

package calc

func Add(a, b int) int {
  return a + b

}

在主程序中引入:

package main

import (
  "fmt"
  "github.com/lczmx/studygo/calc"  // 到文件夹即可
)

func main() {
  a := 1
  b := 2
  res := calc.Add(a, b)
  fmt.Println(res)  // 3

}
  1. 单行导入

    import "fmt"
    
  2. 多行导入
    好像例子中一样

    import (
        "fmt"
        "json"
    )
    

包的别名

假如:github.com/lczmx/studygo/test为导入路径,而包名是calc,使用时还是可以用calc.的方式用,但就看不出calc是从哪里导入的,此时就可以使用别名,解决问题。
格式:

import 别名 "路径"
// 或者
import (
  别名 "路径"
)

举个例子

package main

import (
  "fmt"
  calc "github.com/lczmx/studygo/test"
)

func main() {
  a := 1
  b := 2
  res := calc.Add(a, b)
  fmt.Println(res)  // 3

}
“_”别名

使用_作为别名,意思就是只导入不使用,我们知道假如导入包而不使用的话会报错。那为什么只导入不使用,这是因为可以利用的go语言的一些特性做初始化(运行init函数,下面讲),如:链接数据库等。
使用:import _ calc

“.”别名

使用.作为别名,即不需要用包名.的方式了,可以直接使用,如

package main

import (
  "fmt"
   . "github.com/lczmx/studygo/test"
)

func main() {
  a := 1
  b := 2
  res := Add(a, b)  // 直接使用
  fmt.Println(res)  // 3

}

虽然这看起来方便,但很容易冲突和产生混淆,不建议使用。

导入包时执行顺序

导入一个包是有一定的执行顺序的,了解了其导入的顺序才能更好的理解go语言。
下面使用一张图表示一下:
导入顺序演示
总的来说,导入顺序不外乎import -> 声明常量、变量等 -> init()函数。对于图中的执行结果显而易见:

2.718
area:  12
hello
3

init函数是go语言中的一个特定的函数,它没有参数也没有返回值,会在程序运行时自动调用

定义包

定义一个包将使用一个关键字package,如经常要写的package main

注意:一个文件加下,只能有一个包,否则提示found packages ...

go module

go module是Go1.11版本之后官方推出的版本管理工具,并且从Go1.13版本开始,go module是Go语言默认的依赖管理工具。
也就是说,有了go module之后我们就不需要一定把文件放在GOPATH下了,而且对于管理和下载不同版本的第三方库更加得心应手了。

go module准备工作

因为目前版本(1.14)的go module并不是默认开启的,未来或许会默认开启,但是在这个版本中我们需要修改配置开启:

go env -w GO111MODULE="on",GO111MODULE的三个模式:

"off": 禁用模块支持,编译时会从GOPATHvendor文件夹中查找包
"on": 启用模块支持,编译时会忽略GOPATHvendor文件夹,只根据 go.mod下载依赖
"auto": 当项目在$GOPATH/src外且项目根目录有go.mod文件时,开启模块支持

除此之外,因为我们需要的下载的很多第三方包在Google的服务器上,所以需要设置Go 模块代理(Go module proxy),其作用是用于使 Go 在后续拉取模块版本时能够脱离传统的 VCS 方式,直接通过镜像站点来快速拉取。

go env -w GOPROXY=https://goproxy.cn,direct
go env -w GOSUMDB=sum.golang.google.cn
go module使用
  • 既有项目
    • 初始化
    • go get查找并记录当前项目的依赖
  • 新建项目
    • 初始化
    • 修改go.mod ,go get下载依赖,也可以直接go get下载依赖

在项目目录下执行go mod init,生成一个go.mod文件。
执行go get,查找并记录当前项目的依赖,同时生成一个go.sum记录每个依赖库的版本和哈希值。
新项目
对于一个新创建的项目,我们可以在项目文件夹下按照以下步骤操作:

执行go mod init 项目名命令,在当前项目文件夹下创建一个go.mod文件。
手动编辑go.mod中的require依赖项或执行go get自动发现、维护依赖。

初始化

使用go mod init命令即可初始化,届时会在项目根目录下生成一个文件go.mod,一开始只会记录项目的路径和go版本

拉取了新的依赖

使用go get命令,会把当前依赖全部下载放到GPTH/pkg下。同时还会修改go.mod文件,假如是第一次使用go get还会生成go.sum文件,用来记录模块版本的 SHA-256 哈希值。
运行go get -u将会升级到最新的次要版本,不仅如此go get还可以指定下载的版本。

命令例子 说明
go get golang.org/x/text@latest 拉取最新的版本,若存在tag,则优先使用
go get golang.org/x/text@master 拉取 master 分支的最新 commit
go get golang.org/x/text@v0.3.2 拉取 tag 为 v0.3.2 的 commit
go get golang.org/x/text@342b2e 拉取 hash 为 342b231 的 commit,最终会被转换为 v0.3.2
go module命令
命令 说明
go mod download 下载依赖的module到本地cache(默认为$GOPATH/pkg/mod目录)
go mod edit 编辑go.mod文件
go mod graph 打印模块依赖图
go mod init 初始化当前文件夹, 创建go.mod文件
go mod tidy 增加缺少的module,删除无用的module
go mod vendor 将依赖复制到vendor下
go mod verify 校验依赖
go mod why 解释为什么需要依赖
补充
  1. go.mod文件
    go.mod的文件内容的详解:

    module github.com/eddycjy/module-repo
    
    go 1.13
    
    require (
        example.com/apple v0.1.2
        example.com/banana v1.2.3
        example.com/banana/v2 v2.3.4
        example.com/pear // indirect
        example.com/strawberry // incompatible
    )
    
    exclude example.com/banana v1.2.4
    replace example.com/apple v0.1.2 => example.com/fried v0.1.0 
    replace example.com/banana => example.com/fish
    
    • module:用于定义当前项目的模块路径。
    • go:用于标识当前模块的 Go 语言版本,值为初始化模块时的版本,目前来看还只是个标识作用。
    • require:用于设置一个特定的模块版本。
    • exclude:用于从使用中排除一个特定的模块版本。
    • replace:用于将一个模块版本替换为另外一个模块版本。
    • 另外你会发现 example.com/pear 的后面会有一个indirect标识,indirect 标识表示该模块为间接依赖,也就是在当前应用程序中的 import 语句中,并没有发现这个模块的明确引用,有可能是你先手动 go get 拉取下来的,也有可能是你所依赖的模块所依赖的,情况有好几种。

    参考Go Modules 终极入门

  2. go get的参数说明

    参数 说明
    -d 只下载不安装
    -f 只有在你包含了 -u 参数的时候才有效,不让 -u 去验证 import 中的每一个都已经获取了,这对于本地 fork 的包特别有用
    -fix 在获取源码之后先运行 fix,然后再去做其他的事情
    -t 同时也下载需要为运行测试所需要的包
    -u 强制使用网络去更新包和它的依赖包
    -v 显示执行的命令

一些关键字

这里记录一些其他地方没有讲的关键字,集中起来好查找。

type

type关键字有两个作用:自定义别名、自定义类型。
自定义别名如:byteuint8的别名,作用是方便人阅读、重构等,本质上还是原来的类型。
自定义类型不太一样,这是确实存在的类型,虽然使用基于内置数据类型,但可以为自定义的类型增加方法

自定义别名

格式: type 别名 = 类型

package main

import "fmt"

type myInt = int    // 别名

func main()  {
  var perm myInt 
  perm = 777
  fmt.Println(perm)      // 777
  fmt.Printf("%T\n", perm)  // int

}

go1.9之前是用这种方式定义的:type byte uint8

自定义类型

格式:type 新类型名 类型

package main

import "fmt"

type newInt int
func main()  {
  var num newInt
  num = 20
  fmt.Println(num)        // 20
  fmt.Printf("%T\n", num) // main.newInt

}

新的类型是包名.定义时的名称(如:main.newInt

一些规则

可见性规则

可见性规则是相对与其他包来说的,对于自己的包的数据来说,一切数据都能拿到。但是假如那其他包里的数据则需要一定的规则才能拿到,这就是可见性规则。
比如,我在main包中拿fmt包了的一个函数:
fmt中的print.go文件中有这样一段代码:

func newPrinter() *pp {
  p := ppFree.Get().(*pp)
  p.panicking = false
  p.erroring = false
  p.wrapErrs = false
  p.fmt.init(&p.buf)
  return p
}

但假如在我的main包使用fmt.newPrinter会报错cannot refer to unexported name fmt.newPrinter
这是因为在go语言中,如果你想哪一个内容外界可见,就必须要大写,如:

func Println(a ...interface{}) (n int, err error) {
  return Fprintln(os.Stdout, a...)
}

另外,一般来说假如外界可见的话,需要在声明前增加一个备注且格式为标识符(如变量名)+ 空格 + 解释语,这样你定义的函数或其他东西就能通过go doc 命令查看你的注释了。

// Print formats using the default formats for its operands and writes to standard output.
// Spaces are added between operands when neither is a string.
// It returns the number of bytes written and any write error encountered.
func Print(a ...interface{}) (n int, err error) {
  return Fprint(os.Stdout, a...)
}

输入go doc fmt.Print, 输出:

package fmt // import "fmt"

func Print(a ...interface{}) (n int, err error)
    Print formats using the default formats for its operands and writes to
    standard output. Spaces are added between operands when neither is a string.
    It returns the number of bytes written and any write error encountered.

结构体的字段也要遵循可见性规则

所以假如我们需要json序列号一个结构体的话,需要把它的字段改为大写,假如想要输出的是小写的话,应该指定一个tag:`json:"xxx"`,如:

package main

import (
  "encoding/json"
  "fmt"

)
type data struct {
  Name string `json:"name"`
  Age int `json:"age"`
}

func main()  {
  d := data{Name:"张三", Age:22}
  msg, er := json.Marshal(d)
  if er != nil{
    fmt.Println("Marshal异常:",er)
    return
  }
  fmt.Printf("%#v\n", string(msg))   // "{\"name\":\"张三\",\"age\":22}"
}

文件操作

文件操作即读写文件,假如我要操作的文件名称为config.txt:

[user_info]
username = 12345
pwd = xxx

文件–读取

初始版本

使用os.Open("文件路径"),其有两个返回值:*fileerr*file是文件句柄,可以对它读写、关闭等操作,err判断是否报错如io.EOFos.PathError等。
打开文件后记得关闭文件,以减少性能的浪费,使用的是Clone()函数,而且常常使用defer延迟调用。
在读取文件时用的是Read()函数,传入的值为byte的数组,大小自定,用于接收读取到的数据,其返回值n int, err error,n:本次读了多少, err: 异常
例子:

package main

import (
  "fmt"
  "os"
)

func main()  {
  file, err := os.Open("./config.txt")
  // 判断有无报错
  if err != nil{
    fmt.Printf("error: %T\n", err)
    return

  }
  defer file.Close()  // 关闭文件
  var s [22]byte
  n ,r := file.Read(s[:])
  if r != nil{
    fmt.Printf("error: %T\n", r)
    return

  }
  fmt.Println("已经读取", n)
  fmt.Println(string(s[:]))
  
}

结果:

已经读取 22
[user_info]
username =

从结果中可以看出,一次性读了22个字节,但是文件不止22个字节,而且假如不知道文件的大小怎么搞,所以有新的方法,那就是循环读取:

package main

import (
  "fmt"
  "io"
  "os"
)

func main()  {
  file, err := os.Open("./config.txt")
  if err != nil{
    fmt.Printf("error: %T\n", err)
    return
  }
  defer file.Close()
  var content [] byte
  var temp [22]byte


  for {
    n, r := file.Read(temp[:])

    // 开始判断有没有读取完
    if r == io.EOF {
      fmt.Println("文件已经读取完了")
      break
    }
    // 处理错误
    if r != nil {
      fmt.Printf("error: %T\n", r)
      return
    }
    content = append(content, temp[:]...)
    fmt.Println("已读取",n)

  }
  fmt.Println("文件内容为:")
  fmt.Print(string(content[:]))  // 会连同`\n`打印


}

结果:

已读取 22
已读取 16
文件已经读取完了
文件内容为:
[user_info]
username = 12345
bufio

虽然可以使用for来循环读取文件,但还要自定义一个中间变量接收,显然有点麻烦,所以就有了bufiobufio是在file的基础上封装了一层API,支持更多的功能。

这里调用的是bufio.NewReader()传入文件句柄,返回Reader结构体,而Reader结构体具有很多方法,如:ReadString根据某个字符读取。更多看go标准库

package main

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

func main()  {
  file, err := os.Open("./config.txt")
  if err != nil{
    fmt.Printf("error: %T\n", err)
  }
  defer file.Close()
  var content string
  reader := bufio.NewReader(file)

  for {
    line, err := reader.ReadString('\n') // 注意是字符`\n`, 返回string和error
    if err == io.EOF{
      fmt.Println("文件读取完了")
      break
    }
    if err != nil {
      fmt.Printf("error: %T\n", err)
    }
    content += line
  }
  fmt.Println("读取内容为")
  fmt.Print(content)
}
ioutil

io/ioutil包的ReadFile方法能够读取完整的文件,只需要将文件名(路径)作为参数传入。返回值为[]byteerror

package main

import (
  "fmt"
  "io/ioutil"
)

func main()  {
  content, err := ioutil.ReadFile("./config.txt")
  if err != nil{
    fmt.Printf("error: %T\n", err)
  }
  fmt.Println(string(content[:]))

}

文件–写入

方法一:os.OpenFile()

os.OpenFile()函数能够以指定模式打开文件,从而实现文件写入相关功能。

func OpenFile(name string, flag int, perm FileMode) (*File, error) {
  ...
}

参数:

name: 文件名
flag:打开的模式
perm:文件权限,使用的是linux的方法,用一个八进制数表示(如:0666)

打开模式 说明
os.O_RDONLY 以只读方式打开文件
os.O_WRONLY 以只写方式打开文件
os.O_RDWR 以读写方式打开文件
os.O_APPEND 写入时将数据追加到文件中
os.O_CREATE 如果不存在,创建一个新文件
os.O_EXCL 与O_CREATE一起使用,文件必须不存在
os.O_SYNC 为同步I / O打开
os.O_TRUNC 打开时清空文件

这么多个模式,有些是必须要写的,如下:
必须要填一个的有:O_RDONLYO_WRONLYO_RDWR
选择添加的:O_APPENDO_CREATE O_EXCLO_SYNCO_TRUNC

填入多个模式时使用|作为链接,如os.O_WRONLY|os.O_CREATE ==> 文件存在就只写;文件不存在则创建,然后只写。

例子:


package main

import (
  "fmt"
  "os"
)

func main() {
  file, err := os.OpenFile("./test.txt", os.O_CREATE|os.O_WRONLY, 0666)

  if err != nil {
    fmt.Printf("error: %T\n", err)
    return
  }
  defer file.Close()
  file.Write([]byte("user_name = xxx\n"))
  file.WriteString("pwd = xxx")

}

file.Write()传的是[]byte,而file.WriteString()传的是字符串,需要注意的是,这两个函数都有两个返回值(n int, err error

方法二:bufio.NewWriter

package main

import (
  "bufio"
  "fmt"
  "os"
)

func main() {
  file, err := os.OpenFile("./test.txt", os.O_WRONLY| os.O_APPEND, 0666)
  if err != nil {
    fmt.Printf("error: %T", err)
    return
  }
  defer file.Close()
  writer := bufio.NewWriter(file)
  writer.WriteString("\nnote = xxx") // 写入缓存
  writer.Flush()                   // 缓存内容写入文件

}
方法三:ioutil.WriteFile

package main

import (
  "fmt"
  "io/ioutil"
)

func main() {
  str := "test"
  err := ioutil.WriteFile("./test.txt", []byte(str), 0666)
  if err != nil {
    fmt.Printf("error: %T\n", err)
    return
  }
  
}

反射

反射的概念是由Smith在1982年首次提出的,主要是指程序可以访问、检测和修改它本身状态或行为的一种能力。
在go语言中,凡是是通过reflect包实现的

获取类型TypeOf

获取传入值的类型使用的是reflect.TypeOf(),由于TypeOf的形参使用的是一个空接口,在接口哪里,我们知道,接口保存着实现接口的类型和对应的值,使用TypeOf()获取的是类型。但是go语言中有type关键字,我们可以根据已有的类型自定义类型,所以reflect的类型分namekind,如:

type people struct {
  name string
}
// name: people kind: struct

使用reflect.TypeOf()函数可以获得任意值的类型对象(reflect.Type),程序通过类型对象可以访问任意值的kindname

仅仅使用type定义类型才有name,否则为空

Name()和 Kind()
package main

import (
  "fmt"
  "reflect"
)

type people struct {
  name string
}
// name: people kind: struct
type s int

func main()  {
  var p = people{}
  //var b s
  a := reflect.TypeOf(p)

  fmt.Println("name:", a.Name(), "kind:", a.Kind())    // name: people kind: struct
}

获取值ValueOf

使用reflect.ValueOf返回的是reflect.Value结构体,

package main

import (
  "fmt"
  "reflect"
)

func main() {
  var a = int8(2)
  t := reflect.ValueOf(a)
  k := t.Kind()
  fmt.Println(t, k)     // 2 int8
  fmt.Printf("%T\n", t) // reflect.Value

}

从例子中可以看出,我们虽然能够拿到原来传进去的值,但是值得类型不是原来的类型,而是reflect.Value,所以我们需要转换:

package main

import (
  "fmt"
  "reflect"
)

func main() {
  var a = int8(2)
  t := reflect.ValueOf(a)
  k := t.Kind()

  switch k {
  // reflect.Int8等见下面讲述
  case reflect.Int8:
    v := int8(t.Int())
    fmt.Printf("%d -- %T\n", v, v) // 2 -- int8
  case reflect.Int16:
    v := int16(t.Int())
    fmt.Printf("%T\n", v)
  case reflect.Int:
    v := int(t.Int())
    fmt.Printf("%T\n", v)
  }

}

reflect.Int8等是reflect包中自定义的类型,因为不同类型之间是不能判断的,即是不能使用int8等内置的类型和reflect.Value进行判断,而是要使用reflect包内置的类型判断,下面 给出源码中的Kind

// A Kind represents the specific kind of type that a Type represents.
// The zero Kind is not a valid kind.
type Kind uint

const (
  Invalid Kind = iota
  Bool
  Int
  Int8
  Int16
  Int32
  Int64
  Uint
  Uint8
  Uint16
  Uint32
  Uint64
  Uintptr
  Float32
  Float64
  Complex64
  Complex128
  Array
  Chan
  Func
  Interface
  Map
  Ptr
  Slice
  String
  Struct
  UnsafePointer
)

上面的例子中还涉及了一些reflect.Value的一些方法,下面列出它门

方法 返回值 说明
Interface() interface {} 将值以 interface{} 类型返回,可以通过类型断言转换为指定类型
Int() int64 将值以 int 类型返回,所有有符号整型均可以此方式返回
Uint() uint64 将值以 uint 类型返回,所有无符号整型均可以此方式返回
Float() float64 将值以双精度(float64)类型返回,所有浮点数(float32float64)均可以此方式返回
Bool() bool 将值以 bool 类型返回
Bytes() []bytes 将值以字节数组 []bytes 类型返回
String() string 将值以字符串类型返回
IsNil()和IsValid()和IsZero

除了上面的表格之外的方法,reflect.Value还有几个常用的方法。
看一下它们的签名:

  • isNil()
    func (v Value) IsNil() bool
    IsNil(): 值是否为nil注:必须是通道、函数、接口、映射、指针、切片之一,否则报错

  • isValid()
    func (v Value) IsValid() bool
    IIsValid返回v是否持有一个值。如果v是Value零值会返回假,此时v除了IsValidStringKind之外的方法都会导致panic。绝大多数函数和方法都永远不返回Value零值。如果某个函数/方法返回了非法的Value,它的文档必须显式的说明具体情况。

    IsValid()false时,v除了IsValidStringKind之外的方法都会导致panic

  • IsZero()
    func (v Value) IsZero() bool
    reflect.ValueOf传入的值为零值时,返回false
    举个例子:

    package main
    
    import (
      "fmt"
      "reflect"
    )
    
    func main() {
      var s []int
    
      // IsNil
      v1 := reflect.ValueOf(s)
      fmt.Println(v1.IsNil()) // true
      s = []int{1, 2, 3}
      v1 = reflect.ValueOf(s)
      fmt.Println(v1.IsNil())  // false
      fmt.Println(v1.IsZero()) // false
      // IsValid
      var str string
    
      v2 := reflect.ValueOf(str)
      fmt.Println(v2.IsValid()) // true
      fmt.Println(v2.IsZero())  // true
      str = "abc"
      v2 = reflect.ValueOf(str)
      fmt.Println(v2.IsValid()) // true
      fmt.Println(v2.IsZero())  // false
    
    }
    

修改值ValueOf+Elem()

一般类型的修改

函数参数传递的是值拷贝,必须传递变量地址才能修改变量值。而反射中使用专有的Elem()方法来获取指针对应的值。
过程:ValueOf+Elem()+Set...()
常用的设置方法,更多可以查看文档

// 签名
func (v Value) SetBool(x bool)
func (v Value) SetInt(x int64)
func (v Value) SetUint(x uint64)
func (v Value) SetFloat(x float64)
func (v Value) SetComplex(x complex128)
func (v Value) SetBytes(x []byte)
func (v Value) SetString(x string)

例子

package main

import (
  "fmt"
  "reflect"
)

func main()  {
  i := int64(12)  // 注意是int64
  v := reflect.ValueOf(&i)
  switch t :=  v.Elem().Kind(); t {
  case reflect.Int64:
    v.Elem().SetInt(200)
  case reflect.String:
    v.Elem().SetString("abc")
  }
  fmt.Println(i)  // 200
}
结构体类型的修改

相对于一般的类型来说,结构体修改可能有点不一样。
任意值通过reflect.TypeOf()获得反射对象信息后,如果它的类型是结构体,可以通过反射值对象(reflect.Type)的NumField()Field()方法获得结构体成员的详细信息。
注意是reflect.Type,这是一个接口, 不是reflect.ValueOf()返回的reflect.Value结构体
reflect.ValueOf()reflect.Value ==> reflect.Type

package main

import (
  "fmt"
  "reflect"
)

type b struct {
  Name int `json:"name"`
}

func main() {
  x := b{1}
  v := reflect.ValueOf(x) // 参数必须为指针地址
  // tag := v.Field(0).Tag // 这是不行的,报错
  // reflect.Value ==> reflect.Type
  t := v.Type()  // 这是方法,返回的是reflect.Type接口
  tag := t.Field(0).Tag
  fmt.Println(tag) //json:"name"
}

reflect.Type中与获取结构体成员相关的的方法如下表所示。

方法 说明
Field(i int) StructField 根据索引,返回索引对应的结构体字段的信息。
NumField() int 返回结构体成员字段数量。
FieldByName(name string) (StructField, bool) 根据给定字符串返回字符串对应的结构体字段的信息。
FieldByIndex(index []int) StructField 多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的信息。
FieldByNameFunc(match func(string) bool) (StructField,bool) 根据传入的匹配函数匹配需要的字段。
NumMethod() int 返回该类型的方法集中方法的数目
Method(int) Method 返回该类型方法集中的第i个方法
MethodByName(string)(Method, bool) 根据方法名返回该类型方法集中的方法

StructField类型
StructField类型用来描述结构体中的一个字段的信息。

StructField的定义如下:

type StructField struct {
    // Name是字段的名字。PkgPath是非导出字段的包路径,对导出字段该字段为""。
    // 参见http://golang.org/ref/spec#Uniqueness_of_identifiers
    Name    string
    PkgPath string
    Type      Type      // 字段的类型
    Tag       StructTag // 字段的标签
    Offset    uintptr   // 字段在结构体中的字节偏移量
    Index     []int     // 用于Type.FieldByIndex时的索引切片
    Anonymous bool      // 是否匿名字段
}

例子1

package main

import (
  "fmt"
  "reflect"
)

type b struct {
  i int
}
func main()  {
  x := b{1}
  b := reflect.ValueOf(&x.i) // 参数必须为指针地址
  b.Elem().SetInt(3)
  fmt.Println(x)    // {3}
}

例子2

package main

import (
  "fmt"
  "reflect"
)

type people struct {
  Name string    // 字段需要大写
}

func main() {
  p := people{Name: "多人运动"}
  // 传入指针
  v := reflect.ValueOf(&p)
  if v.Elem().Kind() == reflect.Struct {
    n := v.Elem().FieldByName("Name")
    fmt.Println(n) // 多人运动
    // 开始修改
    // 字段需要大写,否则panic
    n.SetString("唱、跳、rap、篮球")
  }
  fmt.Println(p) // {唱、跳、rap、篮球}
}

上面的例子中还可进一步操作结构体的方法等。

异步

go语言对异步编程特别友好、方便易用,而且天生支持高并发,所以首先了解一下什么是并发、并行及其区别:

并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的。
并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。

并行在多处理器系统中存在,而并发可以在单处理器和多处理器系统中都存在,并发能够在单处理器系统中存在是因为并发是并行的假象,并行要求程序能够同时执行多个操作,而并发只是要求程序假装同时执行多个操作(每个小时间片执行一个操作,多个操作快速切换执行)。
并发
当有多个线程在操作时,如果系统只有一个 CPU,则它根本不可能真正同时进行一个以上的线程,它只能把 CPU 运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状态.这种方式我们称之为并发(Concurrent)。
并行
当系统有一个以上 CPU 时,则线程的操作有可能非并发。当一个 CPU 执行一个线程时,另一个 CPU 可以执行另一个线程,两个线程互不抢占 CPU 资源,可以同时进行,这种方式我们称之为并行(Parallel)。

以上解释来自此博客

goroutine

goroutine是go语言中的一种用户态的线程,它与java等语言不太一样,因为并不是操作系统提供的线程,goroutine是由Go的运行时(runtime)调度和管理的。Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU。Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换的机制。
因为操作系统的线程一般都有固定的栈内存(通常为2MB),一个goroutine的栈在其生命周期开始时只有很小的栈(典型情况下2KB),goroutine的栈不是固定的,他可以按需增大和缩小goroutine的栈大小限制可以达到1GB,虽然极少会用到这个大。所以在Go语言中一次创建十万左右的goroutine也是可以的。
goroutine的使用也非常简单,只需go 函数()即可:

package main

import (
  "fmt"
  "time"
)

func hello(i int)  {
  fmt.Println("hello", i)

}
func main()  {
  for i := 0; i < 10 ; i++ {
    go hello(i)      // 使用goroutine
  }
  fmt.Println("main")
  time.Sleep(time.Millisecond)
}

以上例子中:一共有11个线程,1个主线程(main)+ 10个子线程(hello),子线程依赖于主线程,当主线程结束时,其他子线程也会跟着结束,所以这里使用time.Sleep(time.Millisecond)使主线程等一下子线程(下面有更好的解决方案)。执行后会发现:打印的顺序并不是按照for的顺序的,这是因为这是异步的。

goroutine + sync.WaitGroup

我们写程序的时候当然不能使用time.Sleep()的方式让主程序等子程序啦,所以go语言提供了一个包sync用作异步时使用,里面有一个 sync.WaitGroup,可以解决此问题:
使用格式:

package main

import "sync"

var wg sync.WaitGroup

wg的函数:

函数 说明
wg.Done() goroutine结束就登记-1,建议与defer搭配
wg.Add(int) 登记goroutine,传int,即本次登记多少个goroutine
wg.Wait() 等待所有登记的goroutine都结束

例子:

package main

import (
  "fmt"
  "sync"
)

var wg sync.WaitGroup

func hello(i int) {
  defer wg.Done()    //登记-1
  fmt.Println("hello", i)

}

func main() {
  for i := 0; i < 10; i++ {
    wg.Add(1)  // 登记+1
    go hello(i)
  }
  fmt.Println("main")
  wg.Wait()  // 等
}
补充

此部分转载于这里

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

G很好理解,就是个goroutine的,里面除了存放本goroutine信息外 还有与所在P的绑定等信息。
P管理着一组goroutine队列,P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的goroutine队列做一些调度(比如把占用CPU时间较长的goroutine暂停、运行后续的goroutine等等)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里抢任务。
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:n调度的技术(复用/调度m个goroutine到n个OS线程)。 其一大特点是goroutine的调度是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。 另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上, 再加上本身goroutine的超轻量,以上种种保证了go调度方面的性能。

点我了解更多

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

channel

无论是哪一种语言,线程之间的通讯都是一个应该需要关注的问题。Go语言的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。
所以go语言中的通讯使用的就是channel通道,使用:

/*--------------- 定义 --------------------*/
var ch1 chan int  // int是channel要存储的类型。  
ch1 = make(chan, 10)  // 需要分配内存
// 或者
ch2 := make(chan int, 10)
// 或者
var ch3 = make(chan int, 10)

/*--------------- 使用 --------------------*/

ch1 <- 1      // 将数据加入到通道中--发送(send)
x := <- ch1   // 将数据从通道中取出来--接收(receive)
x, ok := <-ch  // 也可以这样接收,x是值,ok表示状态,关闭并取值时为false。可以使用if判断

close(ch1)    // 关闭通道

因为channel是引用数据类型,所以一定要使用make进行初始化

举一个例子:

package main

import (
  "fmt"
  "sync"
)

var ch = make(chan int, 10)
var wg sync.WaitGroup

func hello() {
  defer wg.Done()
  v := <-ch
  fmt.Println("hello", v)
}
func main() {
  for i := 0; i < 10; i++ {
    ch <- i
    wg.Add(1)
    go hello()
  }
  wg.Wait()
}
单向channel

什么是单向channel?顾名思义,即只允许取值或存值的channel
单向channel有什么用?通常作为函数的参数,不让人操作参数,如time.Tick()的签名:func Tick(d Duration) <-chan Time
使用

<- ch    // 只允许取值
ch <-    // 只允许存值

例子:

package main

import (
  "fmt"
  "sync"
)

var (
  vCh = make(chan int, 1)
  rCh = make(chan int, 1)
)
var wg sync.WaitGroup

func squ(valChan <-chan int, resChan chan<- int) {
  defer wg.Done()
  val := <-valChan
  resChan <- val * val
}

func main() {

  wg.Add(1)
  vCh <- 3
  go squ(vCh, rCh)
  fmt.Println(<-rCh) // 9
  wg.Wait()
}
channel使用for循环取值

对于channel来说,不仅可以使用<-来接收值,还可以使用for range来接收值。
使用for接收值时应保证channel最后被关闭(在for之前或在其他goroutine中),否则会死锁!
例子:

package main

import "fmt"

var ch = make(chan int, 10)

func main() {
  for i := 0; i < 10; i++ {
    ch <- i
  }
  close(ch)    // 注意关闭通道,否则会死锁
  for x := range ch{
    fmt.Println(x)
  }

}

使用无限循环的方式
这比for range的复杂一点,利用的是x, ok := <-ch的特性:

package main

import "fmt"

var ch = make(chan int, 10)

func main() {
  for i := 0; i < 10; i++ {
    ch <- i
  }
  close(ch)    // 注意关闭通道,否则会死锁
  for{
    x, ok := <-ch  // ok为false时,表示已经取完值了(已经关闭的通道)
    if !ok{
      break
    }
    fmt.Println(x)
  }
}
channel使用make注意事项

我们在使用makeslicemap开辟内存时知道,make可以指定开辟多大的内存,而为channel开辟内存时,也是这样,开多大,就能就收多少个值。但不同的是,由于channel涉及到线程之间的通讯问题,所以假如在make时出问题的话就有可能死锁,造成程序崩溃。下面记录几种死锁的make指定内存的方式:

  1. make时没有指定内存
    这称为无缓冲的通道又叫阻塞的通道。我们来看一下下面的代码:

    package main
    
    var ch = make(chan int)
    func main()  {
      ch <- 1    // fatal error: all goroutines are asleep - deadlock!
    }
    

    为什么?因为make时没有指定内存的大小,这属于无缓冲通道。当发送一个值到此通道时必须要有其他的goroutine进行接收值。
    改进不报错版,启用一个goroutine接收值:

    
    package main
    
    var ch = make(chan int)
    
    func recv() {
      <-ch
    
    }
    
    func main() {
      go recv()
      ch <- 1
    }
    
  2. make时指定内存
    这种通道被称为有缓冲的通道。在使用make函数初始化通道的时候为其指定通道的容量,只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量。假如channel内的容量满了,继续发送值的话就阻塞了,直至有其他goroutine接收值,这个与上面的情况有点相似,不同的是,这个可以make channel的时候指定合适的容量,避免阻塞。

    
    package main
    
    var ch = make(chan int, 5)
    
    func main() {
      for i := 0; i < 10; i++ {
        ch <- i    // fatal error: all goroutines are asleep - deadlock!
      }
    
    }
    

    解决版本一,扩大容量:

    package main
    
    var ch = make(chan int, 10)    // 扩大容量
    
    func main() {
      for i := 0; i < 10; i++ {
        ch <- i
      }
    }
    

    解决版本二,使用goroutine接收:

    
    package main
    
    import "sync"
    
    var ch = make(chan int, 5) // 扩大容量
    var wg sync.WaitGroup
    
    func recv() {
      wg.Done()
      <-ch
    }
    func main() {
      for i := 0; i < 10; i++ {
        wg.Add(1)
        go recv()
        ch <- i
      }
    }
    
channel关闭后的注意事项

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

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

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

channel状态总结

用一个表格表示:

channel nil 非空 空的 满了 没满
接收 阻塞 接收值 阻塞 接收值 接收值
发送 阻塞 发送值 发送值 阻塞 发送值
关闭 panic 关闭成功,读完数据后返回零值 关闭成功,返回零值 关闭成功,读完数据后返回零值 关闭成功,读完数据后返回零值

select

在使用多个channel接收或发送值时,就有可能遇到阻塞,go语言中用select解决这个问题,select用起来非常像switch

select{
    case <-ch1:    // 接收值
        ...
    case data := <-ch2:    // 接收值,保存到变量中
        ...
    case ch3<-data:      // 发送值
        ...
    default:
        默认操作
}

我们可以看到,select中的case后面可以是发送值、接收值。例子:

package main

import (
  "fmt"
  "math/rand"
  "sync"
  "time"
)

var (
  ch1 = make(chan int, 5)
  ch2 = make(chan int, 5)
)
var wg sync.WaitGroup

func f() {
  defer wg.Done()
  select {
  case x := <- ch1:
    fmt.Println("ch1 x:", x)
  case x := <-ch2:
    fmt.Println("ch2 x:", x)
  default:
    fmt.Println("default...")
  }
}

func main() {
  // 设置随机数种子
  rand.Seed(time.Now().UnixNano())
  for i := 0; i < 5; i++ {
    // 生成随机数,放入channel中
    ch1 <- rand.Intn(10)
    ch2 <- rand.Intn(10)
  }
  wg.Add(1)
  go f()
  wg.Wait()
}

需要注意的是,select并不是根据case的循环执行的,而是随机取case,取到不阻塞时执行当前case

假如我们在goroutine中不使用channel作为goroutine之间的通讯的话,就很有可能造成数据安全问题:


package main

import (
  "fmt"
  "sync"
)

var num int
var wg sync.WaitGroup

func change(a *int) {
  defer wg.Done()
  *a += 1

}
func main() {
  for i := 0; i < 2000; i++ {
    wg.Add(1)
    go change(&num)
  }
  wg.Wait()
  fmt.Println("num:", num)
}

假如执行这一段代码,就会发现:每次运行所得的结果都不一样,这显然不是我们预期的。
其实,goroutine之间是有竞争的,每个goroutine在它取到之后,它本身只能告诉调度器说它已经准备好了,但不能主动执行,它是否轮到它执行都要由底层的一个调度器去调度,也就是说,有可能几个goroutine操作的是同一个值,那么就会造成数据异常。这种现象在其他语言中也有,解决方法都差不多,那就是加一个锁。
加锁有什么用呢?最大的功能就是把并发的某一段代码变成串联的,让它们排着队执行。

互斥锁

使用:

// 声明一个锁
var lock sync.Mutex


lock.Lock()    // 加锁
/* 中间若干代码 */
lock.Unlock()  // 解锁

例子:


package main

import (
  "fmt"
  "sync"
)

var (
  num int
  wg sync.WaitGroup
  lock sync.Mutex

)



func change(a *int) {
  defer wg.Done()
  lock.Lock()
  *a += 1
  lock.Unlock()

}
func main() {
  for i := 0; i < 2000; i++ {
    wg.Add(1)
    go change(&num)
  }
  wg.Wait()
  fmt.Println("num:", num)
}

例子:

读写锁非常适合读多写少的场景,如果读和写的操作差别不大,读写锁的优势就发挥不出来

读写互斥锁

互斥锁是完全互斥的,但是很多场景下是读多写少的,当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,因此就有了读写互斥锁sync.RWMutex
读写互斥锁有读锁和写锁之分:

/* ------------------ 写锁 ------------------ */
func (rw *RWMutex) Lock()
// Lock方法将rw锁定为写入状态,禁止其他线程读取或者写入。

func (rw *RWMutex) Unlock()
// Unlock方法解除rw的写入锁状态,如果m未加写入锁会导致运行时错误。

/* ------------------ 读锁 ------------------ */
func (rw *RWMutex) RLock()
// RLock方法将rw锁定为读取状态,禁止其他线程写入,但不禁止读取。

func (rw *RWMutex) RUnlock()
// Runlock方法解除rw的读取锁状态,如果m未加读取锁会导致运行时错误。

package main

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

var (
  x      int64
  wg     sync.WaitGroup
  rwlock sync.RWMutex
)

func write() {
  rwlock.Lock() // 加写锁
  x = x + 1
  time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒
  rwlock.Unlock()                   // 解写锁
  wg.Done()
}

func read() {
  rwlock.RLock()               // 加读锁
  time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
  rwlock.RUnlock()             // 解读锁
  wg.Done()
}

func main() {
  start := time.Now()
  for i := 0; i < 10; i++ {
    wg.Add(1)
    go write()
  }

  for i := 0; i < 1000; i++ {
    wg.Add(1)
    go read()
  }

  wg.Wait()
  end := time.Now()
  fmt.Println("耗时:", end.Sub(start))
}

其他

sync包atomic包

网络编程

查看此文浅谈OSI七层网络模型和TCP/IP四层模型,了解OSI七层和tcp/udp协议。

socket编程–TCP

TCP服务端

步骤:

  • 绑定ip,监听端口
  • 与客户端建立链接 for循环
  • 处理客户端发来信息go + for循环

实现代码:

package main

import (
  "bufio"
  "fmt"
  "io"
  "net"
  "strings"
)

func process(conn net.Conn) {
  defer conn.Close()
  // 处理客户端发来信息`go + for循环`
  for {
    reader := bufio.NewReader(conn)
    var buf [128]byte
    n, err := reader.Read(buf[:]) // 读取数据
    if err == io.EOF {
      // 客户端主动关闭
      return
    }
    if err != nil {
      fmt.Println("read err:", err)
      break
    }
    recvStr := string(buf[:n])
    fmt.Println("收到client端发来的数据:", recvStr)
    conn.Write([]byte(strings.ToUpper(recvStr))) // 发送数据
  }

}
func main() {

  // 绑定ip,监听端口
  listen, errL := net.Listen("tcp", "127.0.0.1:8888")
  if errL != nil {
    fmt.Println("listen error:", errL)
    return
  }
  fmt.Println("server端启动成功")
  // 与客户端建立链接 `for循环`
  for {
    conn, errA := listen.Accept()
    if errA != nil {
      fmt.Println("accept error:", errA)
      return
    }
    go process(conn)
  }

}
TCP客户端

步骤:

  • 建立与服务端的链接
  • 进行数据收发 for循环
  • 关闭链接
package main

import (
  "bufio"
  "fmt"
  "net"
  "os"
  "strings"
)

// 客户端
func main() {
  // 建立与服务端的链接
  conn, err := net.Dial("tcp", "127.0.0.1:8888")
  if err != nil {
    fmt.Println("err :", err)
    return
  }
  defer conn.Close() // 关闭连接
  fmt.Println("连接sever端成功")
  inputReader := bufio.NewReader(os.Stdin)
  // 进行数据收发
  for {
    fmt.Print("请输入>>>(q退出)")
    input, _ := inputReader.ReadString('\n') // 读取用户输入
    inputInfo := strings.Trim(input, "\r\n")
    if strings.ToUpper(inputInfo) == "Q" { // 如果输入q就退出
      return
    }
    if inputInfo == ""{
      // 为空时跳过
      continue
    }
    _, err = conn.Write([]byte(inputInfo)) // 发送数据
    if err != nil {
      return
    }
    buf := [512]byte{}
    n, err := conn.Read(buf[:])
    if err != nil {
      fmt.Println("recv error", err)
      return
    }
    fmt.Println("收到server端发来的数据:",string(buf[:n]))
  }
}
注意事项

例子中使用的是bufio,实际上可以直接使用conn的方法:

// 接收
// Read从连接中读取数据
// Read方法可能会在超过某个固定时间限制后超时返回错误,该错误的Timeout()方法返回真
Read(b []byte) (n int, err error)

// 发送
// Write从连接中写入数据
// Write方法可能会在超过某个固定时间限制后超时返回错误,该错误的Timeout()方法返回真
Write(b []byte) (n int, err error)
解决tcp粘包现象
  • TODO

socket编程–UDP

UDP服务端
  • TODO
UDP客户端
  • TODO

http编程(net/http)

http服务端

使用的是http.HandleFunc("/", fooHandler), 其中:第一个参数为urlfooHandler是一个(w http.ResponseWriter, r *http.Request)函数。
假如要修改响应体,只需要修改http.ResponseWriter即可,
http.ResponseWriterhttp.Request的定义:

type ResponseWriter interface {
    // Header返回一个Header类型值,该值会被WriteHeader方法发送。
    // 在调用WriteHeader或Write方法后再改变该对象是没有意义的。
    Header() Header
    // WriteHeader该方法发送HTTP回复的头域和状态码。
    // 如果没有被显式调用,第一次调用Write时会触发隐式调用WriteHeader(http.StatusOK)
    // WriterHeader的显式调用主要用于发送错误码。
    WriteHeader(int)
    // Write向连接中写入作为HTTP的一部分回复的数据。
    // 如果被调用时还未调用WriteHeader,本方法会先调用WriteHeader(http.StatusOK)
    // 如果Header中没有"Content-Type"键,
    // 本方法会使用包函数DetectContentType检查数据的前512字节,将返回值作为该键的值。
    Write([]byte) (int, error)
}

type Request struct {
    // Method指定HTTP方法(GET、POST、PUT等)。对客户端,""代表GET。
    Method string
    // URL在服务端表示被请求的URI,在客户端表示要访问的URL。
    //
    // 在服务端,URL字段是解析请求行的URI(保存在RequestURI字段)得到的,
    // 对大多数请求来说,除了Path和RawQuery之外的字段都是空字符串。
    // (参见RFC 2616, Section 5.1.2)
    //
    // 在客户端,URL的Host字段指定了要连接的服务器,
    // 而Request的Host字段(可选地)指定要发送的HTTP请求的Host头的值。
    URL *url.URL
    // 接收到的请求的协议版本。本包生产的Request总是使用HTTP/1.1
    Proto      string // "HTTP/1.0"
    ProtoMajor int    // 1
    ProtoMinor int    // 0
    // Header字段用来表示HTTP请求的头域。如果头域(多行键值对格式)为:
    //  accept-encoding: gzip, deflate
    //  Accept-Language: en-us
    //  Connection: keep-alive
    // 则:
    //  Header = map[string][]string{
    //    "Accept-Encoding": {"gzip, deflate"},
    //    "Accept-Language": {"en-us"},
    //    "Connection": {"keep-alive"},
    //  }
    // HTTP规定头域的键名(头名)是大小写敏感的,请求的解析器通过规范化头域的键名来实现这点。
    // 在客户端的请求,可能会被自动添加或重写Header中的特定的头,参见Request.Write方法。
    Header Header
    // Body是请求的主体。
    //
    // 在客户端,如果Body是nil表示该请求没有主体买入GET请求。
    // Client的Transport字段会负责调用Body的Close方法。
    //
    // 在服务端,Body字段总是非nil的;但在没有主体时,读取Body会立刻返回EOF。
    // Server会关闭请求的主体,ServeHTTP处理器不需要关闭Body字段。
    Body io.ReadCloser
    // ContentLength记录相关内容的长度。
    // 如果为-1,表示长度未知,如果>=0,表示可以从Body字段读取ContentLength字节数据。
    // 在客户端,如果Body非nil而该字段为0,表示不知道Body的长度。
    ContentLength int64
    // TransferEncoding按从最外到最里的顺序列出传输编码,空切片表示"identity"编码。
    // 本字段一般会被忽略。当发送或接受请求时,会自动添加或移除"chunked"传输编码。
    TransferEncoding []string
    // Close在服务端指定是否在回复请求后关闭连接,在客户端指定是否在发送请求后关闭连接。
    Close bool
    // 在服务端,Host指定URL会在其上寻找资源的主机。
    // 根据RFC 2616,该值可以是Host头的值,或者URL自身提供的主机名。
    // Host的格式可以是"host:port"。
    //
    // 在客户端,请求的Host字段(可选地)用来重写请求的Host头。
    // 如过该字段为"",Request.Write方法会使用URL字段的Host。
    Host string
    // Form是解析好的表单数据,包括URL字段的query参数和POST或PUT的表单数据。
    // 本字段只有在调用ParseForm后才有效。在客户端,会忽略请求中的本字段而使用Body替代。
    Form url.Values
    // PostForm是解析好的POST或PUT的表单数据。
    // 本字段只有在调用ParseForm后才有效。在客户端,会忽略请求中的本字段而使用Body替代。
    PostForm url.Values
    // MultipartForm是解析好的多部件表单,包括上传的文件。
    // 本字段只有在调用ParseMultipartForm后才有效。
    // 在客户端,会忽略请求中的本字段而使用Body替代。
    MultipartForm *multipart.Form
    // Trailer指定了会在请求主体之后发送的额外的头域。
    //
    // 在服务端,Trailer字段必须初始化为只有trailer键,所有键都对应nil值。
    // (客户端会声明哪些trailer会发送)
    // 在处理器从Body读取时,不能使用本字段。
    // 在从Body的读取返回EOF后,Trailer字段会被更新完毕并包含非nil的值。
    // (如果客户端发送了这些键值对),此时才可以访问本字段。
    //
    // 在客户端,Trail必须初始化为一个包含将要发送的键值对的映射。(值可以是nil或其终值)
    // ContentLength字段必须是0或-1,以启用"chunked"传输编码发送请求。
    // 在开始发送请求后,Trailer可以在读取请求主体期间被修改,
    // 一旦请求主体返回EOF,调用者就不可再修改Trailer。
    //
    // 很少有HTTP客户端、服务端或代理支持HTTP trailer。
    Trailer Header
    // RemoteAddr允许HTTP服务器和其他软件记录该请求的来源地址,一般用于日志。
    // 本字段不是ReadRequest函数填写的,也没有定义格式。
    // 本包的HTTP服务器会在调用处理器之前设置RemoteAddr为"IP:port"格式的地址。
    // 客户端会忽略请求中的RemoteAddr字段。
    RemoteAddr string
    // RequestURI是被客户端发送到服务端的请求的请求行中未修改的请求URI
    // (参见RFC 2616, Section 5.1)
    // 一般应使用URI字段,在客户端设置请求的本字段会导致错误。
    RequestURI string
    // TLS字段允许HTTP服务器和其他软件记录接收到该请求的TLS连接的信息
    // 本字段不是ReadRequest函数填写的。
    // 对启用了TLS的连接,本包的HTTP服务器会在调用处理器之前设置TLS字段,否则将设TLS为nil。
    // 客户端会忽略请求中的TLS字段。
    TLS *tls.ConnectionState
}

例子:

package main

import (
  "fmt"
  "log"
  "net/http"
)
func fooHandler(w http.ResponseWriter, r *http.Request) {
  _, err :=fmt.Fprintln(w, "<h1>Index</h1>")

  if err != nil{
    return
  }
}

func main()  {
  http.HandleFunc("/", fooHandler)
  log.Fatal(http.ListenAndServe("127.0.0.1:8899", nil))
}
一些函数

来自GO语言中文网

func HandleFunc

func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
// HandleFunc注册一个处理器函数handler和对应的模式pattern(注册到DefaultServeMux)。ServeMux的文档解释了模式的匹配机制。

func ListenAndServe

func ListenAndServe(addr string, handler Handler) error

ListenAndServe监听TCP地址addr,并且会使用handler参数调用Serve函数处理接收到的连接。handler参数一般会设为nil,此时会使用DefaultServeMux

一个简单的服务端例子:

package main
import (
  "io"
  "net/http"
  "log"
)
// hello world, the web server
func HelloServer(w http.ResponseWriter, req *http.Request) {
  io.WriteString(w, "hello, world!\n")
}
func main() {
  http.HandleFunc("/hello", HelloServer)
  err := http.ListenAndServe(":12345", nil)
  if err != nil {
    log.Fatal("ListenAndServe: ", err)
  }
}

func ListenAndServeTLS

func ListenAndServeTLS(addr string, certFile string, keyFile string, handler Handler) error

ListenAndServeTLS函数和ListenAndServe函数的行为基本一致,除了它期望HTTPS连接之外。此外,必须提供证书文件和对应的私钥文件。如果证书是由权威机构签发的,certFile参数必须是顺序串联的服务端证书和CA证书。如果srv.Addr为空字符串,会使用”:https”。

一个简单的服务端例子:

import (
  "log"
  "net/http"
)
func handler(w http.ResponseWriter, req *http.Request) {
  w.Header().Set("Content-Type", "text/plain")
  w.Write([]byte("This is an example server.\n"))
}
func main() {
  http.HandleFunc("/", handler)
  log.Printf("About to listen on 10443. Go to https://127.0.0.1:10443/")
  err := http.ListenAndServeTLS(":10443", "cert.pem", "key.pem", nil)
  if err != nil {
    log.Fatal(err)
  }
}
// 程序员可以使用crypto/tls包的generate_cert.go文件来生成cert.pem和key.pem两个文件。
增加请求头
  • TODO
获得请求体
  • TODO
增加cookie
  • TODO
客户端
  • TODO

测试工具

  • TODO

数据库操作

以mysql为例

链接数据库

想要链接mysql需要两个包:

  • 内置database/sql:提供了保证SQL或类SQL数据库的泛用接口
  • 第三个mysql驱动
下载第三方库

使用命令go get -u github.com/go-sql-driver/mysql下载

链接

步骤:

  • import
  • 配置数据源,格式:”用户名:密码@tcp(ip:port)/库名
  • 使用sql.Open()方法
  • 使用db.Ping()方法检验是否链接成功,注:db是Open返回的*sql.DB

sql.Open()的签名:

func Open(driverName, dataSourceName string) (*DB, error)

例子:

package main

import (
  "database/sql"
  "fmt"
  _ "github.com/go-sql-driver/mysql"
  "log"
)

func main() {
  dsn := "root:123456@tcp(127.0.0.1:3306)/gotest"
  db, err := sql.Open("mysql", dsn)
  if err != nil{
    log.Fatalln(err)
  }
  err = db.Ping()
  if err != nil {
    log.Fatalln(err)
  }
  fmt.Println("connection success!")

}

一般来说,可以把链接数据库的代码放入init函数中,提前定义一个全局的db,则可以在这个package中的任意函数中使用。

操作数据

增、删、改数据

增、删、改数据用的都是sql命令,然后使用bd.Exec(), ?是mysql的占位符,可以作为bd.Exec()的参数

注意:不要试图自己拼接sql命令,防止sql注入

package main

import (
  "database/sql"
  "fmt"
  _ "github.com/go-sql-driver/mysql"
  "log"
)

var db *sql.DB
var err error

// 链接数据库
func init() {
  dsn := "root:123456@tcp(127.0.0.1:3306)/gotest"
  db, err = sql.Open("mysql", dsn)
  if err != nil {
    log.Fatalln(err)
  }
  err = db.Ping()
  if err != nil {
    log.Fatalln(err)
  }
}

// 增加
func addUser(name string, age int) {
  addCmd := "insert into user(name, age) values(?, ?)"
  ret, err := db.Exec(addCmd, name, age)
  if err != nil {
    log.Println("add error:", err)
  }
  theID, _ := ret.LastInsertId() // 新插入数据的id
  fmt.Printf("增加的数据id为:%d\n", theID)
}

// 删除
func deleteUser(nid int) {
  delCmd := "delete from user where id=?"
  ret, err := db.Exec(delCmd, nid)
  if err != nil {
    log.Println("add error:", err)
  }
  n, _ := ret.RowsAffected() // 操作影响的行数
  fmt.Printf("delete success, affected rows:%d\n", n)
}

// 修改
func updateUser(nid int, name string, age int) {
  updateCmd := "update user set name=?, age=? where id=?"
  ret, err := db.Exec(updateCmd, name, age, nid)
  if err != nil {
    log.Println("add error:", err)
  }
  n, _ := ret.RowsAffected() // 操作影响的行数
  fmt.Printf("update success, affected rows:%d\n", n)

}

func main() {
  // 在这里调用函数
  
  //addUser("李逵", 36)
  //deleteUser(6)
  //updateUser(1, "kk", 23)
}
查询数据
查询一条数据

查询一条数据是调用的是db的QueryRow()方法,该方法的签名:

func (db *DB) Query(query string, args ...interface{}) (*Rows, error)

其中,*Rows是一个结构体,有一个Scan方法。
Scan方法的签名:

func (r *Row) Scan(dest ...interface{}) error

dest是接收数据的变量,以根据传值的顺序接收

注意:一定要调用Scan方法取值,否则当前的链接就不能回到数据库连接池中,而当数据库连接池没有空闲的链接时程序会阻塞`

package main

import (
  "database/sql"
  "fmt"
  _ "github.com/go-sql-driver/mysql"
  "log"
)

var db *sql.DB

type user struct {
  name string
  age  int
}

func init() {
  var err error
  dsn := "root:123456@tcp(127.0.0.1:3306)/gotest"
  db, err = sql.Open("mysql", dsn)
  if err != nil {
    log.Fatalln(err)
  }
  err = db.Ping()
  if err != nil {
    log.Fatalln(err)
  }
}

func query(n int) {
  u := user{}
  selStr := `select name, age from user where id=?`
  // 注意要scan
  // 传的是指针
  _ = db.QueryRow(selStr, n).Scan(&u.name, &u.age)
  fmt.Println(u)

}
func main() {
  query(1)
}
查询多条数据

上面查询一条数据的方法,有时候并不适合查询多条数据,因此*sql.DB还有另外一个方法db.Query(),其签名:

func (db *DB) Query(query string, args ...interface{}) (*Rows, error)

返回值Rows有几个方法,我们需要用到:

  • Close:关闭链接,注意:不是Scan了
  • Next:rows中还有没有值,for循环取值时用到
  • Scan:把变量传进去,取值
package main

import (
  "database/sql"
  "fmt"
  _ "github.com/go-sql-driver/mysql"
  "log"
)

var db *sql.DB
var err error

type user struct {
  name string
  age  int
}

func init() {
  dsn := "root:123456@tcp(127.0.0.1:3306)/gotest"
  db, err = sql.Open("mysql", dsn)
  if err != nil {
    log.Fatalln(err)
  }
  err = db.Ping()
  if err != nil {
    log.Fatalln(err)
  }
}

func query(id int) {
  var u user
  queStr := "select name, age from user where id > ?"
  rows, err := db.Query(queStr, id)
  if err != nil {
    log.Println(err)
  }
  // 延迟调用,关闭
  defer rows.Close()
  // Next方法,没有值时返回false
  for rows.Next() {
    // Scan取值
    err = rows.Scan(&u.name, &u.age)
    if err != nil {
      log.Println(err)
      return
    }
    fmt.Printf("name: %s, age: %d\n", u.name, u.age)

  }

}

func main() {
  query(2)
}

mysql预处理

首先看看普通SQL语句执行过程:

  1. 客户端对SQL语句进行占位符替换得到完整的SQL语句。
  2. 客户端发送完整SQL语句到MySQL服务端
  3. MySQL服务端执行完整的SQL语句并将结果返回给客户端。

而预处理语句用于执行多个相同的 SQL 语句,并且执行效率更高。
预处理语句的工作原理如下:

  1. 预处理:创建 SQL 语句模板并发送到数据库。预留的值使用参数 “?” (mysql的占位符)标记 。例如:INSERT INTO user (name, age) VALUES(?, ?),&nbsp;&nbsp;然后数据库解析,编译,对SQL语句模板执行查询优化,并存储结果不输出。
  2. 执行:最后,将应用绑定的值传递给参数(”?” 标记),数据库执行语句。应用可以多次执行语句,如果参数的值不一样。

相比于直接执行SQL语句,预处理语句几个优点:

  1. 预处理语句大大减少了分析时间,只做了一次查询(虽然语句多次执行)。
  2. 绑定参数减少了服务器带宽,你只需要发送查询的参数,而不是整个语句。
  3. 预处理语句针对SQL注入是非常有用的,因为参数值发送后使用不同的协议,保证了数据的合法性。

那看看在go语言中是怎么实现的?
这里要用到的是,*sql.DBPrepare方法,以下为它的签名:

func (db *DB) Prepare(query string) (*Stmt, error)

query是包含占位符的语句
*Stmt可以使用Exec方法和QueryRow方法和Query方法进行增删改查,不过只需要传参数即可。

package main

import (
  "database/sql"
  "fmt"
  _ "github.com/go-sql-driver/mysql"
  "log"
)
var db *sql.DB
var err error

func init()  {
  dsn := "root:123456@tcp(127.0.0.1:3306)/gotest"
  db, err = sql.Open("mysql", dsn)
  if err != nil {
    log.Fatalln(err)
  }
  err = db.Ping()
  if err != nil {
    log.Fatalln(err)
  }
}
func prepareQuery()  {
  queStr := "insert into user (name, age) values(?, ?)"
  stmt, err := db.Prepare(queStr)
  if err != nil {
    log.Fatalln(err)
  }
  // close掉
  defer stmt.Close()
  ret, _ := stmt.Exec("张三", 22)
  aff, _ := ret.RowsAffected()
  fmt.Printf("影响了%d行\n", aff)
}

func main() {
  prepareQuery()
}

注意:这里演示的插入一条数据,假如多条数据查询的话有两次defer(stmt和rows)

进阶版本

TODO: 厉害的时候补充

内存对齐

详见此文:在 Go 中恰到好处的内存对齐

go标准库

只记录用到过的,因此不完整,可以点这里查看更多

fmt

fmt.Print

打印:结尾不带换行符,没有占位符

package main

import "fmt"

func main()  {
  fmt.Print(1234)
  fmt.Print(5678)
  // 输出   12345678

}

注意: 结果是没有换行符的,所以会连在一起显示

fmt.Println

打印:带换行符,没有占位符

package main

import "fmt"

func main()  {
  fmt.Print(1234)
  fmt.Print(5678)
  // 输出 :
  //    1234
  //    5678

}

fmt.Printf

打印:不带换行符,有占位符
下面先陈列一下占位符,及其说明。再演示一下他们的效果

占位符 说明
%v 以默认格式展示,万能的表示方法
%+v 类似%v,但在输出结构体时会加上字段名
%#v go语法表示的值(待会看例子,一下就懂)
%T 值的类型
%% 百分号
%t 布尔值
%b 二进制
%o 八进制
%d 十进制
%x 十六进制(a-f)
%X 十六进制(A-F)
%f / %F 浮点数
%s 字符串或byte
%q 安全字符串,即没有转义,原封不动输出
%p 指针(十六进制)
%c 字符,注意byte是uint8,所以需要用%c转换
%U 表示为Unicode格式
%e / %E 科学计数法,以e / E 为底

例子:

// pass

strings

TODO: 完善例子说明

开头与结尾

strings.HasPrefix(s1, prefix) bools1是否以prefix开头
strings.HasSuffix(s1, suffix) bools1是否以suffix结尾

package main

import (
  "fmt"
  "strings"
)

func main()  {
  s1 := "hello world!"
  fmt.Println(strings.HasPrefix(s1, "h"))  // ture
  fmt.Println(strings.HasSuffix(s1, "!"))  // ture

}

位置

strings.Index
strings.LastIndex

替换

strings.Replace

计数

strings.Count

重复

strings.Repeat

大小写

strings.ToLower
strings.ToUpper

去除

strings.TrimSpace
strings.Trim
strings.TrimLeft
strings.TrimRight

分割

strings.Field
strings.Split

拼接

strings.Join

bufio包

  • TODO

time

这个包的内容转载于此文

  1. 当前时间

    func timeDemo() {
      now := time.Now() //获取当前时间
      fmt.Printf("current time:%v\n", now)
    
      year := now.Year()     //年
      month := now.Month()   //月
      day := now.Day()       //日
      hour := now.Hour()     //小时
      minute := now.Minute() //分钟
      second := now.Second() //秒
      fmt.Printf("%d-%02d-%02d %02d:%02d:%02d\n", year, month, day, hour, minute, second)
    }
    
  2. 时间戳
    时间戳是自1970年1月1日(08:00:00GMT)至当前时间的总毫秒数。它也被称为Unix时间戳(UnixTimestamp)。
    基于时间对象获取时间戳的示例代码如下:

    func timestampDemo() {
      now := time.Now()            //获取当前时间
      timestamp1 := now.Unix()     //时间戳
      timestamp2 := now.UnixNano() //纳秒时间戳
      fmt.Printf("current timestamp1:%v\n", timestamp1)
      fmt.Printf("current timestamp2:%v\n", timestamp2)
    }
    

    使用time.Unix()函数可以将时间戳转为时间格式。

    func timestampDemo2(timestamp int64) {
      timeObj := time.Unix(timestamp, 0) //将时间戳转为时间格式
      fmt.Println(timeObj)
      year := timeObj.Year()     //年
      month := timeObj.Month()   //月
      day := timeObj.Day()       //日
      hour := timeObj.Hour()     //小时
      minute := timeObj.Minute() //分钟
      second := timeObj.Second() //秒
      fmt.Printf("%d-%02d-%02d %02d:%02d:%02d\n", year, month, day, hour, minute, second)
    }
    
  3. 时间单位

    // time包
    const (
        Nanosecond  Duration = 1            // 纳秒
        Microsecond          = 1000 * Nanosecond    // 微秒
        Millisecond          = 1000 * Microsecond    // 毫秒
        Second               = 1000 * Millisecond    // 秒
        Minute               = 60 * Second        // 分
        Hour                 = 60 * Minute        // 时
    )
    
  4. 时间操作
    Add:
    我们在日常的编码过程中可能会遇到要求时间+时间间隔的需求,Go语言的时间对象有提供Add方法如下:
    func (t Time) Add(d Duration) Time
    举个例子,求一个小时之后的时间:

    func main() {
      now := time.Now()
      later := now.Add(time.Hour) // 当前时间加1小时后的时间
      fmt.Println(later)
    }
    

    Sub:
    求两个时间之间的差值:
    func (t Time) Sub(u Time) Duration
    返回一个时间段t-u。如果结果超出了Duration可以表示的最大值/最小值,将返回最大值/最小值。要获取时间点t-d(d为Duration),可以使用t.Add(-d)。

    Equal:
    func (t Time) Equal(u Time) bool
    判断两个时间是否相同,会考虑时区的影响,因此不同时区标准的时间也可以正确比较。本方法和用t==u不同,这种方法还会比较地点和时区信息。

    Before:
    func (t Time) Before(u Time) bool
    如果t代表的时间点在u之前,返回真;否则返回假。

    After:
    func (t Time) After(u Time) bool
    如果t代表的时间点在u之后,返回真;否则返回假。

  5. 定时器
    使用time.Tick(时间间隔)来设置定时器,定时器的本质上是一个通道(channel)。

    func tickDemo() {
      ticker := time.Tick(time.Second) //定义一个1秒间隔的定时器
      for i := range ticker {
        fmt.Println(i)//每秒都会执行的任务
      }
    }
    
  6. 时间格式化
    时间类型有一个自带的方法Format进行格式化,需要注意的是Go语言中格式化时间模板不是常见的Y-m-d H:M:S而是使用Go的诞生时间2006年1月2号15点04分(记忆口诀为2006 1 2 3 4)。也许这就是技术人员的浪漫吧。

    补充:如果想格式化为12小时方式,需指定PM。

    func formatDemo() {
      now := time.Now()
      // 格式化的模板为Go的出生时间2006年1月2号15点04分 Mon Jan
      // 24小时制
      fmt.Println(now.Format("2006-01-02 15:04:05.000 Mon Jan"))
      // 12小时制
      fmt.Println(now.Format("2006-01-02 03:04:05.000 PM Mon Jan"))
      fmt.Println(now.Format("2006/01/02 15:04"))
      fmt.Println(now.Format("15:04 2006/01/02"))
      fmt.Println(now.Format("2006/01/02"))
    }
    
  7. 解析字符串格式的时间

    now := time.Now()
    fmt.Println(now)
    // 加载时区
    loc, err := time.LoadLocation("Asia/Shanghai")
    if err != nil {
      fmt.Println(err)
      return
    }
    // 按照指定时区和指定格式解析字符串时间:  ParseInLocation(时间格式, 要解析的时间, 时区)
    timeObj, err := time.ParseInLocation("2006/01/02 15:04:05", "2019/08/04 14:15:20", loc)
    if err != nil {
      fmt.Println(err)
      return
    }
    fmt.Println(timeObj)
    fmt.Println(timeObj.Sub(now))
    

sync

  • TODO

atomic

  • TODO

参考资料:

李文周的博客

作者: 忞翛

出处: https://www.lczmx.top/Golang/969ea9a10eee/

版权: 本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。

在线工具