【GO语言编程】(三)

方法

方法** 其实就是一个函数,在 func 这个关键字和方法名中间加入了一个特殊的接收器类型。接收器可以是结构体类型或者是非结构体类型。接收器是可以在方法的内部访问的。

GO
func (t Type) methodName(parameterList) returnList{
}

上面的代码片段创建了一个接收器类型为 Type 的方法 methodName

实例绑定

GO
package main

import "fmt"

// Lesson 定义一个名为 Lesson 的结构体
type Lesson struct {
	Name   string
	Target string
}

// PrintInfo 定义一个与 Lesson 的绑定的方法
func (lesson Lesson) PrintInfo() {
	fmt.Println("name:", lesson.Name)
	fmt.Println("target:", lesson.Target)
}

func main() {
	l := Lesson{
		Name:   "可爱电子羊",
		Target: "咖喱饭真好吃",
	}
	l.PrintInfo()
}

上面的程序中定义了一个与结构体 Lesson 绑定的方法 PrintInfo() ,其中 PrintInfo 是方法名, (lesson Lesson) 表示将此方法与 Lesson 的实例绑定,这里我们把 Lesson 称为方法的接收者,而 lesson 表示实例本身,相当于 Python 中的 self ,Java 中的 this

当然,你可以把上面程序的方法改成一个函数,如下:

GO
package main

import "fmt"

type Lesson struct {
    Name   string
    Target string
}

func PrintInfo(lesson Lesson) {
    fmt.Println("name:", lesson.Name)
    fmt.Println("target:", lesson.Target)
}

func main() {
    lesson := Lesson{
        Name: "可爱电子羊",
        Target: "咖喱饭真好吃",
    }
    PrintInfo(lesson)
}

在这里插入图片描述

运行这个程序,也同样会输出上面一样的答案,那么我们为什么还要用方法呢?因为在 Go 中,相同的名字的方法可以定义在不同的类型上,而相同名字的函数是不被允许的。如果你在上面这个程序添加一个同名函数,就会报错。但是在不同的结构体上面定义同名的方法就是可行的。

GO
package main

import "fmt"

type Lesson struct {
    Name   string
    Target string
}

func (lesson Lesson) PrintInfo() {
    fmt.Println("Lesson name:", lesson.Name)
    fmt.Println("Lesson target:", lesson.Target)
}

type Author struct {
    Name string
}

func (author Author) PrintInfo() {
    fmt.Println("author name:", author.name)
}

func main() {
    lesson := Lesson{
        Name: "电子羊想吃咖喱饭",
        Target: "咖喱饭呀咖喱饭",
    }
    lesson.PrintInfo()
    author := Author{"电子羊"}
    author.PrintInfo()
}

指针接收器与值接收器

值接收器和指针接收器之间的区别在于,在指针接收器的方法内部的改变对于调用者是可见的,然而值接收器的方法内部的改变对于调用者是不可见的,所以若要改变实例的属性时,必须使用指针作为方法的接收者。看看下面的例子就知道了:

GO
package main

import "fmt"

// Lesson 定义一个名为 Lesson 的结构体
type Lesson struct {
    Name string
    Target  string
    SpendTime int
}

// PrintInfo 定义一个与 Lesson 的绑定的方法
func (lesson Lesson) PrintInfo() {
    fmt.Println("name:", lesson.Name)
    fmt.Println("target:", lesson.Target)
    fmt.Println("spendTime:", lesson.SpendTime)
}

func (lesson Lesson) ChangeLessonName(name string) {
//    lesson.name = name
}

// AddSpendTime 定义一个与 Person 的绑定的方法,使 age 值加 n
func (lesson *Lesson) AddSpendTime(n int) {
    lesson.SpendTime = lesson.SpendTime + n
}

func main() {
    lesson := Lesson{
        Name: "电子羊想吃咖喱饭",
        Target: "咖喱饭呀咖喱饭",
        PrintTimes:  1,
    }
    fmt.Println("before change")
    lesson.PrintInfo()

    fmt.Println("after change")
    lesson.AddSpendTime(2)
    lesson.ChangeLessonName("印度长米搭配孜然羊肉的玛莎拉咖喱饭")
    lesson.PrintInfo()
}

在这里插入图片描述

在上面的程序中, AddSpendTime 使用指针接收器最终能改变实例的 SpendTime 值,然而使用值接收器的 ChangeLessonName 最终没有改变实例 Name 的值。

在方法中使用值接收器 与 在函数中使用值参数

当一个函数有一个值参数,它只能接受一个值参数。当一个方法有一个值接收器,它可以接受值接收器和指针接收器。

GO
package main

import "fmt"

type Lesson struct {
    Name string
    Target  string
    PrintTimes int
}

func (lesson Lesson) PrintInfo() {
    fmt.Println(lesson.Name)
}

func PrintInfo(lesson Lesson) {
    fmt.Println(lesson.name)
}

func main() {
    lesson := Lesson{"Go语言微服务核心架构22讲"}
    PrintInfo(lesson)
    lesson.PrintInfo()

    bPtr := &lesson
    //PrintInfo(bPtr) // error
    bPtr.PrintInfo()
}

在上面的程序中,使用值参数 PrintInfo(lesson) 来调用这个函数是合法的,使用值接收器来调用 lesson.PrintInfo() 也是合法的。

然后在程序中我们创建了一个指向 Lesson 的指针 bPtr ,通过使用指针接收器来调用 bPtr.PrintInfo() 是合法的,但使用值参数调用 PrintInfo(bPtr) 是非法的。

在非结构体上的方法

不仅可以在结构体类型上定义方法,也可以在非结构体类型上定义方法,但是有一个问题。为了在一个类型上定义一个方法,方法的接收器类型定义和方法的定义应该在同一个包中。例如:

GO
package main

import "fmt"

type myInt int

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

func main() {
    var x myInt = 50
    var y myInt = 7
    fmt.Println(x.add(y))   // 57
}

接口

在 Go 语言中, 接口 就是方法签名(Method Signature)的集合。在面向对象的领域里,接口定义一个对象的行为,接口只指定了对象应该做什么,至于如何实现这个行为,则由对象本身去确定。当一个类型实现了接口中的所有方法,我们称它实现了该接口。接口指定了一个类型应该具有的方法,并由该类型决定如何实现这些方法。

接口的定义

使用 type 关键字可以定义接口:

GO
type interface_name interface {
    method()
}

接口的实现

创建类型或者结构体,并为其绑定接口定义的方法,接收者为该类型或结构体,方法名为接口中定义的方法名,这样就说该类型或者结构体实现了该接口。例如:

GO
package main

import "fmt"

type Study interface {
    learn()
}

type Student struct {
    name string
}

func (s Student) learn() {
    fmt.Printf("%s 在读 %s", s.name, s.book)
}

func main() {
    student1 := Student{
        name: "张三",
        book: "《Go语言极简一本通》",
    }
    student1.learn()
}

上面的程序定义了一个名为 Study 的接口,接口中有未实现的方法 learn() ,这里还定义了名为 Student 的结构体,其绑定了方法 learn() ,也就隐式实现了 Study 接口,实现的内容是打印语句。

上面的例子使用了值接受者实现接口,下面的例子使用了指针接受者实现接口。

GO
package main

import "fmt"

type Study interface {
    learn()
}

...

type Worker struct {
    name string
    book string
    by   string
}

func (w *Worker) learn() {
    fmt.Printf("%s 在读 %s,通过方式 %s", w.name, w.book, w.by)
}

func main() {
    var s1 Study
    var s2 Study

    student2 := Student{
        name: "李四",
        book: "《Go语言极简一本通》",
    }
    s1 = student2
    s1.learn()

    student3 := Student{
        name: "王五",
        book: "Go语言微服务架构核心22讲",
    }
    s1 = &student3
    s1.learn()

    worker1 := Worker{
        name: "老王",
        book: "从0到Go语言微服务架构师",
        by:   "视频",
    }
    // s2 = worker1 // error
    s2 = &worker1
    s2.learn()
}

该程序定义了结构体 Student ,使用其作为值接受者实现 Study 接口。student2 的类型为 Studentstudent2 赋值给 s1 ,由于 Student 实现了接口变量 s1 所以会有输出。而接下来 s1 又被赋值为 &student3 ,同样有输出。接下来的结构体 Worker 使用指针接受者实现 Study 接口。worker1 的类型为 Workers2 被赋值为 &worker1 ,所以会有输出。但如果把 s2 赋值为 worker1 会报错,对于使用指针接受者的方法,用一个指针或者一个可取得地址的值来调用都是合法的。但接口中存储的具体值(Concrete Value)并不能取到地址,因此对于编译器无法自动获取 worker1 的地址,于是程序报错。

接口实现多态

使用接口可以实现多态,例如下面的程序,定义了名为 Study 的接口,接口中有方法 learn() 。程序中还定义了结构体 StudentWorker ,分别实现了 Study 接口,Student 的 learn name: "李四", book: "《Go语言极简一本通》" 而 Worker 的 learn 为 name: "张三",book: "从0到Go语言微服务架构师",by: "视频" ,利用的接口实现了不同的功能,这就是多态。

