Go学习笔记

1. 简介

Go的优点

  • 编译速度快

  • 天生支持并发

    编写并发程序非常简便

  • 高效GC回收

  • runtime系统调度机制

  • 语法简洁

  • 面向对象语言

  • 目前大厂钟爱

Go的缺点

  • 包管理还不完善:大部分包私人托管在github上
  • 没有泛型
  • 争议:将所有Exception都用Error处理

Go适合做什么

  • 云计算基础设施:docker, k8s, cloudflare CDN
  • 基础后端软件: tidb, influxdb
  • 微服务
  • 互联网基础设施: 以太坊, hyperledger

2. 基础语法

hello world

package main

import "fmt"

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

特点

  • 行末可不加分号,编译器会自动加

    因此有一些换行要求,如:

    • 函数起始大括号 { 一定要和函数名同一行
  • 主程序包名为 main

类型

- bool
- 数值类型
	- int8, int16, int32, int64, int
	- uint8, uint16, uint32, uint64, uint
	- float32, float64
	- complex64, complex128
	- byte
	- rune
- string
  • 默认就用int,32位机器就是int32

  • complex是复数, complex64是实部虚部都是float32的复数,complex128都是float64

    // 几种初始化复数变量方法
    f := 10 + 5i
    f := complex(10, 5)
    
  • byte是uint8的别名

  • rune是int32的别名

类型转换

Go是强类型语言,不会自动转换类型。

比如: c = a+b 如果a和b的类型不同,就不能相加,而c中可以,int+float会转为float。

和c、java一样,通过 类型(变量)强制转换类型。int(b)

变量和常量

声明变量

如果变量未被赋值,Go 会自动地将其初始化,赋值该变量类型的零值

1. var a int // 使用var起始表示变量,先写变量名,再写类型
2. var b int = 100 // 给定初始化值,默认为0
3. var c = 100 // var可以根据初始化值推测类型
4. d := 100  // 使用 := 是最常用的,会根据值推断类型

方法4最常用,但是不支持声明全局变量

//多个变量

var {
    a int = 100
    b string = "abc"
}

var a, b int = 100, 200
var a, b = 100, "abc"

常量

const pi float32 = 3.14

const {
    A = 100
    B = 200
    C = 300
}

// 枚举
// iota会从 0 “逐行”累加,下面即 A=0,B=1,C=2
const {
    A = iota
    B
    C
}
// iota还可以进行一些运算
const {
    A = iota * 2
    B
    C
    D = iota + 2
    F
}
// 以上 A=0 B=2 C=4 D=5 F=6

打印

引入 fmt 包

fmt.Println("hello")
-----
var a int = 100
fmt.Println("a = ", a)
-----
var a int = 100
fmt.Ptintf("a = %d", a)
fmt.Ptintf("type of a is %T", a)  // %T打印变量类型 

函数

函数结构

// 无返回值 无形参
func funcA {
    ...
}
// 单返回值 单形参
func funcA(a int) int {
	return a + 1
}
//单返回值 多形参
func funcA(a int, b int) int {
    return a + b
}
// 多返回值
func funcA(a int, b int) (int, int) {
    return a+1, b*2
}
// 指定返回值名称
func funcA(a int, b int) (r1 int, r2 int) {
    r1 = a + 1
    r2 = b * 2
    return  // 之前赋值过了,可以直接return
}

init函数

go程序执行过程:

image-20211111215430818

每个包都可以有自己的init函数,不定义就没有。init函数用于执行一些资源初始化,不能显示调用,也没有参数和返回值。

defer

延迟函数defer。含有 defer 语句的函数,会在该函数将要返回之前,调用另一个函数。类似AOP的功能。通常用于释放资源,错误处理。

匿名函数

函数可以没有名字,匿名函数可以直接调用。

func main() {  
    func() {
        fmt.Println("hello world first class function")
    }()
}
func main() {  
    func(n string) {  // 还可以接收参数
        fmt.Println("Welcome", n)
    }("Gophers")
}

闭包

闭包是由函数及其相关引用环境组合而成的实体(即:闭包=函数+引用环境)。

当一个匿名函数所访问的变量定义在函数体的外部时,就产生了闭包:

func main(){
  a := 5
  func() {
    fmt.Println("a=",a)
  }()
}

匿名函数中并没有定义a,a是调用它的环境中定义的,它只是引用环境变量。

func main() {
    a := 5
    f1 := func() {
    	a++
    	fmt.Println("a=",a)
    }
    f1()
    f1()
}

闭包所引用的环境变量会在堆中新建一份,而不是使用栈中的那份。

func adda() func() {
	a := 5
	c := func() {
		a++
		fmt.Println("a=",a)
	}
	return c
}

func main() {
    f1 := adda()
    f2 := adda()
    f1()
    f2()  // f1、f2产生了闭包, 在堆中会生成属于该闭包的环境变量a
    adda()()  // 直接调用没产生闭包,a使用的依然是栈中的
    fmt.Println()
    f1()
    f2()
    adda()()
}

上述程序输出:

a= 6
a= 6
a= 6

a= 7
a= 7
a= 6  

头等函数

go是支持头等函数特性的语言。头等函数特性是指,函数可以赋值给变量,可以作为其他函数的参数,作为函数的返回值。这个特性的意思是 函数是类似类型一样的“头等公民”。

头等函数的特性使得函数式编程变得非常灵活。

看看将一个匿名函数赋值给变量,然后调用:

func main() {  
    a := func() {
        fmt.Println("hello world first class function")
    }
    a()
    fmt.Printf("%T", a)
}

函数也类似一种类型,我们也可以通过 type 定义自己的函数类型。

type add func(a int, b int) int  // add 是该函数类型的名称

func main() {  
  // 变量a是add类型,并赋值了一个符合add类型签名的函数
    var a add = func(a int, b int) int { 
        return a + b
    }
    s := a(5, 6)
    fmt.Println("Sum", s)
}

函数作为参数传递给其他函数:

func simple(a func(a, b int) int) {  
    fmt.Println(a(60, 7))
}

func main() {  
    f := func(a, b int) int {
        return a + b
    }
    simple(f)
}

函数作为返回值:

func simple() func(a, b int) int {  
    f := func(a, b int) int {
        return a + b
    }
    return f
}

func main() {  
    s := simple()
    fmt.Println(s(60, 7))
}

使用示例1:

// 过滤数据
type student struct {  
    firstName string
    lastName  string
    grade     string
    country   string
}

// 传入过滤函数就可以过滤
func filter(s []student, f func(student) bool) []student {  
    var r []student
    for _, v := range s {
        if f(v) == true {
            r = append(r, v)
        }
    }
    return r
}

func main() {  
    s1 := student{ "Naveen","Ramanathan", "A","India",}
    s2 := student{ "Samuel","Johnson","B","USA",}
    s := []student{s1, s2}
    f := filter(s, func(s student) bool { // 调用时传入过滤函数
        if s.grade == "B" {
            return true
        }
        return false
    })
    fmt.Println(f)
}

使用示例2:

// 将切片的每个元素扩大5倍
func iMap(s []int, f func(int) int) []int {  
    var r []int
    for _, v := range s {
        r = append(r, f(v))
    }
    return r
}
func main() {  
    a := []int{5, 6, 7, 8, 9}
    r := iMap(a, func(n int) int {
        return n * 5
    })
    fmt.Println(r)
}

Go程序执行的入口是main包下的main函数

一般来说,属于某一个包的源文件都应该放置于一个和包同名的文件夹里。

  • 空白标识符

    导入包却不使用在Go中非法,编译会报错,这是为了极致压缩编译时间。

    有时只需要某个包的init,而不用到它。可以这样做避免报错

    import _ packagename
    

    "_"是空白标识符,用于屏蔽一些错误,避免报错。

if-else

if a > 0 {
    ...
} else if a < 0{
    ...
} else {
    ...
}

if 还有另外一种形式,它包含一个 statement 可选语句部分,它在条件判断之前运行。

if a = 1; a > 0 {
    ...
} else {
    ...
}

注意 else 和 if 的结束大括号在同一行,不在同一行会报错,因为 go 中的分号是自动添加的,不在同一行if结束就会自动加上分号。

循环

和c一样,分号和逗号一样的用法,不过没有括号而已

for init; condition; post {
    
}
---
for i := 0; i < 10; i++ {
    ...
}
---
// 可以作while用
i := 0
for i < 10 {
    ...
    i++
}
---
// 死循环
for {
    ...
}

switch

	finger := 4
    switch finger {
    case 1:
        fmt.Println("Thumb")
    case 2:
        fmt.Println("Index")
    case 3:
        fmt.Println("Middle")
    case 4:
        fmt.Println("Ring")
    case 5:
        fmt.Println("Pinky")
    default:
        fmt.Println("incorrect finger number")

case 可以有多个匹配项,用逗号分隔。

3. 常用类型

数组

// 定义数组
var arr [10]int  
arr := [10]int
arr := [3]int{1,2,3} 
arr := [...]int{1,2,3,4,5}	// 使用...将自动计算数组长度

len(arr) // 获取数组长度

// 遍历
// range方法
for i, v := range arr {
  fmt.Printf(v)
}
// 如果不需要索引
for _, v := range arr {
  ...
}

go中数组的大小是类型的一部分,也就是说 int[5] 和 int[10] 是两种数据类型,在设置形参的时候可以指定数组大小,传入的数组就必须和指定的大小相等。这样使用起来会比较麻烦,所以go还有切片slice。

此外与java不同的是,go中的数组是值类型而不是引用类型。也就是说数组变量传递是拷贝一份数组,java中数组是引用对象,拷贝的是对象引用地址。

切片 slice

切片只是对数组的引用。

// 创建
a := [5]int{1,2,3,4,5}
var s []int = a[1:3]	//创建一个切片,引用数组a的a[1]-a[3]

var s []int{1,2,3} // 创建一个数组,并且返回一个对该数组引用的切片

var s []int = make([]int,3) //使用make创建切片,长度和容量为3

len(s) // 获取长度
cap(s) // 获取容量

切片有长度和容量两个属性。长度是切片包含的元素个数,容量是当前能容纳的元素个数,切片可以自动扩容。

  • 切片追加元素

    使用 append 可以追加新的元素,并且超过容量后会进行扩容,扩大2倍。扩容是将原来的元素复制到一个更大的数组中。

    // 追加元素
    s := []int{1,2,3}
    s = append(s, 4)
    
    // 追加一个切片 "..."
    s1 := []int{1,2,3}
    s2 := []int{4,5}
    s1 = append(s1, s2...)
    

map

make(map[type of key]type of value) 是创建 map 的语法。

map必须初始化后才能使用,未初始化之前它是零值 nil,通常使用make初始化。

map也是引用类型。

var mymap map[string]int	// 未初始化
mymap = make(map[string]int) // 初始化

// 或者
mymap := make(map[string]int)

// 或者创建时指定元素来初始化,那么也就不需要make
mymap := map[string]int {
  "a": 1,
  "b": 2,		// 注意最后的","也是必须的
}

map用起来和数组一样。

mymap := make(map[string]int)
// 给map添加元素
mymap["a"] = 1
mymap["c"] = 3、

// 获取元素
v := mymap["a"]
// 获取不存在的元素返回改类型的零值
v, has := map["a"]	// 第二个返回值是bool类型,map中存在该元素则为true

// 遍历
for k,v := range mymap {
  ...
}

// 删除元素
delete(mymap, "a")

string

go中的string是一个字节切片。(go中采用utf-8编码)

所以我们可以通过 [i] 访问每个字节。虽然utf-8大部分常用字符都是一个字节,不过也有很多字符编码成两个或三个字节。go中提供了 rune (意:符文)类型,rune代表一个代码点,无论多少个字节的字符都可以用一个rune表示。

// 可以将字符串转为rune类型,然后遍历
s := "hello 你好"
runes := []rune(s)  // go中这些转换还是很方便
for i,v := range runes{
  ...
}

// 或者直接使用字符串的range遍历,它是通过字符而非字节遍历
for i,v := range s{
  ...
}

与java一样,string也是不可修改的。

指针

和c一样,*T代表指向T类型的指针,使用&取变量地址。不一样的是go不支持指针运算,例如 p++ 非法。

指针的零值是 nil,引用变量的零值都是 nil,相当于 null 。

结构体

// 定义
type Person struct {
  name string
  age int
}

// 创建
p1 := Person{"Peng",24}
// 创建时若未初始化字段,则为字段类型的零值

相同类型的字段可以写在同一行,更加紧凑。

还可以使用匿名结构体,可以即时的创建一个匿名结构体变量来使用。

// 提前没有定义

p2 := struct {
  name string
  age int
}{
  "Peng",24
}
...

和其他语言一样,使用 . 访问结构体字段。

4. 面向对象

go中似乎是采用结构体取代类。

方法

go不是一个纯面向对象的语言,而且没有class,为了达到与class相似的效果,go新增了一个特性叫做“方法”。

type Person struct {
  name string
  age int
}

func (p Person) show() {
  fmt.Printf("%s is %d", p.name, p.age)
}

func main(){
  p1 := Person {"Peng", 24}
  p1.show()
}

如上,在show方法中 func 和 方法名之间加入了一个特殊的接收器类型,那么我们可以像调用对象的方法一样,这样写起来的代码就很有面向对象的感觉了。

加上了接收器,我们就可以称它为该接收器类型的一个方法,而不是普通的函数了。

相同名称的方法可以定义在不同类型上,而函数不允许重名。

结构体定义和方法定义必须在一个包中。

指针接收器

上述的接收器是值接收器,也就是说方法的调用者是将接收器复制一份传给方法,在方法内部对接收器字段的任何改变都不影响调用者结构。

有时候我们需要使用引用传递而非值传递,这就要使用指针接收器。

func (p *Person) changeAge(newAge int){
  p.age = newAge
}

如果是指针接收器,一般来说我们需要通过接收器指针来调用方法,如写成&p.changeAge(18),不过go种提供了语法糖让我们可以省略 &。

匿名字段的方法

type Person struct {
  name string
  age int
  Child   // Child 也是一个结构体,假设其有一个方法 play()
}

// 匿名字段的方法可以直接这样调用
p.play()

在非结构体上的方法

非结构体类型即是语言自带的类型,如int,string。

// 给 int 类型新增一个方法
func (a int) add(b int) {
}

func main() {
}

// 这样无法通过编译,因为int类型的定义和方法定义不在一个包中

可以这样解决:

type myInt int

func (a myInt) add(b myInt) myInt {
    return a + b
}

接口

go中也有接口,这样定义:

type TestInterface interface {
  // 接口方法签名
  // ...
}

go中的接口实现是隐式的,不需要 implement 这样的关键字,一个类型定义了接口中的所有方法就说明它实现了这个接口。

空接口

没有方法的接口,表示为 interface{},所有类都实现了空接口。因此可以使用空接口作为形参,表示函数可以接收任何类型。

func test(i interface{}) {
  ...
}

类型断言

i.(T),断言接口 i 的具体类型是不是 T。

func test(i interface{}) {
  v := i.(int)	// 若传入类型不是int,则会报错
}

// 不希望报错的用法
v, ok := i.(int)  // 如果不是int,则v赋值为int类型的零值,ok为fasle

断言还可以结合 switch 使用:

func findType(i interface{}) {  
    switch i.(type) {
    case string:
        fmt.Printf("I am a string and my value is %s\n", i.(string))
    case int:
        fmt.Printf("I am an int and my value is %d\n", i.(int))
    case MyInterface:
        fmt.Printf("I am an class, and I implement MyInterface interface)
    default:
        fmt.Printf("Unknown type\n")
    }
} // 这很有意思

嵌套接口

type SalaryCalculator interface {  
    DisplaySalary()
}

type LeaveCalculator interface {  
    CalculateLeavesLeft() int
}

type EmployeeOperations interface {  
    SalaryCalculator
    LeaveCalculator
}

结构体取代类

go中使用结构体来实现类的功能。面向对象语言中类是通过构造器新建的,go中也可以实现类似的做法。

如果你想创建一个类,那么单独把这个结构体放到一个包中。

如果将结构体的名称首字母小写,那么该结构体将不可引用,也就是说我们不能 p := person.Person {...} (person包中的Person结构体)这样来得到一个结构体变量。类似构造器,go中的习惯是在该结构体的包中定义一个名称为 New 的函数来模拟构造器。

package person

type person struct {
  name string
  age int
}

func New(name string, age int) person {
  p := person {name, age}
  return p
}
package main

import "person"

func main() {
  p := person.New("Peng", 24)  // 通过 New 新建“类对象”
}

组合取代继承

go使用组合来代替继承。结构体可以随意嵌套,这就是go的组合。

如果结构体A内嵌套了结构体B字段,那我们可以在A中直接访问B的字段,好像这些字段属于外部结构体一样。好像B就是A的子类。

多态

使用接口就可以实现多态。

5. 并发

- 并行不一定会加快运行速度,因为并行运行的组件之间可能需要相互通信。在我们浏览器的例子里,当文件下载完成后,应当对用户进行提醒,比如弹出一个窗口。于是,在负责下载的组件和负责渲染用户界面的组件之间,就产生了通信。在并发系统上,这种通信开销很小。但在多核的并行系统上,组件间的通信开销就很高了。所以,并行不一定会加快运行速度.

go 原生支持并发,使用Go 协程(Goroutine) 和信道(Channel)来处理并发。

go协程

Go 协程可以看作是轻量级线程。

  • 相比线程而言,Go 协程的成本极低。堆栈大小只有若干 kb,并且可以根据应用的需求进行增减。而线程必须指定堆栈的大小,其堆栈是固定不变的。

  • Go 协程会复用OS 线程。即使程序有数以千计的 Go 协程,也可能只有一个线程。如果该线程中的某一 Go 协程发生了阻塞(比如说等待用户输入),那么系统会再创建一个 OS 线程,并把其余 Go 协程都移动到这个新的 OS 线程。好消息是程序员不需要关心这些细节。

  • Go 协程使用信道来进行通信。信道用于防止多个协程访问共享内存时发生竞态条件(Race Condition)。

启动协程

在调用函数或者方法时,在前面加上 go,则会创建一个协程并发运行。

main 函数运行在一个特有的协程上,称为 Go 主协程。主协程结束,程序也即结束。

func hello() {  
    fmt.Println("Hello world goroutine")
}
func main() {  
    go hello()
    time.Sleep(1 * time.Second) // 等待上面协程运行
    fmt.Println("main function")
}

信道

类似linux中的管道,用于协程间通信。

无缓冲信道

信道 chan 创建需要关联一种类型,然后该信道就只能传输该类型数据。

// 创建信道
a := make(chan int)

// 读取信道
data := <- a
// 写入信道
a <- data
// 注意,读写默认都是阻塞的。

使用信道:

func hello(done chan bool) {
  ...
  done <- true
}
func main() {
  done := make(chan bool)
  go hello(done)
  <-done			// 没有接受值也是可以的
}

以上程序利用信道实现了通信,协程hello在运行完后向信道写入信号,主协程则阻塞等待直到信道信号的到来。

再看一个使用信道的例子:

// 程序目的是计算一个数中每一位的平方和与立方和。设置了两个协程,一个计算平方和,一个计算立方和,主协程计算它俩的和。
func calcSquares(number int, squareop chan int) {  
    sum := 0
    for number != 0 {
        digit := number % 10
        sum += digit * digit
        number /= 10
    }
    squareop <- sum
}

func calcCubes(number int, cubeop chan int) {  
    sum := 0 
    for number != 0 {
        digit := number % 10
        sum += digit * digit * digit
        number /= 10
    }
    cubeop <- sum
} 

func main() {  
    number := 589
    sqrch := make(chan int)
    cubech := make(chan int)
    go calcSquares(number, sqrch)
    go calcCubes(number, cubech)
    squares, cubes := <-sqrch, <-cubech
    fmt.Println("Final output", squares + cubes)
}

以上,可以看到可以通过信道将结果返回,非常高效方便。

关闭信道:

通常由发送方关闭信道,告知接收方不再发送数据。从信道接收数据时,可以多使用一个变量检查信道是否已经关闭:

v,ok := <- ch

遍历信道:

func producer(chnl chan int) {  
    for i := 0; i < 10; i++ {
        chnl <- i
    }
    close(chnl)
}
func main() {  
    ch := make(chan int)
    go producer(ch)
    for v := range ch {  // 循环会直到信道关闭才结束
        fmt.Println("Received ",v)
    }
}
  • 死锁

    上面说到无缓冲信道读写都是阻塞的,有写必须要有读。如果只有写入,没有协程读取,程序就会 出现死锁。

单向信道

上述所说都是双向信道,go还提供单向信道,即只能写或只能读。

通常我们不会直接创建一个单向信道,而是将双向信道转换为单向传入函数,这样在函数中使用信道就只能读或写,用于保护信道信息安全。

// 使用 chan<- 定义只能写入的信道, <-chan 定义只能读的信道
func sendData(sendch chan<- int) {  
    sendch <- 10
}

func main() {  
    cha1 := make(chan int)
    go sendData(cha1)
    fmt.Println(<-cha1)
}

缓冲信道

上述所说的无缓冲信道中不能缓冲数据,因此读写必须成对出现。此外还有缓冲信道,信道可以指定缓冲容量,无缓冲信道容量默认为0.

ch := make(chan type, capacity)

缓冲信道容量未满之前写入不会阻塞,数据未被及时读取导致容量满了仍然会阻塞。

WaitGroup

WaitGroup是一个结构体类型,用于等待一批Go协程执行结束,类似于java中的circlebarrier?

func process(i int, wg *sync.WaitGroup) {  
    fmt.Println("started Goroutine ", i)
    time.Sleep(2 * time.Second)
    fmt.Printf("Goroutine %d ended\n", i)
    wg.Done()
}

func main() {  
    no := 3
    var wg sync.WaitGroup
    for i := 0; i < no; i++ {
        wg.Add(1)
        go process(i, &wg)
    }
    wg.Wait()
    fmt.Println("All go routines finished executing")
}

waitgroup使用计数器工作,Add方法增加计数器,Done将计数器减一,Wait方法则阻塞直到计数器为0。

注意,将waitgroup作为参数传给协程时,应该传递地址,否则协程得到的是一个拷贝。

使用缓冲信道实现工作池

go中的worker pool其实也就是线程池,不过go中是协程。

看一个例子,这个例子中工作是计算一些随机数的每个位之和。

package main

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

type Job struct {  
    id       int
    randomno int
}
type Result struct {  
    job         Job
    sumofdigits int
}

var jobs = make(chan Job, 10)  
var results = make(chan Result, 10)

func digits(number int) int {  
    sum := 0
    no := number
    for no != 0 {
        digit := no % 10
        sum += digit
        no /= 10
    }
    time.Sleep(2 * time.Second)
    return sum
}
func worker(wg *sync.WaitGroup) {  
    for job := range jobs {
        output := Result{job, digits(job.randomno)}
        results <- output
    }
    wg.Done()
}
func createWorkerPool(noOfWorkers int) {  
    var wg sync.WaitGroup
    for i := 0; i < noOfWorkers; i++ {
        wg.Add(1)
        go worker(&wg)
    }
    wg.Wait()
    close(results)
}
func allocate(noOfJobs int) {  
    for i := 0; i < noOfJobs; i++ {
        randomno := rand.Intn(999)
        job := Job{i, randomno}
        jobs <- job
    }
    close(jobs)
}
func result(done chan bool) {  
    for result := range results {
        fmt.Printf("Job id %d, input random no %d , sum of digits %d\n", result.job.id, result.job.randomno, result.sumofdigits)
    }
    done <- true
}
func main() {  
    startTime := time.Now()
    noOfJobs := 100
    go allocate(noOfJobs)
    done := make(chan bool)
    go result(done)
    noOfWorkers := 10
    createWorkerPool(noOfWorkers)
    <-done
    endTime := time.Now()
    diff := endTime.Sub(startTime)
    fmt.Println("total time taken ", diff.Seconds(), "seconds")
}

select

用于在多个信道中做选择。select 会一直阻塞,直到发送/接收操作准备就绪,如果有多个操作同时准备好,那么会随机执行其中一个。语法类似switch。

func server1(ch chan string) {  
    time.Sleep(6 * time.Second)
    ch <- "from server1"
}
func server2(ch chan string) {  
    time.Sleep(3 * time.Second)
    ch <- "from server2"
}
func main() {  
    output1 := make(chan string)
    output2 := make(chan string)
    go server1(output1)
    go server2(output2)
    select {
    case s1 := <-output1:
        fmt.Println(s1)
    case s2 := <-output2:
        fmt.Println(s2)
    }
}

上述例子中,明显server2更快发送数据,因此select会执行case2。

select还可以有default,没有case满足时,执行default。

Mutex

就是锁。提供两个方法 Lock 和 Unlock。

看个使用的例子:

var x  = 0  
func increment(wg *sync.WaitGroup, m *sync.Mutex) {  
    m.Lock()
    x = x + 1
    m.Unlock()
    wg.Done()   
}
func main() {  
    var w sync.WaitGroup
    var m sync.Mutex
    for i := 0; i < 1000; i++ {
        w.Add(1)        
        go increment(&w, &m)
    }
    w.Wait()
    fmt.Println("final value of x", x)
}

其实使用缓冲信道也可以实现锁。

var x  = 0  
func increment(wg *sync.WaitGroup, ch chan bool) {  
    ch <- true
    x = x + 1
    <- ch
    wg.Done()   
}
func main() {  
    var w sync.WaitGroup
    ch := make(chan bool, 1)
    for i := 0; i < 1000; i++ {
        w.Add(1)        
        go increment(&w, ch)
    }
    w.Wait()
    fmt.Println("final value of x", x)
}

容量为1的缓冲信道,第一个协程向信道写入数据后,必须要执行完x=x+1,并将数据消耗掉,其他协程才能继续往信道中写入,否则在写入处阻塞。

6. 错误处理

go中所有的异常、错误统称为错误 error 。

// 打开文件
f, err := os.Open("/test.txt")
if err != nil {
  fmt.Println(err)
  return
}
fmt.Println(f.Name(), "opened successfully")

err 接收一个 error 类型,如果没有出错,err 则为 nil。

error 是一个接口类型:

type error interface {  
    Error() string
}

可能出错的类型都可以实现它。fmt.Println(err) 在打印错误时,会在内部调用 Error() string 方法来得到该错误的描述。

判断错误类型

看看常见的error使用方法:

  1. 断言

    看看 Open 函数返回的错误类型:

    type PathError struct {  
        Op   string
        Path string
        Err  error
    }
    
    func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }
    

    它对 error 进行了包装,是一个PathError。

    f, err := os.Open("/test.txt")
    // 可以通过断言看是不是PathError类型,然后就可以提取出错的路径
    if err, ok := err.(*os.PathError); ok {  
      fmt.Println("File at path", err.Path, "failed to open")
      return
    }
    
  2. 直接比较

    error可以直接用 == 比较。看个例子:

    filepath包中的Glob用于返回所有满足glob模式的文件名,如果模式格式不对,则会返回一个 ErrBadPattern 错误,该错误定义如下:

    var ErrBadPattern = errors.New("syntax error in pattern")
    
    // 使用的时候
    files, error := filepath.Glob("[")  // 此处模式提供格式错误
    if error != nil && error == filepath.ErrBadPattern {
      fmt.Println(error)
      return
    }
    

不能忽略错误

此前学过 _ 符号可以忽略参数,同样也可以用它忽略错误。在实际工程项目中不建议这样做,所有的错误都应该被及时发现及时处理。

自定义错误

有哪些方法创建自定义错误?

  1. 使用 errors.New

    使用New可以创建自定义错误,如上面的 ErrBadPattern 错误就是使用 New定义的。

  2. 使用 fmt 的 Errorf

    Errorf 能格式化输出错误以提供不止于字符串的错误信息。

    和 Printf 用法一样,不过它会返回一个 error 类型。

  3. 使用结构体类型

    和之前的PathError例子一样,我们可以对错误进行结构体包装。

Panic

上面所说的错误其实在java中都是异常,真正的错误是类似虚拟机故障,内存不足,死锁之类,通常这时程序已经无法恢复需要停止,go中把这些称为 panic (恐慌)。不过go中的panic可以使用recover重新获取对程序的控制。

panic用于系统错误,程序员使用时一般只需要 error 即可。

发生 panic

发生 panic 时, 函数会停止运行,但 defer 函数仍会被执行,然后将控制权返回函数调用方,直到当前协程的所有函数都退出。然后控制台会打印 panic 信息,接着打印堆栈跟踪信息,然后程序终止。

看一个例子:

func fullName(firstName *string, lastName *string) {  
    defer fmt.Println("deferred call in fullName")
    if firstName == nil {
        panic("runtime error: first name cannot be nil")
    }
    if lastName == nil {  // 产生panic
        panic("runtime error: last name cannot be nil")
    }
    fmt.Printf("%s %s\n", *firstName, *lastName)
    fmt.Println("returned normally from fullName")
}

func main() {  
    defer fmt.Println("deferred call in main")
    firstName := "Elon"
    fullName(&firstName, nil)  // 传入的lastname为nil
    fmt.Println("returned normally from main")
}
- 该程序会打印:

deferred call in fullName  
deferred call in main  
panic: runtime error: last name cannot be nil

goroutine 1 [running]:  
main.fullName(0x1042bf90, 0x0)  
    /tmp/sandbox060731990/main.go:13 +0x280
main.main()  
    /tmp/sandbox060731990/main.go:22 +0xc0

recover panic

通过在defer函数内部调用 recover,panic可以被恢复,恢复后的panic将继续执行剩下的程序。

依然是上面名字的例子:

func recoverName() {  
    if r := recover(); r!= nil {
        fmt.Println("recovered from ", r) // r 即 panic 的传参
    }
}
// 
func fullName(firstName *string, lastName *string) {  
    defer recoverName()
    ...
}

注意,recover 只能恢复同一个协程发出的 panic。

运行时 panic

类似 java 的运行时异常,比如数组越界访问,这种情况会产生 panic。这种panic同样可以恢复。

恢复后获得堆栈跟踪

panic恢复后即不会打印堆栈跟踪信息,如果仍然想获取这些信息,可以使用 Debug 包中的 PrintStack 函数。通常我们在defer函数中recover后调用。

7. 文件处理

文件操作完记得 close

读文件

读取整个文件

使用 ioutil 包中的 ReadFile 函数。

data, err := ioutil.ReadFile("test.txt")

如果 err 为 nil,则读取成功了,该函数返回的数据是一个字节切片。

import (
    "fmt"
    "io/ioutil"
)

func main() {
    data, err := ioutil.ReadFile("test.txt")
    if err != nil {
        fmt.Println("File reading error", err)
        return
    }
    fmt.Println("Contents of file:", string(data))
}

上述程序读取test.txt 文件,并打印。test.txt文件应该放在运行go install的目录下,否则将找不到文件报错。因此文件可以放在任意位置,取决于你在哪个地方编译程序。还可以使用下面三种方法解决路径问题。

  • 使用绝对路径
  • 使用命令行标记来传递文件路径
  • 将文件绑定在二进制文件中

分块读取文件

文件很大时可能需要分块读取,bufio 包提供了这样的功能。bufio的NewReader

逐行读取文件

bufio还提供了逐行读取的功能,bufio的NewScanner

写文件

写字符串

f, err := os.Create("test.txt")  // 创建文件

l, err := f.WriteString("Hello World") // 写入字符串

写字节

Write方法,可传入字节切片

posted @ 2021-11-19 20:24  Glaci  阅读(141)  评论(0编辑  收藏  举报