共计 14371 个字符,预计需要花费 36 分钟才能阅读完成。
Go 官网:https://go.dev/ 或 https://golang.google.cn/
Go 语言圣经《The Go Programming Language》:https://www.gopl.io/
Go 语言圣经(中文版):https://books.studygolang.com/gopl-zh/
Go 语言特点:
- 强类型,编译型语言
 - Go 语法不区分平台,编译时需要两个环境变量来控制:GOOS 设定运行平台,GOARCH 设定目标运行平台的体系架构
 
环境搭建
GOROOT
Go 语言的安装目录,存放 Go 语言的标准库,可设置在 /usr/local/go。
GOPATH
查看 GOPATH 环境变量:echo %GOPATH%
GOPATH 用于设置 Go 语言的工作空间目录,用来存放开发中需要用到的代码包。Windows 下可配置用户变量 GOPATH 值为 D:\Go,并在 D:\Go 下新建 3 个文件夹:
- bin:executable,放编译后的可执行文件,go install 后的可执行文件
 - pkg:package,缓存包
 - src:source,放自己的源代码,此目录下,一个文件夹就是一个工程
 
GOPROXY
配置代理,可设置为:https://goproxy.cn,direct
Go Tool
当进行导包时,Go Tool 在进行构建会搜索 GOROOT 和 GOPATH 环境变量指定的目录。
- go get:用来获取第三方库,使用 gopm 来获取无法下载的包
 - go build:编译,生成可执行文件
 - go install:将应用程序安装到 GOBIN(%GOPATH%/bin)下面,产生 pkg 文件和可执行文件
 - go run:直接编译运行,不会生成可执行文件
 
入门案例
新建一个 gotest 文件夹,进入 gotest 目录,运行:go mod init gotest,在当前目录初始化 mod,会生成一个 go.mod 文件,用于管理依赖。
接下来,新建 main.go,输入以下内容:
package main // 定义包名 main
import "fmt" // 引用库名 fmt
func main() { // 定义函数 main
  fmt.Println("Hello, world!")
}
运行命令:go run main.go查看输出结果。
Go 是以“包”为管理单位的,每个 Go 源文件必须先声明所属的包,通过 package 声明。main 包是 Go 语言程序的入口包,一个 Go 语言程序只能有一个 main 包。如果一个程序没有 main 包,那么编译时会出错,无法生成可执行文件。
main 函数是 Go 语言程序的入口函数,也即程序启动后运行的第一个函数。main 函数只能声明在 main 包中,并且一个 main 包中有且仅有一个 main 函数。
Go 语言不需要在语句或者声明的末尾添加分号,除非一行上有多条语句。实际上,编译器会主动把特定符号后的换行符转换为分号,因此换行符添加的位置会影响 Go 代码的正确解析。
注释
- 单行注释:以 // 开头
 - 多行注释:也叫块注释,以 / 开头,并以 / 结尾
 
每一个包都应该有相关注释,在使用 package 语句声明包名之前添加相应注释,用来对包的功能及作用进行简要说明。
同时,在 package 语句之前的注释内容将被默认为是这个包的文档说明。一个包可以分散在多个文件中,但是只需要对其中一个进行注释说明即可。
变量
变量声明:var x int对应 keyword name type。变量声明后没有赋值,都对应一个零值:
- 整型为 0,浮点, 为 0.0
 - 布尔型默认值为 false
 - 复数型默认值为 0 + 0i
 - 字符串默认值为 “”
 - 复合类型,默认值为 nil
 
变量初始化
:=声明并赋值这种简洁语法,只能在函数内使用,不能在包内使用。
匿名变量:
package main
import "fmt"
func main() {
  a, _ := 1, 2   // 空白标识符 _ 即匿名变量
  fmt.Println(a) // Output: 1
}
匿名变量不占用命名空间,也不会分配内存。
声明多个变量:
var a, b, c int // 声明多个相同类型变量
// 声明多个不同类型变量
var (
  name      string
  age       int
  isMarried bool
)
变量生命周期
Go 内存中应用了两种数据结构用于存放变量:
- 堆(heap):用于存放进程执行中被动态分配的内存段。它的大小并不固定,可动态扩张或缩减。当进程调用 malloc 等函数分配内存时,新分配的内存就被动态加入到堆上(堆被扩张)。当利用 free 等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)
 - 栈 (stack):又称堆栈,用来存放程序暂时创建的局部变量,也就是函数大括号{} 中定义的局部变量
 
