《Go语言圣经》笔记

这篇博客详细介绍了Go语言的基础语法,包括命名规则、变量声明与赋值、类型系统、包管理以及接口和并发特性。重点讲解了变量的声明、指针、切片、映射和通道的概念及使用,还涵盖了Goroutines、通道同步和锁的使用。此外,还探讨了Go语言的测试框架和标准库的学习,如测试覆盖率和测试函数的编写。
摘要由CSDN通过智能技术生成

文件组成

特点:

  • Go是一门编译型语言,Go语言的工具链将源代码及其依赖转换成计算机的机器指令(译注:静态编译)
  • Go语言原生支持Unicode,它可以处理全世界任何语言的文本

组成:

  • 通过包(package)的方式组织代码import
  • 组成程序的声明种类
    • 常量 - const
    • 变量 - var
    • 函数 - func
    • 类型 - type
  • 初始化函数init
  • 可执行程序入口函数main

例子:

// 声明一个可执行程序(main 比较特殊)
package main

// 通过包组织代码运行和调用
import "fmt"

// 常量 & 变量
const LEVEL = "V5"

var name string

// 初始化
func init() {
	name = "中国"
}

// 程序执行入口
func main() {
	fmt.Println(name, LEVEL, ":Hello word!")
}

基础语法

命名

一个名字必须以一个字母(Unicode字母)或下划线开头,后面可以跟任意数量的字母、数字或下划线

区分大小写

开头字母的大小写决定了名字在包外的可见性。大写表示外部可见( 中文汉字现在默认为小写,未来不一定),否则为包内可见

