Scott's Blog

学则不固, 知则不惑

0%

Go 语法概览 P3

这是 Go 语言之旅的笔记 P3, 内容较长,包括了 Go 中的方法、接口、并发等内容.

方法

Go 没有类。不过你可以为结构体类型定义方法,Go 中方法是带特殊的 {接收者} 参数的函数(方法其实就是函数,只是带了个接收者)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

import (
"fmt"
"math"
)

// 定义了一个结构体
type Vertex struct {
X, Y float64
}

/*-------------后续代码将省略上面的部分------------------*/

// {接收者} 位于 func 关键字和方法名之间
// 在下面的例子中,Abs 方法拥有一个名为 v,类型为 Vertex 的接收者
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
v := Vertex{3, 4}
fmt.Println(v.Abs())
}

/*---------------------------------------------------------*/

// 注:方法相当于函数,所以下面的写法功能是一样的
func Abs(v Vertex) float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
// 但是在使用的时候,不再是 v.Abs(), 而是 Abs(v)
func main() {
v := Vertex{3, 4}
fmt.Println(Abs(v))
}

非结构体也是可以声明方法的,比如对于 float:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type MyFloat float64

// {接收者} 为 (f MyFloat),方法名为 Abs, 返回 float64
// 没有参数
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}

// 使用
f := MyFloat(-1)
f.Abs()

但是,你只能为在同一包内定义的类型的接收者声明方法,而不能为其它包内定义的类型(包括 int 之类的内建类型)的接收者声明方法。

上面的接收者为值接收者,实际上接收者还有一种叫做指针接收者,因为使用指针接收者,你可以在方法内部修改值,所以指针接收者比值接收者更常用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 值接收者 (v Vertex)
func (v Vertex) Abs() float64 {
// 取值、计算
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

// 指针接收者 (v *Vertex)
func (v *Vertex) Scale(f float64) {
// 修改值
v.X = v.X * f
v.Y = v.Y * f
}

func main() {
v := Vertex{3, 4}
v.Scale(10)

// 分析输出
// 如果是指针接收者:
// Scale: 3*10 = 30; 4*10 = 40
// Abs: Sqrt(30*30 + 40*40) = 50

// 如果将指针接收者改为值接收者
// 即 (v *Vertex) -> (v Vertex)
// Scale: 3 ; 4
// Abs: Sqrt(30*30 + 40*40) = 50

方法和指针重定向; 在函数中,如果要求的参数是指针,传值会报错;如果使用指针接收者,则既可以传值,也可以传指针;反之如果要求的参数是值,在函数中传指针也会出错,指针接收者则也可以避免这种情况。

使用指针接收者的好处是:

  1. 指针能够修改传进来的值
  2. 因为传的是指针,避免了值的复制,更高效
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 函数
// 因为参数指定为指针,直接传值会出错
func ScaleFunc(v *Vertex, f float64) {
v.X = v.X * f
v.Y = v.Y * f
}

// 指针接收者
// 下面这两种方式都可以工作
// v.Scale(10), (&v).scale(10)
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}

接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package main

import (
"fmt"
"math"
)

// 一个几何形状的接口,拥有面积和
type geometry interface {
area() float64
perim() float64
}

// 矩形和圆的结构体
type rect struct {
width, height float64
}
type circle struct {
radius float64
}

// 定义函数,实现 rect 的 area() 和 perim() 方法
func (r rect) area() float64 {
return r.width * r.height
}
func (r rect) perim() float64 {
return 2*r.width + 2*r.height
}

// 定义函数,实现 circle 的 area() 和 perim() 方法
func (c circle) area() float64 {
return math.Pi * c.radius * c.radius
}
func (c circle) perim() float64 {
return 2 * math.Pi * c.radius
}

// measure 函数,基于 geometory 接口
func measure(g geometry) {
fmt.Println(g)
fmt.Println(g.area())
fmt.Println(g.perim())
}

// 使用
func main() {
r := rect{width: 3, height: 4}
c := circle{radius: 5}

measure(r)
measure(c)
}

关于接口的另一个例子, 首先是完整的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package main

import (
"fmt"
"math"
)

type Abser interface {
Abs() float64
}

type MyFloat float64

func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}

