方法** 其实就是一个函数,在 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
的类型为 Student
, student2
赋值给 s1
,由于 Student
实现了接口变量 s1
所以会有输出。而接下来 s1
又被赋值为 &student3
,同样有输出。接下来的结构体 Worker
使用指针接受者实现 Study
接口。worker1
的类型为 Worker
, s2
被赋值为 &worker1
,所以会有输出。但如果把 s2
赋值为 worker1
会报错,对于使用指针接受者的方法,用一个指针或者一个可取得地址的值来调用都是合法的。但接口中存储的具体值(Concrete Value)并不能取到地址,因此对于编译器无法自动获取 worker1
的地址,于是程序报错。
使用接口可以实现多态,例如下面的程序,定义了名为 Study
的接口,接口中有方法 learn()
。程序中还定义了结构体 Student
和 Worker
,分别实现了 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: , Value:
}
除了上面讲到的使用空接口作为函数参数的用法,空接口还有以下两种用法。
直接使用 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
}
我们也可以定义一个接收任何类型的 array
、 slice
、 map
、 strcut
。例如:
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 语言的 协程(Groutine) 是与其他函数或方法一起并发运行的工作方式。协程可以看作是轻量级线程。与线程相比,创建一个协程的成本很小。因此在 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
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) ,就是一个管道,可以想像成 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
有几个方法:
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
}
运行上面的程序,主协程就会等待创建的协程执行完毕后退出。
使用上面的信道方法,虽然可行,但在你程序中使用很多协程的话,你的代码就会看起来很复杂,这里就要介绍一种更好的方法,那就是使用 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
语法如下:
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
会选择首先响应先完成的 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
准备就绪时,将会随机地选取其中之一去执行。当 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,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
。
使用互斥锁很简单,但要注意同一协程里不要在尚未解锁时再次加锁,也不要对已经解锁的锁再次解锁。
当然,使用通道也可以处理竞争条件,把通道作为锁在前面讲通道的时候已经讲过,这里就不再赘述。
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())
}
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
和 recover
。只有当程序不能继续运行的时候,才应该使用 panic
和 recover
机制。
panic
有两个合理的用例:
panic
,因为如果不能绑定端口,啥也做不了。nil
作为参数调用了它。在这种情况下,我们可以使用 panic
,因为这是一个编程错误:用 nil
参数调用了一个只能接收合法指针的方法。下面是内建函数 panic
的签名:
GO
func panic(v interface{})
当程序终止时,会打印传入 panic
的参数。
GO
package main
func main() {
panic("panic error")
}
运行上面的程序,会打印出传入 panic
函数的信息,并打印出堆栈跟踪:
GO
panic: panic error
上面已经提到了,当函数发生 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
是一个内建函数,用于重新获得 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
。
内置函数 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
只能分配和初始化类型为 slice
、 map
或 chan
的对象。与 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
:只能为 slice
、 map
、 chan
分配内存,并初始化,返回的是类型。
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)
}()
}
静态类型就是变量声明时候的类型。例如:
GO
// int 是静态类型
var number int
// string 也是静态类型
var name string
动态类型是程序运行时系统才能看见的类型。例如:
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
Go 语言提供了一种机制,能够在运行时更新变量和检查它们的值、调用它们的方法,而不需要在编译时就知道这些变量的具体类型。这种机制被称为 反射 。
反射是把双刃剑,功能强大但代码可读性并不理想,若非必要并不推荐使用反射。
在 Go 中 reflect
包实现了运行时反射。reflect
包会帮助识别 interface{}
变量的底层具体类型和具体值。
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
表示 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
表示的是种类。在使用反射时,需要理解类型(Type)和种类(Kind)的区别。编程中,使用最多的是类型,但在反射中,当需要区分一个大品种的类型时,就会用到种类(Kind)。
Go 语言程序中的类型(Type)指的是系统原生数据类型,如 int
、 string
、 bool
、 float32
等类型,以及使用 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()
方法返回结构体中字段的数量。
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(i int)
方法返回字段 i
的 reflect.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.Type
和 reflect.Value
来表现。
Go 语言里有反射三定律,是你在学习反射时,很重要的参考:
接下来我们就来讲一讲反射三大定律。
Reflection goes from interface value to reflection object.
反射第一定律:反射可以将“接口类型变量”转换为“反射类型对象”。
这里反射类型指 reflect.Type
和 reflect.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()
方法完成了从接口类型变量到反射对象的转换。在这里说接口类型是因为 TypeOf
和 ValueOf
两个函数接收的是 interface{}
空接口类型, Go 语言函数都是值传递,会将类型隐式转换成接口类型。
Reflection goes from reflection object to interface value.
反射第二定律:反射可以将“反射类型对象”转换为“接口类型变量”
第二定律刚好和第一定律相反,第一定律讲的是从接口变量到反射对象的转换,而第二定律讲的是从反射对象到接口变量的转换。
一个 reflect.Value
类型的变量,我们可以使用 Interface
方法恢复其接口类型的值。事实上,这个方法会把 type
和 value
信息打包并填充到一个接口变量中,然后返回。
其函数声明如下:
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)
}
在之前结构体的章节里我们讲过结构体的使用,一般情况下,我们定义结构体每个字段都是由字段名字以及字段的类型构成,例如:
GO
type Book struct {
Name string
Target string
Spend int
}
但这一章要讲的是在字段上增加一个属性,这个属性是用反引号括起来的一个字符串,我们称之为 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/… 下面其它的包也遵循这个约定。Tag
中 json
对应值的第一部分用于指定 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 的格式上面已经说了,它是由反引号括起来的一系列用空格分隔的 key:"value"
键值对序列:
GO
`key1:"value1" key2:"value2" key3:"value3"`
那么我们如何获取到结构体中的 Tag 呢?这里我们用反射的方法。
使用反射的方法获取 Tag 步骤如下:
其中获取字段有三种方式,而获取键值对有两种方式。
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)
}