GO
package main

func main() {
    ...
    s2.learn()
    worker1.learn()
}

接口的内部表示

可以把接口的内部看做 (type, value)type 是接口底层的具体类型(Concrete Type),而 value 是具体类型的值。

GO
package main

import "fmt"

...

func ShowInterface(s Study) {
    fmt.Printf("接口类型: %T\n,接口值: %v\n", s, s)
}

func main() {
    var s Study
    s = student2
    ShowInterface(s)
    s.learn()
}

在上面的程序中,定义了 Study 接口,其中有 learn() 方法,结构体 Student 实现了该接口。使用 s = student2 语句我们把 student2 ( Student 类型)赋值给了 s ( Study 类型),现在打印出 Study 的具体类型为 Student ,而 student2 的值为 name: "李四", book: "《Go语言极简一本通》"

空接口

空接口 是特殊形式的接口类型,没有定义任何方法的接口就称为空接口,可以说所有类型都至少实现了空接口,空接口表示为 interface{} 。例如,我们之前的写过的空接口参数函数,可以接受任何类型的参数:

GO
package main

import "fmt"

func ShowType(i interface{}) {
    fmt.Printf("类型: %T, 值: %v\n", i, i)
}

func main() {
    str := "从0到Go语言微服务架构师"
    ShowType(str)
    num := 3.14
    ShowType(num)
}

上面的程序中我们定义了函数 ShowType 使用空接口作为参数,所以可以给这个函数传递任何类型的参数。

通过上面的例子不难发现接口都有两个属性,一个是值,而另一个是类型。对于空接口来说,这两个属性都为 nil

GO
package main

import "fmt"

func main() {
    var i interface{}
    fmt.Printf("Type: %T, Value: %v", i, i)
    // Type: <nil>, Value: <nil>
}

除了上面讲到的使用空接口作为函数参数的用法,空接口还有以下两种用法。

直接使用 interface{} 作为类型声明一个实例,这个实例就能承载任何类型的值:

GO
package main

import "fmt"

func main() {
    var i interface{}

    i = "从0到Go语言微服务架构师"
    fmt.Println(i) // Let's go

    i = 3.14
    fmt.Println(i) // 3.14
}

我们也可以定义一个接收任何类型的 arrayslicemapstrcut 。例如:

GO
package main

import "fmt"

func main() {
    x := make([]interface{}, 3)
    x[0] = "从0到Go语言微服务架构师"
    x[1] = 3.14
    x[2] = []int{1, 2, 3}
    for _, value := range x {
        fmt.Println(value)
    }
}

空接口可以承载任何值,但是空接口类型的对象是不能赋值给另一个固定类型对象的。

GO
package main

func main() {
    var num = 1
    var i interface{} = num
    var str string = i // error
}

当空接口承载数组和切片后,该对象无法再进行切片。

GO
package main

import "fmt"

func main() {
    var s = []int{1, 2, 3}

    var i interface{} = s

    var s2 = i[1:2] // error
    fmt.Println(s2)
}

类型断言

类型断言用于提取接口的底层值(Underlying Value)。使用 interface.(Type) 可以获取接口的底层值,其中接口 interface 的具体类型是 Type

GO
package main

import "fmt"

func assert(i interface{}) {
    value, ok := i.(int)
    fmt.Println(value, ok)
}

func main() {
    var x interface{} = 3
    assert(x)
    var y interface{} = "从0到Go语言微服务架构师"
    assert(y)
}

类型选择

类型选择用于将接口的具体类型与 case 语句所指定的类型进行比较。它其实就是一个 switch 语句,但在 switch 后面跟的是 i.(type) ,并且每个 case 后面跟的是类型。

GO
package main

import "fmt"

func getTypeValue(i interface{}) {
    switch i.(type) {
    case int:
        fmt.Printf("Type: int, Value: %d\n", i.(int))
    case string:
        fmt.Printf("Type: string, Value: %s\n", i.(string))
    default:
        fmt.Printf("Unknown type\n")
    }
}

func main() {
    getTypeValue(300)
    getTypeValue("从0到Go语言微服务架构师")
    getTypeValue(true)
}

实现多个接口

类型或者结构体可以实现多个接口,例如:

GO
package main

import "fmt"

...


type Happy interface {
	rest()
}

func (s Student) rest() {
    fmt.Printf("%s 放学了,出去玩...", s.name)
}

func (w *Worker) rest() {
    fmt.Printf("%s 下班了,吃大餐去...", w.name)
}

func main() {
    worker2 := Worker{
        name: "小明",
        book: "从0到Go语言微服务架构师",
        by:   "视频",
    }
    worker2.learn()
    worker2.rest()
}

接口的嵌套

虽然在 Go 中没有继承机制,但可以通过接口的嵌套实现类似功能。例如:

GO
package main

import "fmt"

...

type Life interface {
    Study
    Happy
}

func main() {
    worker2 := Worker{
        name: "小明",
        book: "从0到Go语言微服务架构师",
        by:   "视频",
    }
    worker2.learn()
    worker2.rest()
}

go 协程

Go 语言的 协程(Groutine) 是与其他函数或方法一起并发运行的工作方式。协程可以看作是轻量级线程。与线程相比,创建一个协程的成本很小。因此在 Go 应用中,常常会看到会有很多协程并发地运行。

启动一个 go 协程

调用函数或者方法时,如果在前面加上关键字 go ,就可以让一个新的 Go 协程并发地运行。

GO
// 定义一个函数
func functionName(parameterList) {
    code
}

// 执行一个函数
functionName(parameterList)

// 开启一个协程执行这个函数
go functionName(parameterList)

下面是启动一个协程的例子, go PrintInfo()PrintInfo() 函数与 main() 函数会并发执行,主函数运行在一个特殊的协程上,这个协程称之为 主协程(Main Goroutine)

启动一个新的协程时,协程的调用会立即返回。与函数不同,程序控制不会去等待 Go 协程执行完毕。在调用 Go 协程之后,程序控制会立即返回到代码的下一行,忽略该协程的任何返回值。如果 Go 主协程终止,则程序终止,于是其他 Go 协程也会终止。为了让新的协程能继续运行,我们在 main() 函数添加了 time.Sleep(1 * time.Second) 使主协程休眠 1 秒,但这种做法并不推荐,这里只是为了演示而添加。

GO
package main

import (
 "fmt"
 "time"
)

func PrintInfo() {
 fmt.Println("从0到Go语言微服务架构师")
}

func main() {
 // 开启一个协程执行 PrintInfo 函数
 go PrintInfo()
 // 使主协程休眠 1 秒
 time.Sleep(1 * time.Second)
 // 打印 main
 fmt.Println("main")
}

在这里插入图片描述

启动多个 Go 协程

通过下面的例子,可以观察到两个协程就如两个线程一样,并发执行:

GO
package main

import (
	"fmt"
	"time"
)

func PrintNum(num int) {
	for i := 0; i < 3; i++ {
		fmt.Println(num)
		// 避免观察不到并发效果 加个休眠
		time.Sleep(100 * time.Millisecond)
	}
}

func main() {
	// 开启 1 号协程
	go PrintNum(1)
	// 开启 2 号协程
	go PrintNum(2)
	// 使主协程休眠 1 秒
	time.Sleep(time.Second)
}

在这里插入图片描述

channel 通道

通道(channel) ,就是一个管道,可以想像成 Go 协程之间通信的管道。它是一种队列式的数据结构,遵循先入先出的规则。

通道的声明

每个通道都只能传递一种数据类型的数据,在你声明的时候,我们要指定通道的类型。chan Type 表示 Type 类型的通道。通道的零值为 nil

GO
var channel_name chan channel_types

下面的语句声明了一个类型为 string 的通道 nameChan ,该通道 nameChan 的值为 nil

GO
var ch chan string

通道的初始化

声明完通道后,通道的值为 nil ,我们不能直接使用,必须先使用 make 函数对通道进行初始化操作。

GO
ch = make(chan channel_type)

使用下面的语句我们可以对上面声明过的通道 ch 进行初始化:

GO
ch = make(chan string)

这样,我们就已经定义好了一个 string 类型的通道 nameChan 。当然,也可以使用简短声明语句一次性定义一个通道:

GO
ch := make(chan string)

使用通道发送和接收数据

往通道发送数据使用的是下面的语法:

GO
// 把 data 数据发送到 channel_name 通道中
// 即把 data 数据写入到 channel_name 通道中
channel_name <- data

从通道接收数据使用的是下面的语法:

GO
// 从 channel_name 通道中接收数据到 value
// 即从 channel_name 通道中读取数据到 value
value := <- channel_name

通道旁的箭头方向指定了是发送数据还是接收数据。箭头指向通道,代表数据写入到通道中;箭头往通道指向外,代表从通道读数据出去。