type Vertex struct {
X, Y float64
}

func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
var a Abser
f := MyFloat(-math.Sqrt2)
v := Vertex{3, 4}

a = f // a MyFloat 实现了 Abser
a = &v // a *Vertex 实现了 Abser

// 下面一行,v 是一个 Vertex(而不是 *Vertex)
// 所以没有实现 Abser。
a = v

fmt.Println(a.Abs())
}

我们逐一来看,方便研究,首先是直接将变量赋值给接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
"fmt"
"math"
)

type Abser interface {
Abs() float64
}
type MyFloat float64


func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}

func main() {
var a Abser
f := MyFloat(-math.Sqrt2)
a = f

fmt.Println(f.Abs()) // 为 MyFload 实现了 Abs 方法, 即
fmt.Println(a.Abs()) // MyFloat 实现了 Abser
}

下面是将结构体赋值给接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
"fmt"
"math"
)

type Abser interface {
Abs() float64
}

type Vertex struct {
X, Y float64
}

func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
var a Abser

v := Vertex{3, 4}
a = &v // a *Vertex 实现了 Abser

fmt.Println(v.Abs())
fmt.Println(a.Abs())
}

它们不同的地方在于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 条件
f := MyFloat(-math.Sqrt2)
v := Vertex{3, 4}

// 定义不同
func (f MyFloat) Abs() float64 {
//
}
func (v *Vertex) Abs() float64 { // ---------------------
//
}

// 使用不同
a = f // a.Abs(), work; a MyFloat 实现了 Abser
a = &v // a.Abs(), work; a *Vertex 实现了 Abser

a = v
// a.Abs(), Error; Vertex does not implement Abser; 如果将
// func (v *Vertex) Abs() float64 改为
// func (v *Vertex) Abs() 则可以正常运行

接口的隐式实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

type I interface {
M()
}

type T struct {
S string
}

// 此方法表示类型 T 实现了接口 I,但我们无需显式声明此事
// 直接将 func 的名字设置成 接口中的名字即可
func (t T) M() {
fmt.Println(t.S)
}

func main() {
var i I = T{"hello"}
i.M()
}

接口也是值,所以可以像值一样传递。可以用作参数和函数的返回值。

接口值可以用作函数的参数或返回值。

在内部,接口值可以看做包含值和具体类型的元组 (value, type), 接口值保存了一个具体底层类型的具体值。接口值调用方法时会执行其底层类型的同名方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package main

import (
"fmt"
"math"
)
// 接口 I,其中规定了需要实现 M() 方法
type I interface {
M()
}
// 结构体 T,其中有 string 类型的 S
type T struct {
S string
}
// 为结构体 T 增加了 M() 方法
// (t *T) 为指针接收者
func (t *T) M() {
fmt.Println(t.S)
}

// 为 float64 增加了 M() 方法
// (f F) 为值接收者
type F float64
func (f F) M() {
fmt.Println(f)
}

// 打印函数,参数为接口 I
// %v:值的 value
// %T: 值的 type
func describe(i I) {
fmt.Printf("(%v, %T)\n", i, i)
}

// 复习
// & 符号的意思是对变量取地址,如:变量a的地址是&a
// * 符号的意思是对指针取值,如:*&a,就是a变量所在地址的值,当然也就是a的值了
func main() {
var i I // 声明接口 i

i = &T{"Hello"} // 定了结构体 -> 取该结构体的地址 -> 给 i
describe(i) // 打印结构体地址的值,以及 type
i.M() // 结构体增加了 M() 方法,运行成功

i = F(math.Pi) // F 是自定义的 float64,初始化值为 math.pi -> 给 i
describe(i) // 打印 i 的值和地址
i.M()
}

底层值为 nil 的接口值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
var i I // 声明接口

var t *T // 初始化一个结构体指针 t,零值为 nil
i = t // 隐式实现了接口,使得 i 有了 M() 方法
describe(i)
i.M()

i = &T{"hello"}
describe(i)
i.M()
}
// 输出:
/*
(<nil>, *main.T)
<nil>
(&{hello}, *main.T)
hello
*/