常量
定义常量:const male, female = 1, 2,只能是布尔型、数字型(整型、浮点型、复数)和字符串型。
iota 常量生成器
iota 计数器是 Go 语言的常量计数器,只能在常量表达式中使用:
- iota 在 const 关键字出现时将被重置为 0
 - const 中每新增一行常量声明将使 iota 计数一次,iota 可理解为 const 语句块中的行索引
 
使用常量定义枚举类型:
func main() {
  // 普通枚举
  const (
    x = iota
    y
    z
  )
  fmt.Println(x, y, z) // 0 1 2
  // 自增值枚举
  const (b = 1 << (10 * iota)
    kb
    mb
    gb
  )
  fmt.Println(b, kb, mb, gb) // 1 1024 1048576 1073741824
}
数据类型
基本数据类型(整型、浮点型、字符串、布尔类型、数组、结构体)都范属于值类型,特点:当声明未赋值之前存在一个默认值(zero value)。
指针类型(切片、map、channel)都属于引用类型,特点:当声明未赋值之前是没有开辟空间的,即没有默认值。
整型 int
- int8:一个字节,范围 -128~127;uint8 无符号:一个字节,范围 0~255
 - int:与平台相关,32 位操作系统上是 int32,64 位操作系统上是 int64
 - uintptr:保存一个指针地址,可以进行指针运算
 
byte 与 rune
- byte:等价于 uint8,可表达只占用 1 个字节的 ASCII 编码的字符
 - rune:等价于 int32,使用单引号定义,返回采用 UTF- 8 编码的 Unicode 码点
 - Go 语言中没有 char
 
func main() {
  ch1 := 'A' // ASCII 码表中,A 的值是 65
  ch2 := 65
  ch3 := '\x41' // 65 十六进制表示
  ch4 := '\101' // 65 八进制表示
  var ch5 rune = '\u0041'      // 4 个字节使用前缀 \u
  var ch6 int64 = '\U00000041' // 8 个字节使用前缀 \U
  fmt.Println(ch1, ch2, ch3, ch4, ch5, ch6)
}
字符串 string
- 双引号字符串是普通字符串字面量,支持转义字符和字符串插值
 - 反引号字符串则是原始字符串字面量,不支持转义字符和字符串插值
 - 单引号只能标识字符
 
字符串数据类型占⽤ 16 字节空间,前 8 字节是⼀个指针,指向字符串值地址,后 8 字节是⼀个整数,标识字符串长度。字符串数据结构跟切片有些类似,事实上字符串和 byte 切片经常发生转换。
定义多行字符串,可使用反引号,反引号间换行将被作为字符串中的换行,但是所有的转义字符均无效,文本将会原样输出:
func main() {
  s := "hello"
  s2 := `h
i
`
  fmt.Println(s)
  fmt.Println(s2)
}
字符串所占字节长度可通过 len()函数来获取,字符串长度使用 utf8.RuneCountInString()函数:
func main() {
  s := " 张三 "
  fmt.Println(len(s)) // 6
  fmt.Println(utf8.RuneCountInString(s)) // 2
}
字符串拼接:
func main() {
  s1 := " 张三 "
  s2 := " 你好 "
  s3 := s1 + s2
  s4 := s3 +
    " 呀!"
  fmt.Println(s3) // 张三你好
  fmt.Println(s4) // 张三你好呀!
  var strBuilder bytes.Buffer
  strBuilder.WriteString(s1)
  strBuilder.WriteString(s2)
  fmt.Println(strBuilder.String()) // 张三你好
}
使用 WriteString()可以节省内存,提高处理效率。
获取某个字符:
func main() {
  s := " 张三你好 "
  fmt.Println(string([]rune(s)[2])) // 输出 你
}
字符串转字节串:
func main() {
  var s = " 张三 "
  var b = []byte(s) // 默认用 uft- 8 进行编码
  fmt.Println(b)    // [229 188 160 228 184 137]
  // byte 转 string
  fmt.Println(string(b)) // 张三
}
strings 包:
package main
import (
  "fmt"
  "strings"
)
func main() {
  s := "hello"
  fmt.Println(strings.ToUpper(s))        // HELLO
  fmt.Println(strings.HasPrefix(s, "h")) // true 是否以指定前缀开头,指定后缀结尾用 HasSuffix()
  fmt.Println(strings.Contains(s, "e"))  // true
}
其他字符串操作:
- Fields, Split, Join
 - Contains, Index
 - ToLower, ToUpper
 - Trim, TrimRight, TrimLeft
 
