go 方法 接口 并发

based on a tour of go

method, interface, goroutine

method

go没有类,所有类相关的方法在func和函数名中间描述。

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

接收者的类型定义和方法声明必须在同一包内;不能为内建类型声明方法.

对于某类型 T,接收者的类型可以用 *T 的文法。(此外,T 不能是像 *int 这样的指针。)

注意传实参和传指针的区别。类似c,传指针才能在函数内修改接收者。

相比于带指针参数的函数,使用以指针作为接收者的方法有一个好处,既可以传值也可以传指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}

func ScaleFunc(v *Vertex, f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
ScaleFunc(v,1) // compiling fail
ScaleFunc(&v,1) // ok
v.Scale(1) //ok
p := &v
p.Scale(1) //ok

同样的,对于以值作为参数的函数,传指针过去会报错,但以值作为接收者的函数既可以接值也可以接指针。(但这时函数内对值的修改只在函数内有效)

传指针的好处类似于c,在于省内存,避免复制,直接修改变量。

interface

接口

接口类型 是由一组方法签名定义的集合。接口类型的变量可以保存任何实现了这些方法的值。注意,这里如果只实现了指针类型的定义,值类型的定义并不能自动转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type Abser interface {
Abs() float64
}
func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
var a Abser
a = f // a MyFloat 实现了 Abser
a = &v // a *Vertex 实现了 Abser
a = v // 失败,没有实现vertex的Abser

可以理解为(?)带有接收者的函数是相应的成员函数,接口中声明的函数即为类带有的成员函数,相当于对于接口实现了多态,对于实现函数的重载,而接口变量实际上可以是任一种类型,只要这种类型下实现了相应需要的所有函数。

为什么叫接口?实际上它提供了一个位置,让我们自己实现的类型能够服用已经有的逻辑,比如print,error,read等。

接口是值,但是即使接口接的是指针,在外部也不能直接*,只能在相应方法中去用。在内部,它既存了值,也存了对应的类型,便于寻找相应的方法。

对于没有初始化或者本身就是nil的接口值,如果有相应的方法,方法仍会被调用,因此需要在方法内部判断。

1
2
3
4
5
var i I
var t *T
i = t
describe(i)
i.M() // 这里M()需要判断值是否是nil

如果方法也没有(比如直接声明了一个接口就直接用了),在调用方法时会报错。

1
2
3
4
5
6
7
8
9
type I interface {
M()
}

func main() {
var i I
describe(i)
i.M() // bug
}

指定了零个方法的接口值被称为 空接口:

1
var i interface{}

空接口可保存任何类型的值。(因为每个类型都至少实现了零个方法。)空接口被用来处理未知类型的值。例如,fmt.Print 可接受类型为 interface{} 的任意数量的参数.

类型断言

类型断言 type assertions 提供了访问接口值底层具体值的方式。类似于映射的判断方式

1
2
t := i.(T)
t, ok := i.(T) // 如果T和i存的类型不同,上面的写法会panic,这个写法ok,t是对应格式的默认值,ok是true false

类型选择

类型选择 type switch 是一种按顺序从几个类型断言中选择分支的结构。类型选择与一般的 switch 语句相似,不过类型选择中的 case 为类型(而非值), 它们针对给定接口值所存储的值的类型进行比较。

1
2
3
4
5
6
7
8
switch v := i.(type) {
case T:
// v 的类型为 T
case S:
// v 的类型为 S
default:
// 没有匹配,v 与 i 的类型相同
} //此选择语句判断接口值 i 保存的值类型是 T 还是 S。在 T 或 S 的情况下,变量 v 会分别按 T 或 S 类型保存 i 拥有的值。在默认(即没有匹配)的情况下,变量 v 与 i 的接口类型和值相同。

Stringer

fmt 包中定义的 Stringer 是最普遍的接口之一。通过Stringer,所有类型都可以实现一个描述自己的字符串。

1
2
3
4
5
6
type Stringer interface {
String() string
}
func (p Person) String() string {
return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
} // 在print的实现中定义一个stringer a,只要这个struct实现了String(),就令a=要打印的实例,然后用String()拿到要打印的字符串来打印。

error

Go 程序使用 error 值来表示错误状态。与 fmt.Stringer 类似,error 类型是一个内建接口:

1
2
3
type error interface {
Error() string
} // 我们只需要实现对应的Error方法即可

io

io 包指定了 io.Reader 接口,它表示从数据流的末尾进行读取。io.Reader 接口有一个 Read 方法,Read 用数据填充给定的字节切片并返回填充的字节数和错误值。在遇到数据流的结尾时,它会返回一个 io.EOF 错误。

1
func (T) Read(b []byte) (n int, err error)

Goroutine

Go 程(goroutine)是由 Go 运行时管理的轻量级线程。命令go会启动一个新线程执行函数,函数和参数的求值在本线程执行。

1
go f(x, y, z)

信道(channel)是带有类型的管道,你可以通过它用信道操作符 <- 来发送或者接收值。信道在使用前必须创建:

1
2
3
ch := make(chan int)
ch <- v // 将 v 发送至信道 ch。
v := <-ch // 从 ch 接收值并赋予 v。

默认情况下,发送和接收操作在另一端准备好之前都会阻塞。这使得 Go 程可以在没有显式的锁或竞态变量的情况下进行同步。

信道可以是 带缓冲的。将缓冲长度作为第二个参数提供给 make 来初始化一个带缓冲的信道。仅当信道的缓冲区填满后,向其发送数据时才会阻塞。当缓冲区为空时,接受方会阻塞。

1
ch := make(chan int, 100)

发送者可通过 close 关闭一个信道来表示没有需要发送的值了。接收者可以通过为接收表达式分配第二个参数来测试信道是否被关闭:若没有值可以接收且信道已被关闭,那么在执行完

1
v, ok := <-ch

之后 ok 会被设置为 false。循环 for i := range c 会不断从信道接收值,直到它被关闭。只有发送者才能关闭信道,而接收者不能。向一个已经关闭的信道发送数据会引发程序恐慌(panic)。 信道与文件不同,通常情况下无需关闭它们。只有在必须告诉接收者不再有需要发送的值时才有必要关闭,例如终止一个 range 循环。

select 语句使一个 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
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}

func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}

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

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

Go 标准库中提供了 sync.Mutex 互斥锁类型及其两个方法:

  • Lock
  • Unlock

我们可以通过在代码前调用 Lock 方法,在代码后调用 Unlock 方法来保证一段代码的互斥执行。参见 Inc 方法。我们也可以用 defer 语句来保证互斥锁一定会被解锁。参见 Value 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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]
}