空接口, 它没有规定接口的方法,空接口可保存任何类型的值; 空接口被用来处理未知类型的值。例如,fmt.Print 可接受类型为 interface{} 的任意数量的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func main() {
var i interface{} // 指定了零个方法的接口值
describe(i)

i = 42
describe(i)

i = "hello"
describe(i)
}

func describe(i interface{}) {
fmt.Printf("(%v, %T)\n", i, i)
}

关于类型断言,它提供了访问接口值底层值的具体方式,并可以进行检测断言:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func main() {
var i interface{} = "hello"

s := i.(string)
fmt.Println(s)

s, ok := i.(string)
fmt.Println(s, ok)

f, ok := i.(float64)
fmt.Println(f, ok)

f = i.(float64) // 报错(panic)
fmt.Println(f)
}

类型选择,可以按顺序从接口值中选择值的类型,文法如下:

1
2
3
4
5
6
7
8
switch v := i.(type) {
case T:
// v 的类型为 T
case S:
// v 的类型为 S
default:
// 没有匹配,v 与 i 的类型相同
}

看一个类型选择的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import "fmt"

func do(i interface{}) {
// 类型选择中的 case 为类型(而非值
// 类型选择中的声明与类型断言 i.(T) 的语法相同
// 只是具体类型 T 被替换成了关键字 type
switch v := i.(type) {
case int:
fmt.Printf("Twice %v is %v\n", v, v*2)
case string:
fmt.Printf("%q is %v bytes long\n", v, len(v))
default:
fmt.Printf("I don't know about type %T!\n", v)
}
}

func main() {
do(21)
do("hello")
do(true)
}

// output:
/*
Twice 21 is 42
"hello" is 5 bytes long
I don't know about type bool!
*/

下面介绍一个普遍使用的接口,即 fmt 包中的 Stringer。一个可以用字符串描述自己的类型。fmt 包(还有很多包)都通过此接口来打印值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import "fmt"

type Person struct {
Name string
Age int
}

func (p Person) String() string {
return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}

func main() {
a := Person{"Arthur Dent", 42}
z := Person{"Zaphod Beeblebrox", 9001}
fmt.Println(a, z)
fmt.Println(a.String())
}
// 输出
/*
Arthur Dent (42 years) Zaphod Beeblebrox (9001 years)
Arthur Dent (42 years)
*/

关于 Go 中的错误处理:

1
2
3
4
5
6
7
// rror 为 nil 时表示成功;非 nil 的 error 表示失败。
i, err := strconv.Atoi("42")
if err != nil {
fmt.Printf("couldn't convert number: %v\n", err)
return
}
fmt.Println("Converted integer:", i)

关于文件读取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
"io"
"strings"
)

func main() {
r := strings.NewReader("Hello, Reader!")

b := make([]byte, 8)
for {
n, err := r.Read(b)
fmt.Printf("n = %v err = %v b = %v\n", n, err, b)
fmt.Printf("b[:n] = %q\n", b[:n])
// 在遇到数据流的结尾时,它会返回一个 io.EOF 错误
if err == io.EOF {
break
}
}
}

关于图像处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"fmt"
"image" // image 包定义了 Image 接口
)

func main() {
m := image.NewRGBA(image.Rect(0, 0, 100, 100))
// Bounds 方法的返回值 Rectangle 实际上是一个 image.Rectangle,它在 image 包中声明
fmt.Println(m.Bounds())
fmt.Println(m.At(0, 0).RGBA())
}

并发

说到并发,首先介绍Go 中的 Go 程(goroutine),它是由 Go 运行时管理的轻量级线程。当你写 go f(x, y, z) 会启动一个新的 Go 程并执行 f(x, y, z)

在 Go 程的运行过程中,f, x, y 和 z 的求值发生在当前的 Go 程中,而 f 的执行发生在新的 Go 程中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"fmt"
"time"
)

func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(1000 * time.Millisecond)
fmt.Println(s)
}
}

func main() {
go say("world")
say("hello")
}

Go 程在相同的地址空间中运行,因此在访问共享的内存时必须进行同步。