尽量使用驼峰式命名方式,避免缩写单词大小写混用(setByCas => setByCAS

保留关键字如下,保留关键字不允许使用到命名中:

break      default       func     interface   select
case       defer         go       map         struct
chan       else          goto     package     switch
const      fallthrough   if       range       type
continue   for           import   return      var

预定义名称(可以使用作为命名):

内建常量: true false iota nil

内建类型: int int8 int16 int32 int64
         uint uint8 uint16 uint32 uint64 uintptr
         float32 float64 complex128 complex64
         bool string 
         byte  // 自动类型转换, 默认:int8 中文:int32,可用ASCII码 10进制、16进制、单引号 表示
		 rune  // int32 的别名,代表一个Unicode码
		 error

内建函数: make len cap new append copy close delete
         complex real imag
         panic recover

变量

语法
var 变量名[ [*]类型] =

值和类型可以省略其中一个:

  • 省略类型: 根据初始化表达式来推导变量的类型信息。
  • 省略值 : 用零值初始化该变量。
短变量声明
  • 声明和初始化局部变量
    • 允许部分变量已经声明;执行部分初始化,部分声明+初始化
    • 不允许全部变量都声明;全部声明的变量不应该用短变量声明;应该用元组赋值-多重赋值
  • 类型根据表达式来自动推导
零值
数值类型   `0`
布尔类型   `false`
字符串     `""`(空字符串)

接口或引用类型(包括slice、指针、mapchan和函数)  nil

数组或结构体等聚合类型  每个元素或字段都是对应该类型的零值
指针

一个指针的值是另一个变量的地址。一个指针对应变量在内存中的存储位置。

p := &s
fmt.Printf("类型:%T \n", p)
fmt.Printf("指针变量地址:%p 指针变量内容:%v 调用时的地址:%p \n", &p, p, &*p)
fmt.Printf("底层数据地址:%p \n", &s)

// 类型:*string 
// 指针地址:0xc000006030 指针内容:0xc00003a1f0 调用时的地址:0xc00003a1f0 
// 底层数据地址:0xc00003a1f0 
生命周期
  • 全局变量:位于包一级的变量。它们的生命周期和整个程序的运行周期是一致的。
  • 局部变量:位于函数内的变量。每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收

变量逃逸:

针对局部变量的回收特性, 如果将指向短生命周期对象的指针保存到具有长生命周期的对象中,特别是保存到全局变量时,会阻止对短生命周期对象的垃圾回收(从而可能影响程序的性能) 。

说人话:当全局变量指针指向局部变量的内存地址(global = &part),就会导致内存不回收。

赋值

  • 类型必须完全匹配,在赋值语句左边的变量和右边最终的求到的值必须有相同的数据类型
  • nil可以赋值给任何指针或引用类型的变量
  • 元组赋值允许同时更新多个变量的值( 多重赋值 可以用于交换两个变量)
    • 先右后左
    • 将右边各个表达式的值赋值给左边对应位置的各个变量

类型

type 类型名字 底层类型

包的惯例
  1. 必须在头部声明自己的包名
  2. 每个包全部都在一个单独的目录里。一个目录不存在多个包、一个包不会存在多个目录。
  3. 包名应保持小写、简洁
main包
  1. 编译程序会将这种名字的包编译为二进制可执行文件。 可执行程序必须有一个main的包
  2. 必须存在一个main()函数
导入

导入使用关键字 import;导入的方式分为以下三种:

  • 远程导入import "github.com/spf13/viper"
  • 命名导入import myviper "mylib/viper"。一般用于解决冲突或者别名
  • 空白标识符导入import _ "db/driver/mysql"。一般用于初始化代码才会如此
init函数
  1. 每个包可以包含多个init()函数。
    1. 支持单个文件多个
    2. 支持多个文件多个
  2. main()之前运行

PS - 部分场景:在 init()中初始化向某个插件注册功能。可执行程序处引用包含init()的文件、包

执行顺序
  1. import subPkg. 引入包
  2. const. 常量定义
  3. var. 变量定义
  4. subPkg.init(). 引入包执行 init
    1. 以导入声明的顺序初始化
  5. init(). 优先包含 main函数的 init(), 然后其他 init()
  6. main(). 当前main
常见工具
  • go env.查看环境变量
  • go list.查看依赖包
  • go build.编译包和依赖项
    • go build -race竞争检测器来编译并执行
  • go run.编译并运行Go程序
  • go vet.代码错误检查
  • go fmt.代码格式化
  • go get.下载 && 安装软件包和依赖(远程)
  • go install.编译 && 安装软件包和依赖项(本地)

数据类型

Go语言将数据类型分为四类:基础类型复合类型引用类型接口类型

  • 基础类型:数字、字符串和布尔型。
  • 复合类型:数组、结构体 是通过组合简单类型,来表达更加复杂的数据结构。
  • 引用类型:指针、切片、映射、函数、通道
  • 接口类型:接口

基础类型

运算符

优先级排序:

*      /      %      <<       >>     &       &^
+      -      |      ^
==     !=     <      <=       >      >=
&&
||

%(取模):仅用于整数间的运算。取模结果符号与被取模数值一致

-5%3	// -2
-5%-3	// -2

/(除法):用于整数时会采用去尾法

5/2		// 2
5.0/2.0	// 2.5

算数运算时,高位溢出直接丢弃无关符号

var u uint8 = 255
fmt.Println(u, u+1, u*u) // "255 0 1"

类型转换时:

  • 大尺寸的整数类型转为一个小尺寸的整数类型 (int32 -> int16)
  • 将一个浮点数转为整数,可能会改变数值或丢失精度 (float64 -> int32)
整型

带符号和无符号的不同位整型:int8、int16、int32、int64、uint8、uint16、uint32、uint64

int、uint

rune 通常用于表示一个Unicode码点(等价于 int32

byte 字节内容(**等价于 uint8 **)

*uintptr 一般是写底层用的

浮点型

float32、float64

复数

complex64、complex128

布尔值

true、false

字符串

8位字节序列构成的字符串,约定但不必须是utf-8编码的文本。 字符串可以为空但不能是nil,其值不可变

字符串可以用==和<进行比较;比较通过逐个字节比较完成的,因此比较的结果是字符串自然编码的顺序。

文本字符串通常被解释为采用UTF8编码的Unicode码点(rune)序列

len函数 可以返回一个字符串中的字节数目(不是rune字符数目)

len("汉")  // 3. 中文 utf8 下为三个字节

连接通过 "+"号:

fmt.Println("你" + "好")

Unicode 符号和二进制编码的对应关系;

UTF-8 针对Unicode的可变长度字符编码方式。最长的符号达到6个字节了(汉字一般是3个,还有其他的)

  • 目前最长的符号达到6个字节了
  • 汉字一般3字节,但还有其他的4字节;详见unicode码表
  • mysql中常见的 utf8mb4 是为了兼容宽字节(也就是4个字节)的情况
tempStr := "汉1"
fmt.Printf("rune(char):%q\n", []rune(tempStr))	// rune(char):['汉' '1']
fmt.Printf("rune(hex):%x\n", []rune(tempStr))	// rune(hex):[6c49 31]		-- Unicode 码值
fmt.Printf("bytes(hex):% x\n", []byte(tempStr))	// bytes(hex):e6 b1 89 31	-- UTF-8 编码后

// len 访问时
fmt.Println(len(tempStr)) // 4 => 字节数

// 访问指定偏移量时
fmt.Println(tempStr[2]) // 137 => 字节序访问

// 直接遍历字符串的时候
for _, val := range tempStr {
    fmt.Println(val)	// 27721 49 => unicode码值
}

// 上面的三个步骤说明如下:
// 1. 按照一个个字符拆分
// 2. 字符转为 unicode码表
// 3. 使用UTF-8转换为字节序

fmt.Println([]rune(tempStr)) 				// [27721 49] 十进制的
fmt.Println(string(27721), string(0x6c49))	// 汉 汉

// 递归字节切片
for _, val := range []byte(tempStr) {
    fmt.Println(val)
}
// 230 177 137 49
常量

常量的数值来源为基础类型。表达式在编译期间计算,而不是运行中计算。

值不可修改

可使用的函数: len、cap、real、imag、complex和unsafe.Sizeof

iota 常量生成器

让编译器为每个常量复制相同的表达式,直到声明区结束,或者遇到一个新的赋值语句。

  • 只能在常量的表达式中使用
  • 从0开始,每次递增1
  • 每次 const 出现时,都会让 iota 初始化为0
  • 同一代码块中允许多次使用 iota (不会重新计数),通过"="重新赋值后需要使用 iota 继续应用表达式
  • 支持自定义枚举类型
    • 也就是语言基本类型和对应的命名类型
    • 注意 bytestrings
  • 可通过 “_” 跳过值
  • 可通过 “=” 赋值其他值

声明示例:

const A = iota	 // 0
const (
	B = iota     // 0 -- 每次 const 出现时,都会让 iota 初始化为0 
	C            // 1
    D = "A"		 // A
	E			 // A
	F			 // A
)

const A = iota // 0
const (
	B = iota     // 0 -- 每次 const 出现时,都会让 iota 初始化为0
	C            // 1 -- 从0开始,每次递增1
    D = "A"		 // A -- 可通过 "=" 赋值其他值
	E = iota	 // 3 -- 同一代码块中允许多次使用 iota,通过"="重新赋值后需要使用 iota 继续应用表达式
	F			 // 4
)

type Stereotype int
const (
	TypicalNoob Stereotype = 2 * iota // 2 * 0 => 0 -- 支持自定义枚举类型
    TypicalHipster                	  // 2 * 1 => 2
    TypicalUnixWizard                 // 2 * 2 => 4
    TypicalStartupFounder             // 2 * 3 => 6
)

const (
    _ = iota	// 0 -- 可通过 "_" 跳过值
    _			// 1
    V2			// 2
    _			// 3
    V4			// 4
)

常见用法:

type ByteSize float64

const (
	_           = iota
	KB ByteSize = 1 << (10 * iota) // 1 << (10*1)
	MB                             // 1 << (10*2)
	GB                             // 1 << (10*3)
	TB                             // 1 << (10*4)
	PB                             // 1 << (10*5)
	EB                             // 1 << (10*6)
	ZB                             // 1 << (10*7)
	YB                             // 1 << (10*8)
)

复合类型

数组
底层特点
  • 长度固定
  • 元素类型相同
  • 占用内存连续分配(元素间相邻)
  • 访问任意下标元素的速度相同。(原因是内存连续 + 类型相同 可以定位指定偏移量)
  • 函数传参时通过 值传递
使用示例
// 简单的声明 - 初始化为 "零值"
var array1 [5]int
fmt.Println(array1) // [0 0 0 0 0]

// 赋值初始化 - 初始化所有数值
var array2 = [5]int{10, 20, 30, 40, 50}
fmt.Println(array2) // [10 20 30 40 50]

// 自动推导长度
array3 := [...]int{10, 20, 30, 40, 50}
fmt.Println(array3) // [10 20 30 40 50]

// 赋值初始化 - 初始化指定下标
array4 := [5]int{1: 10, 2: 20}
fmt.Println(array4) //[0 10 20 0 0]

// 数组指针
array5 := [5]*int{0: new(int), 2: new(int)}
*array5[0] = 10
*array5[1] = 20
fmt.Println(array5) // [0xc000010148 <nil> 0xc000010180 <nil> <nil>

// 复制
var array6a [5]string
var array6b := [5]string{"Red", "Blue", "Green", "Yellow", "Pink"}

array6a = array6b    // 长度、元素类型 相同才可以允许相互赋值
fmt.Println(array6a) // [Red Blue Green Yellow Pink]

// 指针 复制
// 指针数组 相互赋值 两边的指针依然是 --- "指向 相同的内存地址"

结构体
声明和使用
// 声明一个结构类型
type Man struct {
	Name   string
	Age    int
	Height float64
	int // 匿名字段. 必须不重复. 访问时为字段类型名
}

// 使用结构类型声明变量 & 初始化为零值
var zhangsan Man

// 使用结构类型声明变量 & 初始化信息
lisi := Man{
	Name: "李四",
}

// 注意零值(默认值)
fmt.Println(zhangsan == Man{}, lisi) // true {李四 0 0}
内嵌结构体
  • 默认访问最外层 字段、方法
  • 最外层可以重新声明嵌入类型定义的 字段、方法
  • 可以通过层级独立访问内嵌类型的方法
type Admin struct {
	// 内嵌的结构体
	User  Man
	Level int
}

teacher := Admin{
    User: Man{
    	Name: "李四",
    },
    Level: 127,
}

引用类型

切片
底层特点
  • 是一种数据结构。指针、长度、容量(指针指向底层数组首个元素内存地址
  • 长度动态变更。通过 append扩容、通过二次切片缩小
  • 函数传参return返回时 复制切片本身,不涉及底层数组
  • 容量必定大于等于长度
  • 底层数组元素小于1000时,扩容系数为2;大于1000时扩容系数为1.25

PS: 解决在函数中产生扩容后 “新切片指针” 和 "旧切片指针"指向的底层数组不同的问题

// 分配包含 100 万个整型值的切片
slice := make([]int, 1e6)

// 将 slice 传递到函数 foo
slice = foo(slice)

// 函数 foo 接收一个整型切片,并返回这个切片
func foo(slice []int) []int {
    ...
    return slice
}
使用示例
// 声明长度 默认容量
slice := make([]string, 5) // len:5 cap:5

// 声明长度 声明容量
slice := make([]string, 5, 6) // len:5 cap:6

// 初始化 所有值 自动推断长度、容量
slice := []string{"Red", "Blue", "Green", "Yellow", "Pink"} // len:5 cap:5

// 初始化 指定值 自动推断长度、容量
slice := []string{99: ""}	// len:100 cap:100

// 指针说明
slice := []int{10, 20, 30, 40, 50}
newSlice := slice[1:3]			// 此处指针后移至 索引"1"处; len:2 cap:4
newSlice[1] = 35 				// 两个切片指向同一个底层数组,因此修改是同步的
newSlice = append(newSlice, 6) 	// 当底层容量大于长度时(未扩容) 指向的还是同一个底层数组
fmt.Println(slice, newSlice) 	// [10 20 35 6 50] [20 35 6]

// 针对 ·当底层容量大于长度时(未扩容) 指向的还是同一个底层数组· 的问题可以通过第三个参数解决
newSlice := slice[1:3:3] // 指定容量 slice[i:x:y] len:x-i cap:y-i

// 扩容后
newSliceAdd := append(slice, 5)
// 此处有两个特别的:
// 1. 由于本身切片属于隐性指针,所以打印内存地址的时候不需要加上 &slice
// 2. 这里的指针指向的是首个元素的内存地址, 所以起始位置不一样的也会不同
// 扩容其实就是创建一个更大的数组,然后把数据复制过去
fmt.Printf("old指针地址:%p new指针地址:%p \n", slice, newSliceAdd) //old指针地址:0xc00000c3c0 new指针地址:0xc00001c1e0

// 切片合并
s1 := []int{1, 2}
s2 := []int{3, 4}
fmt.Println(append(s1, s2...)) // [1 2 3 4]

// 切片迭代
for index, val := range slice {
    // index, val 属于值传递 && 有效范围在代码块里面
    fmt.Printf("%v = %v\n", index, val)
}
nil切片和空切片

不论nil 切片还是空切片,都没有分配任何存储空间。

nil 切片:声明&不初始化

var slice []int
fmt.Printf("val:%v isnil:%v type:%T len:%d, cap:%d\n", slice, nil == slice, slice, len(slice), cap(slice))

// val:[] isnil:true type:[]int len:0, cap:0

空切片:声明&初始化空切片

// 两种方式等价
slice := make([]int, 0)
slice := []int{}
fmt.Printf("val:%v isnil:%v type:%T len:%d, cap:%d\n", slice, nil == slice, slice, len(slice), cap(slice))

// val:[] isnil:false type:[]int len:0, cap:0
切片复制

copy返回被复制的元素数量,它会是 len(src) 和 len(dst) 中较小的那个。来源和目标的底层内存可以重叠

  • 将元素从来源切片复制到目标切片中,
  • 将字节从字符串复制到字节切片中

func copy

func copy(dst, src []Type) int
映射
底层特点
  • 映射是一个存储键值对的无序集合. (底层为散列。TODO::书中没有详细备注,暂时理解为hashmap)
  • 迭代时是无序的,可能每次的迭代顺序是不一样的
  • 迭代时当类型未定义时返回对应类型的零值
  • nil映射无法直接赋值
  • 函数传参时 会用很小的代价复制,不涉及底层数组(TODO::后面详细看看为啥)
使用示例
// 创建一个映射
colors := make(map[string]string)

// 创建一个映射 并初始化
colors := map[string]string{"Red": "#da1337", "Orange": "#e95a22"}

colors["read"] = "#cccc"
fmt.Println(colors)

m := make(map[string]int)

// 添加 & 修改
m["Answer"] = 42
fmt.Println("The value:", m["Answer"])
m["Answer"] = 48
fmt.Println("The value:", m["Answer"])

for index := range m {
	fmt.Println("for index:", index)
}

// 删除
delete(m, "Answer")
fmt.Println("The value:", m["Answer"])

// 检测某个键是否存在 -- 不存在 exists 为 false
value, exists := m["Answer"]
fmt.Println("The value:", value, "Present?", exists)
nil映射

nil 映射:声明&不初始化nil映射无法直接赋

var dict map[string]int
fmt.Printf("val:%v isnil:%v type:%T\n", dict, nil == dict, dict)

// val:map[] isnil:true type:map[string]int

dict["aaa"] = 1 // 会报错:assignment to entry in nil map
函数&方法

语法

func name(parameter-list) (result-list) {
	body
}
错误处理策略
  1. 直接返回子程序的错误

    resp, err := http.Get(url)
    if err != nil{
    	return nill, err	// 返回子程序错误
    }
    
  2. 失败重试 + 超时时间(最大重试次数)

  3. 输出错误信息并结束程序

    if err := WaitForServer(url); err != nil {
    	fmt.Fprintf(os.Stderr, "Site is down: %v\n", err)
    	os.Exit(1)
        // log.Fatalf("Site is down: %v\n", err) // 和上面相同效果的代码
    }
    
  4. 只需要输出错误信息就足够了,不需要中断程序的运行

  5. 我们可以直接忽略掉错误

文件结尾错误(EOF)
in := bufio.NewReader(os.Stdin)
for {
	r, _, err := in.ReadRune()
	if err == io.EOF {
		break // finished reading
	}
	if err != nil {
		return fmt.Errorf("read failed:%v", err)
	}
	// ...use r…
}
匿名函数

函数字面量的语法和函数声明相似,区别在于func关键字后没有函数名。函数值字面量是一种表达式,它的值被成为匿名函数(anonymous function)。

通过这种方式定义的函数可以访问完整的词法环境(lexical environment),这意味着在函数中定义的内部函数可以引用该函数的变量

作用域导致的BUG
for _, i := range []byte("CBA") {
    go func() {
        fmt.Println(i)	// 由于这里引用了块中的变量,所以运行的结果是一样的,!!!而不是预期的值!!!
    }()
}
time.Sleep(2 * time.Second)

// 解决办法思路一致:增加副本

// 解决办法 1
for _, i := range []byte("CBA") {
    i := i	// 循环变量的副本
    go func() {
        fmt.Println(i)
    }()
}
time.Sleep(2 * time.Second)

// 解决办法 2
for _, i := range []byte("CBA") {
    go func(i byte) {
        fmt.Println(i)
    }(i)	// 参数传递副本
}
time.Sleep(2 * time.Second)
可变参数

参数列表的最后一个参数类型之前加上省略符号“…”,这表示该函数会接收任意数量的该类型参数。

func sum(vals ...int) {
	fmt.Printf("参数类型:%T is nil:%v 参数值:%v \n", vals, vals == nil, vals)
}

sum() 					// 参数类型:[]int is nil:true 参数值:[] 
sum(1, 2, 3)			// 参数类型:[]int is nil:false 参数值:[1 2 3] 
sum([]int{5, 6, 7}...)	// 参数类型:[]int is nil:false 参数值:[5 6 7] 
方法

如果一个函数有接收者,这个函数就被称为方法

接收者类型

它的作用是将函数与接收者的类型绑在一起一般情况下会使用其中一种,不会混用

  • 值接收者。调用时会使用这个值的一个副本来执行
  • 指针接收者。调用时能够修改其接收者实际指向的值
指针重定向

指针接收者的方法被调用时,接收者( 声明对应类型的值 )既能为(隐性转换为 &p)又能为指针

// 普通函数
func manChangeName(man *Man, name string) {
    man.Name = name
}

m := Man{}
manChangeName(m, "new") // 编译错误!因为类型不对,函数定义传指针
manChangeName(&m,"new") // OK

// 方法
func (man *Man) changeName(name string) {
    man.Name = name
    // 对于下方的 "ma", 由于返回的就是指针,所以可以调用
    // 对于下方的 "mv", 这里Go隐性转换了访问方式 (&p).Name = name
}

ma := &Man{}			// 这里返回的是指针
ma.changeName("new")	// OK

mv := Man{}				// 这里返回的是值, 但为什么可以调用指针接收者的方法?见上面备注
mv.changeName("new")	// OK

值接收者的方法被调用时,接收者( 声明对应类型的值 )既能为又能为指针(*隐性转换为 p)

// 普通函数
func manGetName(man Man) string {
    return man.Name
}

m := Man{Name:"张三"}
manGetName(m ) // OK
manGetName(&m) // 编译错误!因为类型不对,函数定义传指针

// 方法
func (man *Man) getName() string {
    return man.Name
    // 对于下方的 "ma", 这里Go隐性转换了访问方式 (*p).Name . 指向底层数据
}

ma := &Man{}	// 这里返回的是指针,但为什么可以调用值接收者的方法?见上面备注
ma.getName()	// OK

mv := Man{}		// 这里返回的是值
ma.getName()	// OK

PS:所以要防止数据的指针传递时不小心修改到原始数据

m := &Man{Name:"张三"}
n := m

n.changeName("李四")
fmt.Println(n.getName(), m.getName()) // 李四 李四. !!!修改到原来的数据了!!!

fmt.Printf("n内存地址:%p m内存地址:%p \n", &*m, &*n) // 一样的

接口类型

接口是用来定义行为的类型。这些被定义的行为不由接口直接实现,而是通过方法由用户定义的类型实现

空接口

指定了零个方法的接口值被称为 *空接口:( **空接口可保存任何类型的值 *)

interface{}

// 接收 任意类型的 参数
func Decode(v interface{}) error {
    // ...
}
底层原理

TODO::itable 什么的还是要最后确认后才可以确定;下面是初略的理解。

实体类型(用户定义的类型)赋值给接口时,接口通过两部分数据来保存对应的信息:

  • 内部表 iTable 的地址
    • 实体对象 类型. (值类型、指针类型)
    • 方法集。内部映射了 接口类型声明的方法 - 实体对象方法地址(TODO::看报错像是这样的)
  • 实体对象 值的地址

PS:实体对象 描述不正确,是自己瞎起的。就是 实体类型 初始化后

定义方法
type Man interface {
    setName(n string) string
    getName() string
    online()
}
使用场景

直接赋值;获取实体类型的值和接口需要的方法集

// 假设 管理员角色 包含 人的结构体信息
man Man := Manager{Name:"张三", Online:false}

man.online() // 执行用户上线
man.setGuestAcl(....) // !!!这里要特别注意,此处是不可能调用到的!!!

类型约束;使用函数的时候进行类型约束,是否实现对应的方法集

man := Manager{Name:"张三", Online:false}


func resetDefaultUserName(m Man) {
    _ := m.setName(DEFAULT_MAN_NAME)
    man.setGuestAcl(....) // !!!这里要特别注意,此处也是不可能调用到的!!!
}
方法集规则

方法集定义了一组关联到实体类型的值或者指针的方法。(明确一点 int 和 *int 不是一个类型

实体类型定义方法时使用的接收者的类型决定了这个方法是关联到值,还是关联到指针,还是两个都关联

接收者类型值类型
指针接收者 (v *T)指针类型
值接收者(v T)指针类型 & 值类型

PS:特殊场景

package main

import "fmt"

// 声明一个 命名类型
type duration int

// 指针接收者类型
func (d *duration) pretty() string {
	return fmt.Sprintf("Duration: %d", *d)
}

func main() {
	// 默认 Go 会尽可能的进行 指针重定向;但已下会报错查找不到
	duration(42).pretty()
	
	// 这样却可以,因为隐性的转换了
	d := duration(42)
	d.pretty()
}

Goroutines和channel

goroutine

Go 语言的并发同步模型来自一个叫作通信顺序进程(Communicating Sequential Processes, CSP)的范型(paradigm)。 CSP 是一种消息传递模型,通过在 goroutine 之间传递数据来传递消息,而不是对数据进行加锁来实现同步访问。用于在 goroutine 之间同步和传递数据的关键数据类型叫作通道(channel)。

当主函数返回时,所有的goroutine都会直接打断,程序退出。

竞争状态

如果两个或者多个 goroutine 在没有互相同步的情况下,访问某个共享的资源,并试图同时读和写这个资源,就处于相互竞争的状态,这种情况被称作竞争状态(race candition)。

通过 go build -race( -race 使用竞争检测器)来编译执行发现问题

避免竞争状态的方法如下:

  • 尽量避免写变量
  • 避免从多个goroutine访问变量。通过单一goroutine访问,通过同步通道来设置和输出对应的值
  • 允许很多goroutine去访问变量,但是在同一个时刻最多只有一个goroutine在访问
    • 通过设置容量为1的buffer channel来限制同一时刻访问的goroutine数量
    • 通过互斥锁 sync.Mutex 限制访问

共享资源锁

原子函数

以很底层的加锁机制来同步访问整型变量和指针 。强制同一时刻只能有一个 goroutine 运行并完成指定操作。

sync/atomic

import "sync/atomic"

// 类型共六种:int32, int64, uint32, uint64, uintptr, unsafe.Pinter
// 操作共五种:增减, 比较并交换, 载入, 存储,交换

// 增减 => AddT => AddInt64
func AddInt32(addr *int32, delta int32) (new int32)

// 载入 => LoadT => LoadInt64
// 存储 => StoreT => StoreInt64
func LoadInt32(addr *int32) (val int32)
func StoreInt32(addr *int32, val int32)

// 交换 => SwapT => SwapInt64
func SwapInt32(addr *int32, new int32) (old int32)

// 比较并交换(就是 CAS) => CompareAndSwapT => CompareAndSwapInt64
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
互斥锁

互斥锁用于在代码上创建一个临界区,保证同一时间只有一个 goroutine 可以执行这个临界区代码

当手动调用runtime.Gosched()强制将当前 goroutine 退出当前线程后,调度器会再次分配这个 goroutine 继续运

import "sync"

var mutex sync.Mutex

mutex.Lock()
// ...临界区内容...
mutex.Unlock()
读写锁

允许多个只读操作并行执行,但写操作会完全互斥

这种锁叫作“多读单写”锁(multiple readers, single writer lock),Go语言提供的这样的锁是sync.RWMutex

RLock只能在临界区共享变量没有任何写入操作时可用

var mu sync.RWMutex
var balance int
func Balance() int {
    mu.RLock() // readers lock
    defer mu.RUnlock()
    return balance
}

RWMutex需要更复杂的内部记录,所以会让它比一般的无竞争锁的mutex慢一些。

初始化懒加载
var loadIconsOnce sync.Once
var icons map[string]image.Image
// Concurrency-safe.
func Icon(name string) image.Image {
	loadIconsOnce.Do(loadIcons)	// 真正需要调用的时候初始化
	return icons[name]
}

func loadIcons() {
	icons = make(map[string]image.Image)
	icons["spades.png"] = loadIcon("spades.png")
	icons["hearts.png"] = loadIcon("hearts.png")
	icons["diamonds.png"] = loadIcon("diamonds.png")
	icons["clubs.png"] = loadIcon("clubs.png")
}

通道

可通过通道共享的内容:内置类型命名类型结构类型引用类型的值或者指针

读写未初始化chan (nil)都会阻塞

若在关闭Channel后继续从中接收数据,接收者就会收到该Channel返回的零值

对于从无缓冲Channel进行的接收,发生在对该Channel进行的发送完成之前

对于带缓冲的Channel,对于Channel的第K个接收完成操作发生在第K+C个发送操作完成之前,其中C是Channel的缓存大小。

// 无缓冲的通道
unbuffered := make(chan int)
// 有缓冲的通道
buffered := make(chan string, 10)

// 发送
buffered<-true
// 读取
val := <-buffered
val, isclose := <-buffered

// 关闭通道
close(buffered)

// 只写通道 out chan<- int
// 只读通道 in <-chan int
func squarer(out chan<- int, in <-chan int) {
	for v := range in {
		out <- v * v
	}
	close(out)
}

无缓冲的通道
  • 发送者:阻塞等待
  • 接收者:阻塞等待
  • 同步方式:同步发送。保证发送、接收的goroutine会在同一时间进行数据交换

goroutines泄漏:

goroutines因为使用同步通道没有人接收而被永远卡住

和垃圾变量不同,泄漏的goroutines并不会被自动回收,因此确保每个不再需要的goroutine能正常退出是重要的。

无缓存的Channel上的发送操作总在对应的接收操作完成前发生

有缓冲的通道
  • 发送者:通道没有可用缓冲区容纳被发送的值时,发送动作阻塞
  • 接收者: 通道中没有要接收的值时,接收动作阻塞
  • 同步方式:异步发送。不保证同时交换数据
close
  • 当通道关闭后, goroutine 依旧可以从通道接收数据,但是不能再向通道里发送数据。(保证数据不会丢失)
  • 从一个已经关闭没有数据的通道里获取数据,总会立刻返回,并返回一个通道类型的零值。(不能通过值判断是否有数据,而应该通过可选标志位获取通道状态)
  • 这个方法应该只由发送者调用, 而不是接收者。却调用的channel应该是双向的或者只写(chan<- Type)
select

语法特点:

  • 表达式会被解析求值。求值顺序:自上而下、从左到右
  • 多个同时到达则随机选择一个完成(CSP理论);存在default则执行default;否则阻塞等待
遍历方式总结
for-range

通道关闭时自动退出循环

for data := range ch {
    // ...
}
for

通道关闭时手动断言关闭退出

for {
    in, ok := <-ch
    if !ok {
        return // 通道关闭
    }
    // ....
}
for-select

通道关闭时手动置为nil,避免总是获取关闭通道的分支

	for {

		select {
		case n, ok := <-a1:
			if !ok {
				a1 = nil	// 设置为nil不再进入该分支
			} else {
				c <- n
			}

		case n, _ := <-a2:
			c <- n			// 通道关闭后死循环了,每次都进入该分支
		default:
			fmt.Println("default")
			time.Sleep(time.Second)
		}
	}

标准库学习

测试 testing

_test.go 为后缀名的源文件是go test的一部分

_test.go 文件中,有三种测试类型:

  • 测试函数(格式:Test*)
  • 用于测试程序的一些逻辑行为是否正确
  • `go test命令会调用这些测试函数并报告测试结果是PASS或FAIL
  • 基准测试函数(格式:Benchmark*)
    • 用于衡量一些函数的性能
    • go test命令会多次运行基准函数以计算一个平均的执行时间
  • 示例函数 (格式:Example*)
测试覆盖率

使用go tool cover -html=cover.out可以查看当前的测试覆盖率。

红色:未覆盖

绿色:覆盖

测试常用方法

在测试函数和基准测试中,会经常使用到以下的方法

是否失败

查询当前的测试函数是否已标记为失败

func (c *T) Failed() bool
标记失败

标记当前的测试函数测试失败

func (c *T) Fail()		// 后续代码继续执行
func (c *T) FailNow()	// 停止执行该测试

func (c *T) Failed() bool
错误日志

对于测试来说,格式化文本只会在测试失败或者设置了 -test.v 标志的情况下被打印出来

对于基准测试来说,为了避免 -test.v 标志的值对测试的性能产生影响, 格式化文本总会被打印出来

func (c *T) Log(args ...interface{})
func (c *T) Logf(format string, args ...interface{})
常用操作
// 等价于 Log[f] + Fail()
func (c *B) Error(args ...interface{})
func (c *B) Errorf(format string, args ...interface{})

// 等价于 Log[f] + FailNow()
func (c *B) Fatal(args ...interface{})
func (c *B) Fatalf(format string, args ...interface{})
测试函数 Test

运行指令:go test -v

必须导入 t *testing.T

名字必须以Test开头, 可选的后缀名必须以大写字母开头

func TestName(t *testing.T) {
	// ...
}
基准测试 Benchmark

运行指令:go test -test.bench=.*

必须导入 t *testing.B

名字必须以Benchmark开头, 可选的后缀名必须以大写字母开头

import "testing"

func BenchmarkIsPalindrome(b *testing.B) {
    // 指定执行 N 次
	for i := 0; i < b.N; i++ {
		IsPalindrome("A man, a plan, a canal: Panama")
	}
}
示例函数 Example

名字必须以Example开头, 可选的后缀名必须以大写字母开头

它的作用如下:

  • 文档用例说明(编译时会检查示例代码
  • 生成在线运行脚本
一、Go语言的由来      Go语言亦叫Golong语言,是由谷歌Goggle公司推出。Go语言的主要开发者有:肯.汤姆逊(Ken Thompson)、罗布.派克(Rob Pike)和罗伯特.格里泽默(Robert Griesemer)。这三个都是大神,稍介绍一下他们的贡献:     肯.汤姆逊(Ken Thompson):图灵奖得主,Uinx发明人,B语言作者(C语言前身),还做飞行员,后来被谷歌挖走。     罗布.派克(Rob Pike):Unix团队和Plan 9操作系统计划的成员,与Ken老爷子共事多年,并共创出广泛使用的UTF-8 字元编码。     罗伯特.格里泽默(Robert Griesemer):曾协助制作Java的HotSpot编译器,和Chrome浏览器的JavaScript引擎V8。     膜拜一下大神的容颜:Ken老爷子(左),Rob Pike(右)         二、开发Go语言的初衷     根据Go语言开发者自述,近10多年,从单机时代的C语言到现在互联网时代的Java,都没有令人满意的开发语言,而 C++往往给人的感觉是,花了100%的经历,却只有60%的开发效率,产出比太低,Java和C#的哲学又来源于C++。并且,随着硬件的不断升级,这些语言不能充分的利用硬件及CPU。因此,一门高效、简洁、开源的语言诞生了。 三、Go语言的特点    Go语言保证了既能到达静态编译语言的安全和性能,又达到了动态语言开发速度和易维护性,有人形容Go语言:Go = C + Python , 说明Go语言既有C静态语言程序的运行速度,又能达到Python动态语言的快速开发。 Go语言有以下特性: 1.自动垃圾回收     C/C++最头疼的就是指针问题,一不小心就野指针了或者又越界了。在Go语言里再也不用担心,也不用考虑delete或者free,系统自动会回收。 2.函数可以返回多个值     这个很神奇,大多数语言只能返回一个值,Go语言可以返回多个值。这个功能使得开发者再不用绞尽脑汁的想到底怎么返回值的设计,也不用为了传值专门定义一个结构体。 3.并发编程     Go语言天然并发,只需要关键字“go”就可以让函数并发执行,使得并发编程变得更为简单,这也是Go语言最大的优势。 四、Go语言能做什么开发     Go语言是非常有潜力的语言,是因为它的应用场景是目前互联网非常热门的几个领域,比如区块链开发、大型游戏服务端开发、分布式/云计算开发。像Goggle、阿里、京东等互联网公司都开始用Go语言开发自己的产品。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值