下面的例子演示了通道的使用:

package main

import (
	"fmt"
)

func PrintChan(c chan string) {
	// 往通道传入数据 "从0到Go语言微服务架构师"
	c <- "赤土之王与三朝圣者"
}

func main() {
	// 创建一个通道
	ch := make(chan string)

	fmt.Println("3.1版本更新")
	// 开启协程
	go PrintChan(ch)
	// 从通道接收数据
	rec := <-ch
	// 打印从通道接收到的数据
	fmt.Println(rec)
	// 打印 "学习目标:全面掌握Go语言微服务落地,代码级一次性解决微服务和分布式系统。"
	fmt.Println("虚空劫灰往事书")
}

在这里插入图片描述

该程序模拟了两个协程并发调用的场景,在 main 函数中,创建了一个通道,在 main 函数中先打印了 学习课程: ,然后开启协程运行 PrintChan 函数,而 main 函数通过协程接收数据,主协程发生了阻塞,等待通道 ch 发送的数据,在函数中,数据 从0到Go语言微服务架构师 传入通道中,当写入完成时,主协程接收了数据,解除了阻塞状态,打印出从通道接收到的数据 从0到Go语言微服务架构师 ,最后打印 `学习目标:全面掌握 Go 语言微服务落地,代码级一次性解决微服务和分布式系统。
Tips: 发送与接收默认是阻塞的

  • 从上面的例子我们知道,如果从通道接收数据没接收完主协程是不会继续执行下去的。当把数据发送到通道时,会在发送数据的语句处发生阻塞,直到有其它协程从通道读取到数据,才会解除阻塞。与此类似,当读取通道的数据时,如果没有其它的协程把数据写入到这个通道,那么读取过程就会一直阻塞着。

通道的关闭

对于一个已经使用完毕的通道,我们要将其进行关闭。

GO
close(channel_name)

这里要注意,对于一个已经关闭的通道如果再次关闭会导致报错,我们可以在接收数据时,判断通道是否已经关闭,从通道读取数据返回的第二个值表示通道是否没被关闭,如果已经关闭,返回值为 false ;如果还未关闭,返回值为 true

GO
value, ok := <- channel_name

通道的容量与长度

我们在前面讲过 make 函数是可以接收两个参数的,同理,创建通道可以传入第二个参数——容量。

  • 当容量为 0 时,说明通道中不能存放数据,在发送数据时,必须要求立马有人接收,否则会报错。此时的通道称之为无缓冲通道。
  • 当容量为 1 时,说明通道只能缓存一个数据,若通道中已有一个数据,此时再往里发送数据,会造成程序阻塞。利用这点可以利用通道来做锁。
  • 当容量大于 1 时,通道中可以存放多个数据,可以用于多个协程之间的通信管道,共享资源。

既然通道有容量和长度,那么我们可以通过 cap 函数和 len 函数获取通道的容量和长度。

GO
package main

import (
	"fmt"
)

func main() {
	// 创建一个通道
	c := make(chan int, 3)
	fmt.Println("初始化后:")
	fmt.Println("cap =", cap(c))
	fmt.Println("len =", len(c))
	c <- 1
	c <- 2
	fmt.Println("传入两个数后:")
	fmt.Println("cap =", cap(c))
	fmt.Println("len =", len(c))
	<- c
	fmt.Println("取出一个数后:")
	fmt.Println("cap =", cap(c))
	fmt.Println("len =", len(c))
}

在这里插入图片描述

缓冲通道与无缓冲通道

按照是否可缓冲数据可分为:缓冲通道无缓冲通道

无缓冲通道在通道里无法存储数据,接收端必须先于发送端准备好,以确保你发送完数据后,有人立马接收数据,否则发送端就会造成阻塞,原因很简单,通道中无法存储数据。也就是说发送端和接收端是同步运行的。

GO
c := make(chan int)
// 或者
c := make(chan int, 0)

缓冲通道允许通道里存储一个或多个数据,设置缓冲区后,发送端和接收端可以处于异步的状态。

GO
c := make(chan int, 3)

双向通道

到目前为止,上面定义的都是双向通道,既可以发送数据也可以接收数据。例如:

GO
package main

import (
	"fmt"
	"time"
)

func main() {
	// 创建一个通道
	c := make(chan int)

	// 发送数据
	go func() {
		fmt.Println("send: 1")
		c <- 1
	}()

	// 接收数据
	go func() {
		n := <- c
		fmt.Println("receive:", n)
	}()

	// 主协程休眠
	time.Sleep(time.Millisecond)
}

单向通道

单向通道只能发送或者接收数据。所以可以具体细分为只读通道和只写通道。

<-chan 表示只读通道:

GO
// 定义只读通道
c := make(chan string)
// 定义类型
type Receiver = <-chan string
var receiver Receiver = c

// 或者简单写成下面的形式
type Receiver = <-chan int
receiver := make(Receiver)

chan<- 表示只写通道:

GO
// 定义只写通道
c := make(chan int)
// 定义类型
type Sender = chan<- int
var sender Sender = c

// 或者简单写成下面的形式
type Sender = chan<- int
sender := make(Sender)

下面是一个例子:

GO
package main

import (
	"fmt"
	"time"
)

// Sender 只写通道类型
type Sender = chan<- string

// Receiver 只读通道类型
type Receiver = <-chan string

func main() {
	// 创建一个双向通道
	var ch = make(chan string)

	// 开启一个协程
	go func() {
		// 只能写通道
		var sender Sender = ch
		fmt.Println("即将学习:")
		sender <- "Go语言微服务架构核心22讲"
	}()

	// 开启一个协程
	go func() {
		// 只能读通道
		var receiver Receiver = ch
		message := <-receiver
		fmt.Println("开始学习: ", message)
	}()

	time.Sleep(time.Millisecond)
}

在这里插入图片描述

遍历通道

使用 for range 循环可以遍历通道,但在遍历时要确保通道是处于关闭状态,否则循环会被阻塞。

GO
package main

import (
   "fmt"
)

func loopPrint(c chan int) {
   for i := 0; i < 10; i++ {
      c <- i
   }
   // 记得要关闭通道
   // 否则主协程遍历完不会结束,而会阻塞
   close(c)
}

func main() {
   // 创建一个通道
   var ch2 = make(chan int, 5)
   go loopPrint(ch2)
   for v := range ch2 {
      fmt.Println(v)
   }
}

用通道做锁

上面讲过,当通道容量为 1 时,说明通道只能缓存一个数据,若通道中已有一个数据,此时再往里发送数据,会造成程序阻塞。例如:

GO
package main

import (
	"fmt"
	"time"
)

// 由于 x = x+1 不是原子操作
// 所以应避免多个协程对 x 进行操作
// 使用容量为 1 的通道可以达到锁的效果
func increment(ch chan bool, x *int) {
	ch <- true
	*x = *x + 1
	<- ch
}

func main() {
	ch3 := make(chan bool, 1)
	var x int
	for i := 0; i < 10000; i++ {
		go increment(ch3, &x)
	}
	time.Sleep(time.Millisecond)
	fmt.Println("x =", x)
}

死锁

讲完了锁,不得不提死锁。当协程给一个通道发送数据时,照理说会有其他 Go 协程来接收数据。如果没有的话,程序就会在运行时触发 panic ,形成死锁。同理,当有协程等着从一个通道接收数据时,我们期望其他的 Go 协程会向该通道写入数据,要不然程序也会触发 panic

GO
package main

func main() {
	ch := make(chan bool)
	ch <- true
}

运行上面的程序,会触发 panic ,报下面的错误:

GO
fatal error: all goroutines are asleep - deadlock!

下面再来看看几个例子。

GO
package main

import "fmt"

func main() {
	ch := make(chan bool)
	ch <- true
	fmt.Println(<-ch)
}

上面的代码你看起来可能觉得没啥问题,创建一个通道,往里面写入数据,再从里面读出数据,但运行后会报同样的错误:

GO
fatal error: all goroutines are asleep - deadlock!

那么为什么会出现死锁呢?前面的基础学的好的就不难想到使用 make 函数创建通道时默认不传递第二个参数,通道中不能存放数据,在发送数据时,必须要求立马有人接收,即该通道为无缓冲通道。所以在接收者没有准备好前,发送操作会被阻塞。

分析完引发异常的原因后,我们可以将代码修改如下,使用协程,将接收者代码放在另一个协程里:

GO
package main

import (
	"fmt"
	"time"
)

func funcRecieve(c chan bool) {
	fmt.Println(<-c)
}
func main() {
	ch4 := make(chan bool)
	go funcRecieve(ch4)
	ch4 <- true
	time.Sleep(time.Millisecond)
}

当然,还有一种更加直接的方法,把无缓冲通道改为缓冲通道就行了:

GO
package main

import "fmt"

func main() {
	ch5 := make(chan bool, 1)
	ch5 <- true
	fmt.Println(<-ch5)
}

有时候我们定义了通道的容量,但通道里的容量已经放不下新的数据,而没有接收者接收数据,就会造成阻塞,而对于一个协程来说就会造成死锁:

GO
package main

import "fmt"

func main() {
	ch6 := make(chan bool, 1)
	ch6 <- true
	ch6 <- false
	fmt.Println(<-ch6)
}

同理,当程序一直在等待从通道里读取数据,而此时并没有发送者会往通道中写入数据。此时程序就会陷入死循环,造成死锁。

WaitGroup

在实际开发中我们并不能保证每个协程执行的时间,如果需要等待多个协程,全部结束任务后,再执行某个业务逻辑。下面我们介绍处理这种情况的方式。

WaitGroup 有几个方法:

  • Add:初始值为 0 ,这里直接传入子协程的数量,你传入的值会往计数器上加。
  • Done:当某个子协程完成后,可调用此方法,会从计数器上减一,即子协程的数量减一,通常使用 defer 来调用。
  • Wait:阻塞当前协程,直到实例里的计数器归零。
使用信道

信道可以实现多个协程间的通信,于是乎我们可以定义一个信道,在任务执行完成后,往信道中写入 true ,然后在主协程中获取到 true ,就可以认为子协程已经执行完毕。

GO
package main

import "fmt"

func main() {
	isDone := make(chan bool)
	go func() {
		for i := 0; i < 5; i++{
			fmt.Println(i)
		}
		isDone <- true
	}()
	<- isDone
}

运行上面的程序,主协程就会等待创建的协程执行完毕后退出。

使用 WaitGroup

使用上面的信道方法,虽然可行,但在你程序中使用很多协程的话,你的代码就会看起来很复杂,这里就要介绍一种更好的方法,那就是使用 sync 包中提供的 WaitGroup 类型。WaitGroup 用于等待一批 Go 协程执行结束。程序控制会一直阻塞,直到这些协程全部执行完毕。当然 WaitGroup 也可以用于实现工作池。

WaitGroup 实例化后就能使用:

GO
var name sync.WaitGroup

下面看具体示例:

GO
package main

import (
	"fmt"
	"sync"
)

func task(taskNum int, wg *sync.WaitGroup) {
	// 延迟调用 执行完子协程计数器减一
	defer wg.Done()
	// 输出任务号
	for i := 0; i < 3; i++ {
		fmt.Printf("task %d: %d\n", taskNum, i)
	}
}

func main() {
	// 实例化 sync.WaitGroup
	var waitGroup sync.WaitGroup
	// 传入子协程的数量
	waitGroup.Add(3)
	// 开启一个子协程 协程 1 以及 实例 waitGroup
	go task(1, &waitGroup)
	// 开启一个子协程 协程 2 以及 实例 waitGroup
	go task(2, &waitGroup)
	// 开启一个子协程 协程 3 以及 实例 waitGroup
	go task(3, &waitGroup)
	// 实例 waitGroup 阻塞当前协程 等待所有子协程执行完
	waitGroup.Wait()
}

Select

select 语句用在多个发送/接收通道操作中进行选择。

  • select 语句会一直阻塞,直到发送/接收操作准备就绪。
  • 如果有多个通道操作准备完毕, select 会随机地选取其中之一执行。

select 语法如下:

GO
select {
    case expression1:
        code
    case expression2:
        code
    default:
        code
}

下面是使用 select-case 的一个简单例子:

package main

import "fmt"

func main() {
	// 创建3个通道
	ch1 := make(chan string, 1)
	ch2 := make(chan string, 1)
	ch3 := make(chan string, 1)
	// 往通道 1 千朵玫瑰带来的黎明
	ch1 <- "千朵玫瑰带来的黎明"
	// 往通道 2 发送数据 赤土之王3与三朝圣者
	ch2 <- "赤土之王3与三朝圣者"
	// 往通道 3 发送数据 虚空劫灰往事书
	ch3 <- "虚空劫灰往事书"

	select {
	// 如果从通道 1 收到数据
	case message1 := <-ch1:
		fmt.Println("ch1 received:", message1)
	// 如果从通道 2 收到数据
	case message2 := <-ch2:
		fmt.Println("ch2 received:", message2)
	// 如果从通道 3 收到数据
	case message3 := <-ch3:
		fmt.Println("ch3 received:", message3)
	// 默认输出
	default:
		fmt.Println("No data received.")
	}
}

在这里插入图片描述

上面的程序创建了 3 个通道,并在执行 select 语句之前往通道 1 、通道 2 和 通道 3 分别发送数据,在执行 select 语句时,如果有机会的话会运行所有表达式,只要其中一个通道接收到数据,那么就会执行对应的 case 代码,然后退出。所以运行该程序可能输出下面的语句:

SHELL
ch3 received: 虚空劫灰往事书

也有可能输出下面的这条语句,具体看哪个通道首先接收到数据:

SHELL
ch2 received: 赤土之王3与三朝圣者
ch1 received: 千朵玫瑰带来的黎明

select 的应用

每个任务执行的时间不同,使用 select 语句等待相应的通道发出响应。select 会选择首先响应先完成的 task,而忽略其它的响应。使用这种方法,我们可以做多个 task,并给用户返回最快的 task 结果。

下面的程序模拟了这种服务:

package main

import (
	"fmt"
	"time"
)

func task1(ch chan string) {
	time.Sleep(5 * time.Second)
	ch <- "正法炬书"
}

func task2(ch chan string) {
	time.Sleep(7 * time.Second)
	ch <- "水天供书"
}

func task3(ch chan string) {
	time.Sleep(2 * time.Second)
	ch <- "吉祥具书"
}

func main() {
	// 创建两个通道
	ch1 := make(chan string)
	ch2 := make(chan string)
	ch3 := make(chan string)
	go task1(ch1)
	go task2(ch2)
	go task3(ch3)

	select {
	// 如果从通道 1 收到数据
	case message1 := <-ch1:
		fmt.Println("ch1 received:", message1)
	// 如果从通道 2 收到数据
	case message2 := <-ch2:
		fmt.Println("ch2 received:", message2)
	// 如果从通道 3 收到数据
	case message3 := <-ch3:
		fmt.Println("ch3 received:", message3)
	}
}

在这里插入图片描述

当然,上面的程序会发现,没有 default 分支,因为如果加了该默认分支,如果还没从通道接收到数据, select 语句就会直接执行 default 分支然后退出,而不是被阻塞。

造成死锁

上面的例子引出了一个新的问题,那就是如果没有 default 分支, select 就会阻塞,如果一直没有命中其中的某个 case 最后会造成死锁。

GO
package main

import (
    "fmt"
)

func main() {
    // 创建两个通道
    ch1 := make(chan string, 1)
    ch2 := make(chan string, 1)
    ch3 := make(chan string, 1)

    select {
    // 如果从通道 1 收到数据
    case message1 := <-ch1:
        fmt.Println("ch1 received:", message1)
    // 如果从通道 2 收到数据
    case message2 := <-ch2:
        fmt.Println("ch2 received:", message2)
	// 如果从通道 3 收到数据
    case message3 := <-ch3:
        fmt.Println("ch3 received:", message3)
    }
}

运行上面的程序会造成死锁。解决该问题的方法是写好 default 分支。

当然还有另一种情况会导致死锁的发生,那就是使用空 select

GO
package main

func main() {
    select {}
}

运行上面的程序会抛出 panic

Tips:

  • 前面学习 switch-case 的时候,里面的 case 是顺序执行的,但在 select 里并不是顺序执行的。在上面的第一个例子就可以看出,当 select 由多个 case 准备就绪时,将会随机地选取其中之一去执行。

select 超时处理

case 里的通道始终没有接收到数据时,而且也没有 default 语句时, select 整体就会阻塞,但是有时我们并不希望 select 一直阻塞下去,这时候就可以手动设置一个超时时间。

GO
package main

import (
    "fmt"
    "time"
)

func makeTimeout(ch chan bool, t int) {
    time.Sleep(time.Second * time.Duration(t))
    ch <- true
}

func main() {
    c1 := make(chan string, 1)
    c2 := make(chan string, 1)
    c3 := make(chan string, 1)
    timeout := make(chan bool, 1)

    go makeTimeout(timeout, 2)

    select {
    case msg1 := <-c1:
        fmt.Println("c1 received: ", msg1)
    case msg2 := <-c2:
        fmt.Println("c2 received: ", msg2)
    case msg3 := <-c3:
        fmt.Println("c3 received: ", msg3)
    case <-timeout:
        fmt.Println("Timeout, exit.")
    }
}

读取/写入数据

select 里的 case 表达式只能对通道进行操作,不管你是往通道写入数据,还是从通道读出数据。

GO
package main

import (
    "fmt"
)

func main() {
    c1 := make(chan string, 2)

    c1 <- "千朵玫瑰带来的黎明"
    select {
    case c1 <- "捕风的异乡人":
        fmt.Println("c1 received: ", <-c1)
        fmt.Println("c1 received: ", <-c1)
    default:
        fmt.Println("channel blocking")
    }
}

在这里插入图片描述

线程同步

在 Go 语言中,经常会遇到并发的问题,当然我们会优先考虑使用通道,同时 Go 语言也给出了传统的解决方式 Mutex(互斥锁)RWMutex(读写锁) 来处理竞争条件。


package main
type Bank struct {
    balance int
}

func (b *Bank) Deposit(amount int) {
    b.balance += amount
}

func (b *Bank) Balance() int {
    return b.balance
}

func main() {
    b := &Bank{}

    b.Deposit(1000)
    b.Deposit(1000)
    b.Deposit(1000)

    fmt.Println(b.Balance())  //3000
}

3000

在这里插入图片描述

临界区

首先我们要理解并发编程中临界区的概念。当程序并发地运行时,多个 Go 协程不应该同时访问那些修改共享资源的代码。这些修改共享资源的代码称为临界区

GO
func main() {
    var wg sync.WaitGroup
    b := &Bank{}

    n := 1000
    wg.Add(n)
    for i := 1; i <= n; i++ {
        go func() {
            b.Deposit(1000)
            wg.Done()
        }()
    }
    wg.Wait()
    fmt.Println(b.Balance())  //972000,962000,941000
}

在这里插入图片描述

我们这里举一个简单的例子,当前变量的值增加 b.balance += amount

当然,对于只有一个协程的程序来说,上面的代码没有任何问题。但是,如果有多个协程并发运行时,就会发生错误,这种情况就称之为数据竞争(data race)。使用下面的互斥锁 Mutex 就能避免这种情况的发生。

互斥锁 Mutex

互斥锁(Mutex,mutual exclusion) 用于提供一种 加锁机制(Locking Mechanism) ,可确保在某时刻只有一个协程在临界区运行,以防止出现竞争。也是为了来保护一个资源不会因为并发操作而引起冲突导致数据不准确。

Mutex 有两个方法,分别是 Lock()Unlock() ,即对应的加锁和解锁。在 Lock()Unlock() 之间的代码,都只能由一个协程执行,就能避免竞争条件。

如果有一个协程已经持有了锁(Lock),当其他协程试图获得该锁时,这些协程会被阻塞,直到Mutex解除锁定。

下面使用一个例子来讲一讲互斥锁的使用 :

GO
package main

import (
    "fmt"
    "sync"
)

type BankV2 struct {
    balance int
    m       sync.Mutex
}

func (b *BankV2) Deposit(amount int) {
    b.m.Lock()
    b.balance += amount
    b.m.Unlock()
}

func (b *BankV2) Balance() int {
    return b.balance
}

func main() {
    var wg sync.WaitGroup
    b := &BankV2{}

    n := 1000
    wg.Add(n)
    for i := 1; i <= n; i++ {
        go func() {
            b.Deposit(1000)
            wg.Done()
        }()
    }
    wg.Wait()
    fmt.Println(b.Balance()) //1000000
}

为了解决竞争问题,我们就要对 Deposit 这个方法中加上互斥锁,使同一时刻,只能有一个协程对 balance 进行操作:

更改后的代码不管运行多少次,都只会输出一个结果,那就是 1000000

使用互斥锁很简单,但要注意同一协程里不要在尚未解锁时再次加锁,也不要对已经解锁的锁再次解锁。

当然,使用通道也可以处理竞争条件,把通道作为锁在前面讲通道的时候已经讲过,这里就不再赘述。

读写锁 RWMutex

sync.RWMutex 类型实现读写互斥锁,适用于读多写少的场景,它规定了当有人还在读取数据(即读锁占用)时,不允许有人更新这个数据(即写锁会阻塞);为了保证程序的效率,多个人(协程)读取数据(拥有读锁)时,互不影响不会造成阻塞,它不会像 Mutex 那样只允许有一个人(协程)读取同一个数据。读锁与读锁兼容,读锁与写锁互斥,写锁与写锁互斥。

  • 可以同时申请多个读锁;
  • 有读锁时申请写锁将阻塞,有写锁时申请读锁将阻塞;
  • 只要有写锁,后续申请读锁和写锁都将阻塞。

定义一个 RWMuteux 读写锁:

GO
var rwMutex sync.RWMutex

RWMutex 里提供了两种锁,每种锁分别对应两个方法,为了避免死锁,两个方法应成对出现,必要时请使用 defer

  • 读锁:调用 RLock 方法开启锁,调用 RUnlock 释放锁;
  • 写锁:调用 Lock 方法开启锁,调用 Unlock 释放锁。
GO
package main

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

type BankV3 struct {
    balance int
    rwMutex sync.RWMutex // read write lock
}

func (b *BankV3) Deposit(amount int) {
    b.rwMutex.Lock() // write lock
    b.balance += amount
    b.rwMutex.Unlock() // wirte unlock
}

func (b *BankV3) Balance() (balance int) {
    b.rwMutex.RLock() // read lock
    balance = b.balance
    b.rwMutex.RUnlock() // read unlock
    return
}

func main() {
    var wg sync.WaitGroup
    b := &BankV3{}

    n := 1000
    wg.Add(n)
    for i := 1; i <= n; i++ {
        go func() {
            b.Deposit(1000)
            wg.Done()
        }()
    }
    wg.Wait()
    fmt.Println(b.Balance())
}

在这里插入图片描述

条件变量 sync.Cond

Cond 实现了一个条件变量,在 Locker 的基础上增加的一个消息通知的功能,保存了一个通知列表,用来唤醒一个或所有因等待条件变量而阻塞的 Go 程,以此来实现多个 Go 程间的同步。

GO
type Cond struct {
    ...
    L Locker
    ...
}

// 创建一个带锁的条件变量,Locker 通常是一个 *Mutex 或 *RWMutex
func NewCond(l Locker) *Cond

// 唤醒所有因等待条件变量 c 阻塞的 goroutine
func (c *Cond) Broadcast()

// 唤醒一个因等待条件变量 c 阻塞的 goroutine
func (c *Cond) Signal()

// 等待 c.L 解锁并挂起 goroutine,在稍后恢复执行后,Wait 返回前锁定 c.L,
// 只有当被 Broadcast 和 Signal 唤醒,Wait 才能返回。
func (c *Cond) Wait()

注意:在调用 Signal,Broadcast 之前,应确保目标 Go 程进入 Wait 阻塞状态。

package main

import (
	"fmt"
	"os"
	"os/signal"
	"sync"
	"time"
)

func listen(name string, s []string, c *sync.Cond) {
	c.L.Lock()
	c.Wait()
	fmt.Println(name, " 大梦的曲调:", s)
	c.L.Unlock()
}

func broadcast(event string, c *sync.Cond) {
	time.Sleep(time.Second)
	c.L.Lock()
	fmt.Println(event)
	c.Broadcast()
	c.L.Unlock()
}

func main() {
	s1 := []string{"兰拉娜"}
	s2 := []string{"兰犍多"}
	s3 := []string{"兰荼茶"}
	var m sync.Mutex
	cond := sync.NewCond(&m)

	// listener 1
	go listen("林中奇遇", s1, cond)

	// listener 2
	go listen("原为一炊之梦", s2, cond)

	// listener 3
	go listen("为了所有的孩子们", s3, cond)

	// broadcast
	go broadcast("森林会记住一切:", cond)

	ch := make(chan os.Signal, 1)
	signal.Notify(ch, os.Interrupt)
	<-ch
}

在这里插入图片描述

错误与异常

错误

内建错误

在 Go 中, 错误 使用内建的 error 类型表示。error 类型是一个接口类型,它的定义如下:

GO
type error interface {
    Error() string
}

error 有了一个签名为 Error() string 的方法。所有实现该接口的类型都可以当作一个错误类型。Error() 方法给出了错误的描述。fmt.Println 在打印错误时,会在内部调用 Error() string 方法来得到该错误的描述。

下面的例子演示了程序尝试打开一个不存在的文件导致的报错:

GO
package main

import (
    "fmt"
    "os"
)

func main() {
    // 尝试打开文件
    file, err := os.Open("/a.txt")
    // 如果打开文件时发生错误 返回一个不等于 nil 的错误
    if err != nil {
        fmt.Println(err)
        return
    }
    // 如果打开文件成功 返回一个文件句柄 和 一个值为 nil 的错误
    fmt.Println(file.Name(), "opened successfully")
}

我们这里没有存在一个文件 a.txt ,所以尝试打开文件将会返回一个不等于 nil 的错误。

GO
open /a.txt: The system cannot find the file specified.
自定义错误

使用 errors 包中的 New 函数可以创建自定义错误。下面是 errors 包中 New 函数的实现代码:

GO
package errors

func New(text string) error {
    return &errorString{text}
}

type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

errorString 是一个结构体类型,只有一个字符串字段 s 。它使用了 errorString 指针接受者,来实现 error 接口的 Error() string 方法。New 函数有一个字符串参数,通过这个参数创建了 errorString 类型的变量,并返回了它的地址。于是它就创建并返回了一个新的错误。

下面是一个简单的自定义错误例子,该例子创建了一个计算矩形面积的函数,当矩形的长和宽两者有一个为负数时,就会返回一个错误:

GO
package main

import (
    "errors"
    "fmt"
)

func area(a, b int) (int, error) {
    if a < 0 || b < 0 {
        return 0, errors.New("计算错误, 长度或宽度,不能小于0.")
    }
    return a * b, nil
}
func main() {
    a := 100
    b := -10
    r, err := area(a, b)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println("Area =", r)
}

运行上面的程序会报出自定义的错误:

GO
计算错误, 长度或宽度,不能小于0.
给错误添加更多信息

上面的程序能报出我们自定义的错误,但是没有具体说明是哪个数据出了问题,所以下面就来改进一下这个程序,我们使用 fmt 包中的 Errorf 函数,规定错误格式,并返回一个符合该错误的字符串。

GO
package main

import (
    "fmt"
)

func area(a, b int) (int, error) {
    if a < 0 || b < 0 {
        return 0, fmt.Errorf("计算错误, 长度%d或宽度%d,不能小于0", a, b)
    }
    return a * b, nil
}
func main() {
    a := 100
    b := -10
    area, err := area(a, b)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println("Area =", area)
}

运行上面的程序,我们可以看到输出的错误中打印了长度和宽度的具体值:

GO
计算错误, 长度100或宽度-10,不能小于0

当然,给错误添加更多信息还可以 使用结构体类型和字段 实现。下面还是通过改进上面的程序来讲解这种方法的实现:

首先创建一个表示错误的结构体类型,一般错误类型名称都是以 Error 结尾,上面的错误是由于面积计算中长度或宽度错误导致的,所以这里把结构体命名为 areaError

GO
package main

import (
    "fmt"
)

type areaError struct {
    // 错误信息
    err string
    // 错误有关的长度
    length int
    // 错误有关的宽度
    width int
}

// 使用指针接收者 *areaError 实现了 error 接口的 Error() string 方法
func (e *areaError) Error() string {
    // 打印长度和宽度以及错误的描述
    return fmt.Sprintf("length %d, width %d : %s", e.length, e.width, e.err)
}

func rectangleArea(a, b int) (int, error) {
    if a < 0 || b < 0 {
        return 0, &areaError{"length or width is negative", a, b}
    }
    return a * b, nil
}
func main() {
    a := 100
    b := -10
    area, err := rectangleArea(a, b)
    // 检查了错误是否为 nil
    if err != nil {
        // 断言 *areaError 类型
        if err, ok := err.(*areaError); ok {
            // 如果错误是 *areaError 类型
            // 用 err.length 和 err.width 来获取错误的长度和宽度 打印出自定义错误的消息
            fmt.Printf("length %d or width %d is less than zero", err.length, err.width)
            return
        }
        fmt.Println(err)
        return
    }
    fmt.Println("Area =", area)
}

运行该程序输出如下:

GO
length 100 or width -10 is less than zero

当然,我们还可以使用 结构体类型的方法 来给错误添加更多信息。下面我们继续完善上面的程序,让程序更加精确的定位是长度引发的错误还是宽度引发的错误。

首先,我们还是跟上面一样创建一个表示错误的结构体:

GO
package main

import (
    "fmt"
)

type areaError struct {
    // 错误信息
    err string
    // 长度
    length int
    // 宽度
    width int
}

// 使用指针接收者 *areaError 实现了 error 接口的 Error() string 方法
func (e *areaError) Error() string {
    return e.err
}

// 长度为负数返回 true
func (e *areaError) lengthNegative() bool {
    return e.length < 0
}

// 宽度为负数返回 true
func (e *areaError) widthNegative() bool {
    return e.width < 0
}

func area(length, width int) (int, error) {
    err := ""
    if length < 0 {
        err += "length is less than zero"
    }
    if width < 0 {
        if err == "" {
            err = "width is less than zero"
        } else {
            err += " and width is less than zero"
        }
    }
    if err != "" {
        return 0, &areaError{err, length, width}
    }
    return length * width, nil
}

func main() {
    length := 100
    width := -10
    area, err := area(length, width)
    // 检查了错误是否为 nil
    if err != nil {
        // 断言 *areaError 类型
        if err, ok := err.(*areaError); ok {
            // 如果错误是 *areaError 类型
            // 如果长度为负数 打印错误长度具体值
            if err.lengthNegative() {
                fmt.Printf("error: 长度 %d 小于0\n", err.length)
            }
            // 如果宽度为负数 打印错误宽度具体值
            if err.widthNegative() {
                fmt.Printf("error: 宽度 %d 小于0\n", err.width)
            }
            return
        }
        fmt.Println(err)
        return
    }
    fmt.Println("Area =", area)
}

还是使用之前的例子中的参数,但我们这次报错结果更加具体,运行该程序输出如下:

GO
error: width -10 is less than zero

异常

错误和异常是两个不同的概念,非常容易混淆。错误指的是可能出现问题的地方出现了问题;而异常指的是不应该出现问题的地方出现了问题。

panic

在有些情况,当程序发生异常时,无法继续运行。在这种情况下,我们会使用 panic 来终止程序。当函数发生 panic 时,它会终止运行,在执行完所有的延迟函数后,程序返回到该函数的调用方。这样的过程会一直持续下去,直到当前协程的所有函数都返回退出,然后程序会打印出 panic 信息,接着打印出堆栈跟踪,最后程序终止。

我们应该尽可能地使用错误,而不是使用 panicrecover 。只有当程序不能继续运行的时候,才应该使用 panicrecover 机制。

panic 有两个合理的用例:

  • 发生了一个不能恢复的错误,此时程序不能继续运行。一个例子就是 web 服务器无法绑定所要求的端口。在这种情况下,就应该使用 panic ,因为如果不能绑定端口,啥也做不了。
  • 发生了一个编程上的错误。假如我们有一个接收指针参数的方法,而其他人使用 nil 作为参数调用了它。在这种情况下,我们可以使用 panic ,因为这是一个编程错误:用 nil 参数调用了一个只能接收合法指针的方法。
触发 panic

下面是内建函数 panic 的签名:

GO
func panic(v interface{})

当程序终止时,会打印传入 panic 的参数。

GO
package main

func main() {
    panic("panic error")
}

运行上面的程序,会打印出传入 panic 函数的信息,并打印出堆栈跟踪:

GO
panic: panic error
发生 panic 时的 defer

上面已经提到了,当函数发生 panic 时,它会终止运行,在执行完所有的延迟函数后,程序返回到该函数的调用方。这样的过程会一直持续下去,直到当前协程的所有函数都返回退出,然后程序会打印出 panic 信息,接着打印出堆栈跟踪,最后程序终止。下面通过一个简单的例子看看是不是这样:

GO
package main

import "fmt"

func myTest() {
    defer fmt.Println("defer myTest")
    panic("panic myTest")
}
func main() {
    defer fmt.Println("defer main")
    myTest()
}

运行该程序后输出如下:

GO
defer myTest
defer main
panic: panic myTest
recover

recover 是一个内建函数,用于重新获得 panic 协程的控制。下面是内建函数 recover 的签名:

GO
func recover() interface{}

recover 必须在 defer 函数中才能生效,在其他作用域下,它是不工作的。在延迟函数内调用 recover ,可以取到 panic 的错误信息,并且停止 panic 续发事件,程序运行恢复正常。下面是网上找的一个例子:

GO
package main

import "fmt"

func outOfArray(x int) {
    defer func() {
        // recover() 可以将捕获到的 panic 信息打印
        if err := recover(); err != nil {
            fmt.Println(err)
        }
    }()
    var array [5]int
    array[x] = 1
}
func main() {
    // 故意制造数组越界 触发 panic
    outOfArray(20)
    // 如果能执行到这句 说明 panic 被捕获了
    // 后续的程序能继续运行
    fmt.Println("main...")
}

虽然该程序触发了 panic ,但由于我们使用了 recover() 捕获了 panic 异常,并输出 panic 信息,即使 panic 会导致整个程序退出,但在退出前,有 defer 延迟函数,还是得执行完 defer 。然后程序还会继续执行下去:

GO
runtime error: index out of range [20] with length 5
main...

这里要注意一点,只有在相同的协程中调用 recover 才管用, recover 不能恢复一个不同协程的 panic

make 和 new

内置函数 new 分配内存。该函数只接受一个参数,该参数是一个任意类型(包括自定义类型),而不是值,返回指向该类型新分配零值的指针。

GO
// The new built-in function allocates memory. The first argument is a type,
// not a value, and the value returned is a pointer to a newly
// allocated zero value of that type.
func new(Type) *Type

使用 new 函数首先会分配内存,并设置类型零值,最后返回指向该类型新分配零值的指针。

GO
package main

import (
	"fmt"
)

func main() {
	num := new(int)
	// 打印出类型的值
	fmt.Println(*num)  // 0
}

make 函数

内置函数 make 只能分配和初始化类型为 slicemapchan 的对象。与 new 一样,第一个参数是类型,而不是值。与 new 不同, make 的返回类型与其参数的类型相同,而不是指向它的指针。结果取决于类型:

  • slice:size 指定长度。切片的容量等于其长度。可提供第三个参数以指定不同的容量;它不能小于长度。
  • map:为空映射分配足够的空间来容纳指定数量的元素。可以省略大小,在这种情况下,分配一个小的起始大小。
  • chan:使用指定的缓冲区容量初始化通道的缓冲区。如果为零,或者忽略了大小,则通道是无缓冲的。
GO
func make(t Type, size ...IntegerType) Type

注意,使用 make 函数必须初始化。例如:

GO
// slice
a := make([]int, 2, 10)

// map
b := make(map[string]int)

// chan
c := make(chan int, 10)

new 和 make 的区别

new:为所有的类型分配内存,并初始化为零值,返回指针。

make:只能为 slicemapchan 分配内存,并初始化,返回的是类型。

头等函数

Go 语言拥有 头等函数(First-class Function) ,头等函数是指函数可以被当作变量一样使用,即函数可以被当作参数传递给其他函数,可以作为另一个函数的返回值,还可以被赋值给一个变量。

把函数赋值给变量

下面是一个把函数赋值给变量的例子,该函数没有名称,调用该函数的唯一方法就是使用赋值后的变量。

GO
package main

import "fmt"

func main() {
    bookFunc := func() {
        fmt.Println("《森林书》")
    }
    bookFunc()
    fmt.Printf("bookFunc 的类型是 %T\n", bookFunc)
}

运行该程序输出如下:
在这里插入图片描述


《森林书》
bookFunc 的类型是 func()

传递一个函数作为参数

我们把 接收一个或多个函数作为参数 或者 返回值也是一个函数 的函数称为 高阶函数(Hiher-order Function)

下面的是把函数作为参数,并传递给其他函数的例子:

GO
package main

import "fmt"

// printRes 接收一个函数参数
func printRes(show func(author, book string) string) {
    fmt.Println(show("电子羊", "《千朵玫瑰带来的黎明》"))
}

func main() {
    // 创建匿名函数
    f := func(x, y string) string {
       return x + y
    }
    // 把匿名函数作为参数传入另一个函数
    printRes(f)
}

在这里插入图片描述

返回一个函数

下面的是函数返回一个函数的例子:

GO
package main

import "fmt"

// show 返回一个函数
func show() func(author, book string) string {
    return func(x, y string) string {
        return x + y
    }
}

func main() {
    // 变量获取返回的函数
    s := show()
    // 调用返回的函数
    fmt.Println(s("电子羊", "《为了没有眼泪的明天》"))
}

在这里插入图片描述

闭包

闭包(Closure) 是匿名函数的一个特例。当一个匿名函数所访问的变量定义在函数体的外部时,就称这样的匿名函数为闭包。

GO
package main

import "fmt"

func main() {
    x := 100
    func() {
        fmt.Println(x)
    }()
}

静态类型与动态类型

静态类型(static type)

静态类型就是变量声明时候的类型。例如:

GO
// int 是静态类型
var number int
// string 也是静态类型
var name string

动态类型(concrete type)

动态类型是程序运行时系统才能看见的类型。例如:

GO
// in 的静态类型为 interface{}
var in interface{}
// in 的静态类型为 interface{} 动态类型为 int
in = 100
// in 的静态类型为 interface{} 动态类型为 string
in = "《千朵玫瑰带来的黎明》"

通过上面的例子,可以看到我们定义了一个空接口 in ,它的静态类型永远是 interface{} ,但它可以接受任何类型,接受整型数据时,它的动态类型就为 int ;接受字符串型数据时,它的动态类型就变为 string

接口组成

每个接口变量实际上都是由一 pair 对组成,其中记录了实际变量的值和类型。例如:

GO
var number int = 100

这里声明了一个类型为 int 的变量,变量名叫 number 值为 100 。知道了接口的组成,我们也可以使用下面的方式定义一个变量:

GO
package main

import "fmt"

func main() {
    number := (int)(100)
    // 或者写成 number := (interface{})(100)
    fmt.Printf("number type: %T, data: %v", number, number)
}

运行上面的程序输出如下:

SHELL
number type: int, data: 100

在这里插入图片描述

反射

reflect 包

Go 语言提供了一种机制,能够在运行时更新变量和检查它们的值、调用它们的方法,而不需要在编译时就知道这些变量的具体类型。这种机制被称为 反射

反射是把双刃剑,功能强大但代码可读性并不理想,若非必要并不推荐使用反射。

在 Go 中 reflect 包实现了运行时反射。reflect 包会帮助识别 interface{} 变量的底层具体类型和具体值。

reflect.Type

reflect.Type 表示 interface{} 的具体类型。reflect.TypeOf() 方法返回 reflect.Type

像我们之前讲过的空接口参数的函数,可以通过类型断言来判断传入变量的类型,也可以借助反射来确定传入变量的类型。

GO
package main

import (
    "fmt"
    "reflect"
)

func reflectType(x interface{}) {
    obj := reflect.TypeOf(x)
    fmt.Println(obj)
}

func main() {
    var a int64 = 123
    reflectType(a)
    var b string = "金色的那菈!"
    reflectType(b)
}

在这里插入图片描述

reflect.Value

reflect.Value 表示 interface{} 的具体值。reflect.ValueOf() 方法返回 reflect.Value

GO
package main

import (
    "fmt"
    "reflect"
)

func reflectType(x interface{}) {
    typeX := reflect.TypeOf(x)
    valueX := reflect.ValueOf(x)
    fmt.Println(typeX)
    fmt.Println(valueX)
}

func main() {
    var a int64 = 123
    reflectType(a)
    var b string = "为了果实、种子还有树"
    reflectType(b)
}

在这里插入图片描述

relfect.Kind

relfect.Kind 表示的是种类。在使用反射时,需要理解类型(Type)和种类(Kind)的区别。编程中,使用最多的是类型,但在反射中,当需要区分一个大品种的类型时,就会用到种类(Kind)。

Go 语言程序中的类型(Type)指的是系统原生数据类型,如 intstringboolfloat32 等类型,以及使用 type 关键字定义的类型,这些类型的名称就是其类型本身的名称。例如使用 type A struct{} 定义结构体时,A 就是 struct{} 的类型。

种类(Kind)指的是对象归属的品种,在 reflect 包中有如下定义:

GO
// 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
)

通过下面这个程序,相信你会很容易明白这两者的区别:

GO
package main

import (
    "fmt"
    "reflect"
)

func reflectType(x interface{}) {
    typeX := reflect.TypeOf(x)
    fmt.Println(typeX.Kind()) // struct
    fmt.Println(typeX)        // main.book
}

type book struct {
}

func main() {
    var b book
    reflectType(b)
}
relfect.NumField()

relfect.NumField() 方法返回结构体中字段的数量。

GO
package main

import (
    "fmt"
    "reflect"
)

func reflectNumField(x interface{}) {
    // 检查 x 的类别是 struct
    if reflect.ValueOf(x).Kind() == reflect.Struct {
        v := reflect.ValueOf(x)
        fmt.Println("Number of fields", v.NumField())
    }
}

type book struct {
    name string
    spend  int
}

func main() {
    var b book
    reflectNumField(b)
}

relfect.Field()

relfect.Field(i int) 方法返回字段 ireflect.Value

GO
package main

import (
    "fmt"
    "reflect"
)

func reflectNumField(x interface{}) {
    // 检查 x 的类别是 struct
    if reflect.ValueOf(x).Kind() == reflect.Struct {
        v := reflect.ValueOf(x)
        fmt.Println("Number of fields", v.NumField())
        for i := 0; i < v.NumField(); i++ {
            fmt.Printf("Field:%d type:%T value:%v\n", i, v.Field(i), v.Field(i))
        }
    }
}

type book struct {
    name string
    spend  int
}

func main() {
    var b = book{"《为了不再哭泣的孩子们》", 8}
    reflectNumField(a)
}

反射的三大定律

之前在 静态类型与动态类型章节中讲过,一个接口变量,实际上都是由一 pair 对(type 和 data)组合而成,pair 对中记录着实际变量的值和类型。也就是说在真实世界(反射前环境)里,type 和 value 是合并在一起组成接口变量的。

而在反射的世界(反射后的环境)里,type 和 data 却是分开的,他们分别由 reflect.Typereflect.Value 来表现。

Go 语言里有反射三定律,是你在学习反射时,很重要的参考:

  1. Reflection goes from interface value to reflection object.
  2. Reflection goes from reflection object to interface value.
  3. To modify a reflection object, the value must be settable.

接下来我们就来讲一讲反射三大定律。

反射第一定律

Reflection goes from interface value to reflection object.

反射第一定律:反射可以将“接口类型变量”转换为“反射类型对象”。

这里反射类型指 reflect.Typereflect.Value

通过之前我们讲过的 reflect.TypeOf() 方法和 reflect.ValueOf() 方法可以分别获得接口值的类型和接口值的值。这两个方法返回的对象,我们称之为反射对象。

GO
package main

import (
    "fmt"
    "reflect"
)

func main() {
    var a interface{} = 3.14
    fmt.Printf("接口变量的类型为 %T ,值为 %v\n", a, a)
    t := reflect.TypeOf(a)
    v := reflect.ValueOf(a)
    fmt.Printf("从接口变量到反射对象:Type对象类型为 %T\n", t)
    fmt.Printf("从接口变量到反射对象:Value对象类型为 %T\n", v)
}

在这里插入图片描述

可以看到,使用 reflect.TypeOf()reflect.ValueOf() 方法完成了从接口类型变量到反射对象的转换。在这里说接口类型是因为 TypeOfValueOf 两个函数接收的是 interface{} 空接口类型, Go 语言函数都是值传递,会将类型隐式转换成接口类型。

反射第二定律

Reflection goes from reflection object to interface value.

反射第二定律:反射可以将“反射类型对象”转换为“接口类型变量”

第二定律刚好和第一定律相反,第一定律讲的是从接口变量到反射对象的转换,而第二定律讲的是从反射对象到接口变量的转换。

一个 reflect.Value 类型的变量,我们可以使用 Interface 方法恢复其接口类型的值。事实上,这个方法会把 typevalue 信息打包并填充到一个接口变量中,然后返回。

其函数声明如下:

GO
// Interface returns v's current value as an interface{}.
// It is equivalent to:
//    var i interface{} = (v's underlying value)
// It panics if the Value was obtained by accessing
// unexported struct fields.
func (v Value) Interface() (i interface{}) {
    return valueInterface(v, true)
}

最后转换后的对象静态类型为 interface{},我们可以使用类型断言转换为原始类型。

GO
package main

import (
    "fmt"
    "reflect"
)

func main() {
    var a interface{} = 3.14

    fmt.Printf("接口变量的类型为 %T ,值为 %v\n", a, a)

    t := reflect.TypeOf(a)
    v := reflect.ValueOf(a)

    // 反射第一定律
    fmt.Printf("从接口变量到反射对象:Type对象类型为 %T\n", t)
    fmt.Printf("从接口变量到反射对象:Value对象类型为 %T\n", v)

    // 反射第二定律
    i := v.Interface()
    fmt.Printf("从反射对象到接口变量:对象类型为 %T,值为 %v\n", i, i)
    // 使用类型断言进行转换
    x := v.Interface().(float64)
    fmt.Printf("x 类型为 %T,值为 %v\n", x, x)
}

在这里插入图片描述

反射第三定律

To modify a reflection object, the value must be settable.

反射第三定律:如果要修改“反射类型对象”其值必须是“可写的”

我们首先来看一看下面这段代码:

GO
package main

import "reflect"

func main() {
    var a float64 = 3.14
    v := reflect.ValueOf(a)
    v.SetFloat(2.1)
}

运行该代码段将会抛出异常:

GO
panic: reflect: reflect.Value.SetFloat using unaddressable value

这里你可能会疑惑,为什么这里会抛出寻址的异常,其实是因为这里的变量 v 是“不可写的”。settable(“可写性”)是反射类型变量的一个属性,但也不是说所有的反射类型变量都有这个属性。

要想知道一个 reflect.Value 类型变量的“可写性”,我们可以使用 CanSet 方法来进行检查:

GO
package main

import (
    "fmt"
    "reflect"
)

func main() {
    var a float64 = 3.14
    v := reflect.ValueOf(a)
    fmt.Println("是否可写:", v.CanSet())
}

可以看到,我们这个变量 v 是不可写的。对于一个不可写的变量,使用 Set 方法会报错。这里实质上还是 Go 语言里的函数都是值传递问题,想象一下这里传递给 reflect.ValueOf 函数的是变量 a 的一个拷贝,而非 a 本身,所以如果对反射对象进行更新,其原始变量 a 根本不会受到影响,所以是不合法的,“可写性”就是为了避免这个问题而设计出来的。

所以,要让反射对象具备“可写性”,一定要注意创建反射对象时要传入变量的指针,于是乎我们修改代码如下:

GO
package main

import (
    "fmt"
    "reflect"
)

func main() {
    var a float64 = 3.14
    v := reflect.ValueOf(&a)
    fmt.Println("是否可写:", v.CanSet())
}

但运行该程序还是会输出不可写,因为事实上我们这里要修改的是该指针指向的数据,使用还要使用 Value 类型的 Elem() 方法,对指针进行“解引用”,该方法返回指针指向的数据。

GO
package main

import (
    "fmt"
    "reflect"
)

func main() {
    var a float64 = 3.14
    v := reflect.ValueOf(&a).Elem()
    fmt.Println("是否可写:", v.CanSet())

    v.SetFloat(2)
    fmt.Println(v)
}

结构体里的 Tag 标签

在之前结构体的章节里我们讲过结构体的使用,一般情况下,我们定义结构体每个字段都是由字段名字以及字段的类型构成,例如:

GO
type Book struct {
    Name   string
    Target string
    Spend  int
}

Tag 的使用

但这一章要讲的是在字段上增加一个属性,这个属性是用反引号括起来的一个字符串,我们称之为 Tag(标签) 。例如:

GO
type Person struct {
    Name   string `json:"name"`
    Target string `json:"target"`
    Spend  int    `json:"spend,omitempty"`
}

结构体的 Tag 可以是任意的字符串面值,但是通常是一系列用空格分隔的 key:"value" 键值对序列;因为值中含有双引号字符,因此成员 Tag 一般用原生字符串面值的形式书写。一般我们常用在 JSON 的数据处理方面。

json 开头键名对应的值用于控制 encoding/json 包的编码和解码的行为,并且 encoding/… 下面其它的包也遵循这个约定。Tagjson 对应值的第一部分用于指定 JSON 对象的名字,比如将 Go 语言中的 TotalCount 成员对应到 JSON 中的 total_count 对象。

上面的例子中 gender 字段的 Tag 还带了一个额外的 omitempty 选项,表示当 Go 语言结构体成员为空或零值时不生成该 JSON 对象(这里 false 为零值)。例如:

package main

import (
	"encoding/json"
	"fmt"
)

type Book struct {
	Name   string `json:"name"`
	Target string `json:"target"`
	Spend  int    `json:"spend,omitempty"`
}

func main() {
	// Book 1 without Spend
	book1 := Book{
		Name:   "仿生程序员会梦见代码羊吗",
		Target: "赛博朋克",
	}
	// 结构体转为 JSON
	data1, err := json.Marshal(book1)
	if err != nil {
		panic(err)
	}
	// book1 won't print Spend attribute
	fmt.Printf("%s\n", data1)

	// Book 2 have Gender attribute
	book2 := Book{
		Name:   "献给阿尔吉侬的花束",
		Target: "科幻",
		Spend:  9,
	}
	// 结构体转为 JSON
	data2, err := json.Marshal(book2)
	if err != nil {
		panic(err)
	}
	// person2 will print Gender attribute
	fmt.Printf("%s\n", data2)
}

在这里插入图片描述

可以看到,因为 Spend 字段里有 omitempty 属性,因此 encoding/json 在将此结构体对象转化为 JSON 字符串时,发现对象里面的 Spend 为 false , 0 ,空指针,空接口,空数组,空切片,空映射,空字符串中的一种,就会被忽略。

Tag 的获取

Tag 的格式上面已经说了,它是由反引号括起来的一系列用空格分隔的 key:"value" 键值对序列:

GO
`key1:"value1" key2:"value2" key3:"value3"`

那么我们如何获取到结构体中的 Tag 呢?这里我们用反射的方法。

使用反射的方法获取 Tag 步骤如下:

  1. 获取字段
  2. 获取 Tag
  3. 获取键值对

其中获取字段有三种方式,而获取键值对有两种方式。

GO
// 三种获取 field
field := reflect.TypeOf(obj).FieldByName("Name")
field := reflect.ValueOf(obj).Type().Field(i)  // i 表示第几个字段
field := reflect.ValueOf(&obj).Elem().Type().Field(i)  // i 表示第几个字段

// 获取 Tag
tag := field.Tag

// 获取键值对
labelValue := tag.Get("label")
labelValue,ok := tag.Lookup("label")
// Get 当没有获取到对应 Tag 的内容,会返回空字符串

下面是一个获取 Tag 以及键值对的例子:

GO
package main

import (
    "fmt"
    "reflect"
)

type Book struct {
    Name   string `json:"name"`
    Target string `json:"target"`
    Spend string `json:"spend,omitempty"`
}

func main() {
    p := reflect.TypeOf(Book{})
    name, _ := p.FieldByName("Name")
    tag := name.Tag
    fmt.Println("Name Tag :", tag)
    keyValue, _ := tag.Lookup("json")
    fmt.Println("key: json, value:", keyValue)
}

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值