遍历字符串:
func main() {
  s := " 我是张三 "
  for i, ch := range s {fmt.Printf("%d %c\n", i, ch) // %c 打印字符
  }
}
类型别名
type NewInt int     // 将 NewInt 定义为 int 类型
type IntAlias = int // 将 int 取一个别名叫 IntAlias
func main() {
  var a NewInt
  fmt.Printf("a type: %T\n", a) // main.NewInt
  var b IntAlias
  fmt.Printf("b type: %T\n", b) // int
}
类型转换
Go 语言中只有 强制类型转换,没有隐式类型转换。只有相同底层类型的变量之间可以进行相互转换,不同底层类型的变量相互转换时会引发编译错误(如将 bool 类型转换为 int 类型)。
string 与 int 类型转换:
package main
import (
  "fmt"
  "reflect"
  "strconv"
)
func main() {
  a := 100
  b := strconv.Itoa(a)           // convert int to string
  fmt.Println(reflect.TypeOf(b)) // output: string
}
ParseBool 返回字符串表示的 bool 值,只接受 1、0、t、f、T、F、true、false、True、False、TRUE、FALSE,否则返回错误:
func main() {b, _ := strconv.ParseBool("true")
  b2, err := strconv.ParseBool("666")
  fmt.Println(b)   // true
  fmt.Println(b2)  // false
  fmt.Println(err) // 返回错误
}
数组
数组其实是和字符串一样的序列类型,不同于字符串在内存中连续存储字符,数组用 [] 语法将同一类型的多个值存储在一块连续内存中。
func main() {names := [3]string{"Alice", "Bob", "Charlie"}
  fmt.Println(names, reflect.TypeOf(names)) // [Alice Bob Charlie] [3]string
  ages := [...]int{23, 24, 25}            //  […]省略长度赋值
  fmt.Println(ages, reflect.TypeOf(ages)) // [23 24 25] [3]int
  names2 := [...]string{1: " 张三 ", 3: " 王五 "} // 索引设置
  fmt.Println(names2[3], names2)          // 王五 [张三  王五]
  arr := [2][3]int{{1, 2, 3}, {4, 5, 6}} // 二维数组
  fmt.Println(arr[0][1])                 // 2
}
遍历数组:
func main() {var ages = [...]int{23, 24, 25}
  for i, v := range ages {fmt.Println(i, v)
  }
}
指针
数据在内存中的地址称为指针,如果一个变量存储了一份数据的指针,称为指针变量。Go 语言中对指针存在两种操作:取址和取值。
| 符号 | 名称 | 作用 | 
|---|---|---|
| & 变量 | 取址符 | 返回变量在内存中的地址 | 
| * 指针变量 | 取值符 | 返回指针指向变量的值 | 
func main() {
  var a = 100
  var p = &a
  fmt.Println("p 值:", p)      // p 值: 0xc00000a0c8
  fmt.Println("p 地址对应值:", *p) // p 地址对应值: 100
}
new 函数
初始化一个指针变量,其值为 nil,nil 值是不能直接赋值的。new 函数是创建指针的另一种方法,其创建一个对应类型的指针,创建过程会分配内存,被创建的指针指向默认值。
func main() {var p *int = new(int)
  fmt.Println(p)  // 0xc00000a0c8
  fmt.Println(*p) // 0
  *p = 10
  fmt.Println(*p) // 10
}
通过内建 new 函数返回一个指向新分配类型为 int 的指针,指针值为 0xc00000a0c8,这个指针指向的内容值为零(zero value)。
切片 slice
是一个动态数组(对数组的引用,即数组的视图),因为数组长度是固定的,所以操作起来很不方便。在开发中数组并不常用,切片类型才是大量使用的。
切片创建有两种方式:
- 
从数组或者切片上切取获得切片对象
func main() {var ages = [...]int{23, 24, 25, 26, 27} var slice = ages[:] // 切片存 3 个值:起始地址 长度 容量(切片开始位置到数组最后位置长度)fmt.Println(len(slice), cap(slice)) // 长度 5 容量 5 newSlice := slice[1:3] fmt.Println(len(newSlice), cap(newSlice)) // 长度 2 容量 4 } - 直接声明切片
 
func main() {var slice = []int{23, 24, 25, 26, 27}
  fmt.Println(len(slice), cap(slice)) // 长度 5 容量 5
}
make 函数
对于引用类型变量,不光要声明它,还要为它分配内容空间。要分配内存,就需要 make 函数用于 channel、map 以及切片的内存创建,而且返回类型就是这三个类型本身。
使用 make 函数(make([]Type, size, cap))动态创建切片:
func main() {s := make([]int, 2, 10)
  fmt.Println(s, len(s), cap(s)) // Output: [0 0] 2 10
}
size 为分配的元素数量,cap 为预分配的元素数量,这个值设定后不影响 size,只是提前分配空间,降低多次分配空间造成的性能问题。
append
func main() {var s []int
  s1 := append(s, 1, 2, 3)
  fmt.Println(s1) // Output: [1 2 3]
  s2 := append(s1, s1...) // ... 是解包操作
  fmt.Println(s2) // Output: [1 2 3 1 2 3]
  s3 := make([]int, 2, 3)
  s4 := append(s3, 1)           // s4 is [0 0 1]
  s5 := append(s4, 1)           // s5 is [0 0 1 1]
  fmt.Println(len(s5), cap(s5)) // Output: 4 6 容量不够 2 倍扩容,此时底层数组已经发生改变
}
切片插入元素:
func main() {var s = []int{1, 2, 3}
  s1 := append([]int{0}, s...) // 开头添加元素
  fmt.Println(s1)              // Output: [0 1 2 3]
  i := 2
  x := 100
  s2 := append(s[:i], append([]int{x}, s[i:]...)...) // 在索引 i 处添加元素 x
  fmt.Println(s2)                                    // Output: [1 100 3]
}
切片删除元素:
func main() {s := []int{1, 2, 3}
  i := 1
  s = append(s[:i], s[i+1:]...) // 删除索引为 1 的元素
  fmt.Println(s)                // Output: [1 3]
}
copy
func main() {s1 := []int{1, 2, 3}
  s2 := []int{11, 22, 33, 44}
  copy(s1, s2) // copy s2 to s1
  fmt.Printf("s1: %v\n", s1)
  fmt.Printf("s2: %v\n", s2)
}
映射 map
在编程语言中大都会存在一种映射(key-value)类型,在 JS 中叫 json 对象类型,在 Python 中叫字典(dict)类型,而在 Go 语言中则叫 Map 类型。
func main() {info := map[string]string{"name": " 张三 ", "age": "18"} // 直接声明赋值 map[K]V
  fmt.Println(info)
  info2 := make(map[string]string) // 声明并初始化
  info2["name"] = " 张三 "
  info2["age"] = "23"
  fmt.Println(info2)
}
判断某个键是否存在:
func main() {info := map[string]string{"name": " 张三 ", "age": "18"}
  value, is_exist := info["name"]
  if is_exist {fmt.Print(value, " ")
    fmt.Println(is_exist)
  } else {fmt.Println(" 键不存在!")
  }
}
遍历 map:
func main() {info := map[string]string{"name": " 张三 ", "age": "18"}
  for k, v := range info {fmt.Println(k, v)
  }
}
删除 map:
func main() {info := map[string]string{"name": " 张三 ", "age": "18"}
  delete(info, "age")
  fmt.Printf("%v\n", info) // map[name: 张三]
}
线程安全的 map
并发情况下读写 map 时会出现问题,因为 map 内部会对这种并发操作进行检查并提前发现。
需要并发读写时,一般做法是加锁,但这样性能不高。Go 1.9 提供了一种效率较高的并发安全的 sync.Map,和 map 不同,不是以语言原生形态提供,而是在 sync 包下的特殊结构。
func main() {
  var m sync.Map
  m.Store("key", "value") // 存值
  m.Store("key2", "value2")
  m.Store("key3", "value3")
  fmt.Println(m.Load("key")) // 取值
  m.Delete("key") // 删除值
  m.Range(func(k, v interface{}) bool {fmt.Println(k, v) // 遍历
    return true
  })
}
sync.Map 为了保证并发安全有一些性能损失,因此在非并发情况下,使用 map 相比使用 sync.Map 会有更好的性能。
结构体
函数
函数体包含局部变量和自由变量。
输出函数
| 操作系统 | 换行符 | |
|---|---|---|
| Windows | \r\n | |
| macOS | \r | |
| Linux | \n | 
\r:回到行首,\n:到下一行。
Print 和 Println 函数都可以打印出字符串或变量的值,Println 输出到控制台会换行。
Printf 打印出格式化字符串,只可输出字符串类型变量,不可以输出别的类型:
func main() {fmt.Printf("%b\n", 12)  // 1100 二进制表示 十进制(%d) 八进制(%o) 十六进制(%x) 大写十六进制(%X)
  fmt.Printf("%#b\n", 12) // 0b1100 二进制表示带 0b
  fmt.Printf("%+d\n", 12) // +12 带符号整型
  fmt.Printf("%f\n", 3.1415)     // 3.141500 默认精度为 6
  fmt.Printf("%.3f\n", 3.1415)   // 3.142
  fmt.Printf("%e\n", 3.1415*100) // 科学计数法 3.141500e+02
  fmt.Printf("%s\n", "666")  // 666
  fmt.Printf("%q", "") // "" 双引号围绕的字符串
  fmt.Printf("%v\n", "666")  // 666 以默认方式打印变量值
  fmt.Printf("%#v\n", "666") // "666" 相应值 Go 语法表示
  fmt.Printf("%T\n", "666") // string 对应值类型
  fmt.Printf("%t\n", true)  // true 或 false
  fmt.Printf("%-10s %s\n", "666", "777") // 左对齐 宽度为 10
  fmt.Printf("%10s %s\n", "666", "777")  // 右对齐 宽度为 10
}
Sprintf 也是替换字符串,Printf 直接输出到终端,Sprintf 不直接输出到终端,而是返回该字符串。
输入函数
Scan 从标准输入扫描文本,读取由空白符分隔的值保存到传递给本函数的参数中,换行符视为空白符:
func main() {
  var (
    name string
    age  int
  )
  fmt.Print(" 请输入姓名和年龄,用空格分割:")
  fmt.Scan(&name, &age)                    // 输入 张三 18
  fmt.Printf("name:%s age:%d ", name, age) // name: 张三 age:18
}
Scanln 类似于 Scan,它遇到换行立即结束输入,而 Scan 会将换行符作为一个空白符继续下一个输入:
func main() {
  var a, b int
  fmt.Scanln(&a, &b) // 输入 5 回车
  fmt.Println(a + b) // 输出 5 而不是 10
}
Scanf 从标准输入扫描文本,根据 format 参数指定格式去读取由空白符分隔的值保存到传递给本函数的参数中:
func main() {
  var a, b int
  fmt.Scanf("%d+%d", &a, &b) // 输入 1+2
  fmt.Println(a + b)         // 输出 3
}
init 函数
初始化函数,用来进行一些初始化操作,每个源文件都可以包含一个 init 函数。该函数会在 main 函数执行前,被 Go 运行框架调用。
执行顺序:全局变量定义 > init 函数 > main 函数
不定长参数
func add(i ...int) int {
  total := 0
  for _, v := range i {total += v}
  return total
}
func main() {println(add(1, 2, 3)) // 6
}
语句
程序由语句构成,而流程控制语句用来控制程序中每条语句执行顺序,通过控制语句能够实现更丰富的逻辑以及更强大的功能。流程控制方式有:
- 顺序结构
 - 分支结构
 - 循环结构
 
语句分隔符
; 和换行符,都可作为语句分隔符,推荐换行作为分隔符。
分支结构
if 条件里可以赋值,赋值的变量作用域在这个 if 语句里:
func main() {
  if a, b := 1, 2; a > b {println("a is greater than b")
  } else {println("b is greater than a")
  }
  // println(a, b) // a, b 未定义,出错
}
switch 会自动 break,除非使用 fallthrough。switch 后可以没有表达式:
func grade(score int) string {
  switch {
  case score < 0 || score > 100:
    return "Invalid score"
  case score >= 90:
    return "A"
  case score >= 80:
    return "B"
  case score >= 70:
    return "C"
  case score >= 60:
    return "D"
  default:
    return "F"
  }
}
func main() {println(grade(101), grade(95), grade(85), grade(75), grade(65), grade(55), grade(-1))
}
分支语句区别:
- switch 比 if else 更简洁
 - switch 执行效率更高。switch case 不用像 if else 那样遍历条件分支直到命中条件,而只需访问对应索引号的表项从而达到定位分支的目的
 - 到底使用哪一个选择语句,与代码环境有关。如果是范围取值,使用 if else 更为快捷;如果是确定取值,则使用 switch 是更优方案
 
循环语句
Go 语言中循环语句只支持 for 关键字,而不支持 while 和 do while。
func printFile(filename string) {file, err := os.Open(filename)
  if err != nil {fmt.Println("Error:", err)
    return
  }
  defer file.Close()
  scanner := bufio.NewScanner(file)
  for scanner.Scan() { // 读入一行,并移除行末换行符
    println(scanner.Text()) // 调用 scanner.Text() 得到读取的内容}
}
func main() {printFile("test.txt")
}
defer 语句
Go 语言提供的一种用于注册延迟调用机制,是 Go 语言中一种很有用的特性。
func main() {fmt.Println("1")
  defer fmt.Println("2")
  fmt.Println("3")
}
defer 语句注册了一个函数调用,这个调用会延迟到 defer 语句所在函数执行完毕后执行,所谓执行完毕是指该函数执行了 return 语句、函数体已执行完最后一条语句或函数所在协程发生了恐慌。
编程经常会需要申请一些资源,比如数据库连接、打开文件句柄、申请锁、获取可用网络连接、申请内存空间等,这些资源都有一个共同点那就是在使用完之后都需要将其释放掉,否则会造成内存泄漏或死锁等其它问题。但操作完资源忘记关闭释放是正常的,而 defer 可以很好解决这个问题。
当一个函数中有多个 defer 语句时,会形成 defer 栈,后定义的 defer 语句会被最先调用。即按 defer 定义顺序 逆序 执行,也就是说最先注册的 defer 函数调用最后执行。
当执行 defer 语句时,函数调用不会马上发生,会先把 defer 注册的函数及变量拷贝到 defer 栈中保存,直到函数 return 前才执行 defer 中的函数调用。需要格外注意的是,这一拷贝拷贝的是那一刻函数的值和参数的值。注册之后再修改函数值或参数值时,不会生效。
defer 执行时机
在 Go 语言中,函数 return 语句不是原子操作,而是被拆成了两步:
- rval = xxx
 - ret rval
 
而 defer 语句就是在这两条语句之间执行,也就是:
rval = xxx
defer_func
ret rval
defer x = 100
x := 10
return x // rval=10, x=100, ret rval
goto 语句
Go 语言支持 goto 语句,可以无条件地转移到程序中指定行,goto 语句通常与条件语句配合使用。在 Go 程序设计中,一般不建议使用 goto 语句,以免造成程序流程混乱。
文件操作
读文件
package main
import (
  "bufio"
  "fmt"
  "io"
  "os"
)
func readBytes(file *os.File) {var b = make([]byte, 3)
  n, err := file.Read(b)
  if err != nil {fmt.Println("err:", err)
    return
  }
  fmt.Printf(" 读取字节数:%d\n", n)
  fmt.Printf(" 切片值:%v\n", b)
  fmt.Printf(" 读取内容:%v\n", string(b[:]))
}
func readLines(file *os.File) {reader := bufio.NewReader(file)
  for {// (1) 按行读字符串
    strs, err := reader.ReadString('\n') // 读取到换行符为止,读取内容包括换行符
    fmt.Print(err, "\t", strs)
    if err == io.EOF { // io.EOF 读取到文件末尾
      break
    }
  }
}
func readFile() {content, err := os.ReadFile("test.txt") // 包含了打开和读取,适用于较小文件
  if err != nil {fmt.Println("read file failed, err:", err)
    return
  }
  fmt.Print(string(content))
}
func main() {// file, err := os.Open("test.txt")
  // if err != nil {//   fmt.Println("err: ", err)
  // }
  // 当函数退出时让 file 关闭,防止内存泄漏
  // defer file.Close()
  // (1) 按字节读
  // readBytes(file)
  // (2) 按行读
  // readLines(file)
  // (3) 读整个文件
  readFile()}
写文件
package main
import (
  "bufio"
  "fmt"
  "os"
)
func writeByteOrStr(file *os.File) {
  str := " 满江红 666\n"
  // 写入字节切片数据
  file.Write([]byte(str))
  // 直接写入字符串数据
  file.WriteString(" 怒发冲冠, 凭栏处、潇潇雨歇。")
}
func writeByBufio(file *os.File) {writer := bufio.NewWriter(file)
  // 将数据先写入缓存,并不会到文件中
  writer.WriteString(" 大浪淘沙 \n")
  // 必须 flush 将缓存中内容写入文件
  writer.Flush()}
func writeFile() {
  str := " 怒发冲冠,凭栏处、潇潇雨歇。"
  err := os.WriteFile(" 满江红 2.txt", []byte(str), 0666)
  if err != nil {fmt.Println("write file failed, err:", err)
    return
  }
}
func main() {file, err := os.OpenFile(" 满江红.txt", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
  if err != nil {fmt.Println("open file failed, err:", err)
    return
  }
  // 写字节或字符串
  writeByteOrStr(file)
  // flush 缓冲写
  writeByBufio(file)
  // 写文件
  writeFile()}
序列化
序列化:通过某种方式把数据结构或对象写入到磁盘文件中或通过网络传到其他节点的过程。
反序列化:把磁盘中对象或网络节点中传输的数据恢复为数据对象的过程。
package main
import (
  "encoding/json"
  "fmt"
)
func main() {s := []int{1, 2, 3}
  // 序列化
  data, _ := json.Marshal(s)
  fmt.Println(string(data))
  // 反序列化
  s2 := make([]int, 3)
  json.Unmarshal(data, &s2)
  fmt.Println(s2)
}
错误处理
Go 的错误处理流程:当一个函数在执行过程中出现了异常或遇到 panic(),正常语句就会立即终止,然后执行 defer 语句,再报告异常信息,最后退出 goroutine。如果在 defer 中使用了 recover()函数,则会捕获错误信息,使该错误信息终止报告。
注意:导致关键流程出现不可修复性错误时使用 panic,其他使用 error。
defer+recover
func test(a int, b int) {
  // defer 加匿名函数调用,来捕获错误
  defer func() {
    // 内置函数 recover 可以捕获错误
    err := recover()
    if err != nil {fmt.Println(" 出现异常:", err)
    }
  }()
  fmt.Printf("%d / %d = %d\n", a, b, a/b)
}
func main() {test(10, 2)
  test(10, 0)
  fmt.Println(" 除法操作执行成功。。。")
  fmt.Println(" 继续执行下面逻辑。。。")
}
自定义错误
func test(a int, b int) (err error) {
  if b == 0 {
    // 抛出自定义错误
    return errors.New(" 除数不能为 0 ")
  } else {fmt.Printf("%d / %d = %d\n", a, b, a/b)
    return nil
  }
}
func main() {err := test(10, 0)
  if err != nil {fmt.Println(" 出现自定义错误:", err)
    panic(err) //panic 会停止当前 Go 程序的正常执行,并打印错误信息
  }
  fmt.Println(" 除法操作执行成功。。。")
  fmt.Println(" 继续执行下面逻辑。。。")
}
包
导入路径
包的绝对路径就是 GOROOT/src/ 或 GOPATH 后面包的存放路径。
- import “lab/test”:test 包是自定义包,其源码位于 GOPATH/lab/test 目录下
 - import “database/sql/driver:driver 包的源码位于 GOROOT/src/database/sql/driver 目录下
 
引用格式
别名导入:
import F "fmt" // F 就是 fmt 包的别名
func main() {F.Println("666")
}
点导入,go-staticcheck 对于点导入会有警告:should not use dot imports (ST1001) go-staticcheck。点导入很少使用,这种方式很有可能导致导入的标识符发生冲突。import . "fmt"相当于把 fmt 包直接合并到当前程序中,在使用 fmt 包内的方法时可以不用加前缀 fmt,直接引用即可。
匿名导入:使用下划线作为别名,就意味着无法使用了,这种情况下,只能执行导入包内的 init 函数了,主要作用是做包的初始化用。
匿名导入的包与其他方式导入的包一样都会被编译到可执行文件中。使用标准格式引用包,但是代码中却没有使用包,编译器会报错。如果包中有 init 初始化函数,则通过 import _ " 包路径 " 这种方式引用包,仅执行包的初始化函数,即使包没有 init 初始化函数,也不会引发编译器报错。
go mod
Go Module 是 Go 语言从 1.11 版本之后官方推出的版本管理工具,并且从 Go 1.13 版本开始,Go Module 成为了 Go 语言默认的依赖管理工具。
使用 Go Module 就可以告别 GOPATH。使用 Go Module 管理项目,就不需要非得把项目放到 GOPATH src 目录下,可以在磁盘的任意位置新建项目。
使用 Go Module 之前需要设置环境变量:
- GO111MODULE=on
 - GOPROXY=https://goproxy.cn,direct