Go语言学习笔记

编程 · 02-20 · 147 人浏览

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会有更好的性能。

结构体

Go语言面向对象

函数

函数体包含局部变量和自由变量。

输出函数

操作系统换行符
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之前需要设置环境变量:

Go
Theme Jasmine by Kent Liao