关于信道,在 Go 中,实质上是带有类型的管道。

1
2
// 创建信道
ch := make(chan int)

信道操作符有 ->, <-,箭头的方向预示着数据流的方向。

1
2
ch <- v    // 将 v 发送至信道 ch。
v := <-ch // 从 ch 接收值并赋予 v。

以下示例对切片中的数进行求和,将任务分配给两个 Go 程。一旦两个 Go 程完成了它们的计算,它就能算出最终的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // 将和送入 c
}

func main() {
s := []int{7, 2, 8, -9, 4, 0}

c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // 从 c 中接收

fmt.Println(x, y, x+y)
}

关于带缓冲的信道, 使用 make 创建

1
ch := make(chan int, 100)

仅当信道的缓冲区填满后,向其发送数据时才会阻塞。当缓冲区为空时,接受方会阻塞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
// 这里会报错
// ch <- 3
fmt.Println(<-ch)
fmt.Println(<-ch)
}

对于使用信道来说,还有两个基本的操作,即关闭信道,或者是检查一个信道是否关闭了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
"fmt"
)

// 一个求斐波拉契数列的例子
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c) // 使用 close 关闭信道
}

func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
// 循环 for i := range c 会不断从信道接收值,直到它被关闭
for i := range c {
fmt.Println(i)
}
}

  • 注意:只有发送者才能关闭信道,而接收者不能。向一个已经关闭的信道发送数据会引发程序恐慌(panic)。

  • 信道与文件不同,通常情况下无需关闭它们。只有在必须告诉接收者不再有需要发送的值时才有必要关闭,例如终止一个 range 循环。

关于 Go 总的 Select 语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Go's _select_ lets you wait on multiple channel
// operations. Combining goroutines and channels with
// select is a powerful feature of Go.

package main

import (
"fmt"
"time"
)

func main() {

// For our example we'll select across two channels.
c1 := make(chan string)
c2 := make(chan string)

// Each channel will receive a value after some amount
// of time, to simulate e.g. blocking RPC operations
// executing in concurrent goroutines.
go func() {
time.Sleep(1 * time.Second)
c1 <- "one"
}()
go func() {
time.Sleep(2 * time.Second)
c2 <- "two"
}()

// We'll use `select` to await both of these values
// simultaneously, printing each one as it arrives.
for i := 0; i < 2; i++ {
select {
case msg1 := <-c1:
fmt.Println("received", msg1)
case msg2 := <-c2:
fmt.Println("received", msg2)
}
}
}

当 select 中的其它分支都没有准备好时,default 分支就会执行。 为了在尝试发送或者接收时不发生阻塞,可使用 default 分支:

1
2
3
4
5
6
select {
case i := <-c:
// 使用 i
default:
// 从 c 中接收会阻塞时执行
}

Go 中信道非常适合在不同的 Go 程中进行通信。但如果我们不需要通信呢?若我们只是想保证每次只有一个 Go 程能够访问一个共享的变量,从而避免冲突?

这个概念叫互斥,Go 中有一个专门的数据结构来提供这一功能,互斥锁 Mutex,sync.Mutex.

sync.Mutex 中有两个方法,Lock 和 Ulock。

我们可以通过在代码前调用 Lock 方法,在代码后调用 Unlock 方法来保证一段代码的互斥执行

我们也可以用 defer 语句来保证互斥锁一定会被解锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

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

// SafeCounter 的并发使用是安全的。
type SafeCounter struct {
v map[string]int
mux sync.Mutex
}

// Inc 增加给定 key 的计数器的值。
func (c *SafeCounter) Inc(key string) {
c.mux.Lock()
// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
c.v[key]++
c.mux.Unlock()
}

// Value 返回给定 key 的计数器的当前值。
func (c *SafeCounter) Value(key string) int {
c.mux.Lock()
// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
defer c.mux.Unlock()
return c.v[key]
}

func main() {
c := SafeCounter{v: make(map[string]int)}
for i := 0; i < 1000; i++ {
go c.Inc("somekey")
}

time.Sleep(time.Second)
fmt.Println(c.Value("somekey"))
}