EffectiveGo-4-方法、接口和其它类型

Parts of Effective Go

方法

指针和值 Pointers vs Values

可以为任何已命名的类型(除了指针和接口)定义方法
接收者可以不是结构体

1
2
3
4
5
6
7
8
type ByteSlice []byte
// Append 是类型ByteSlice 的方法
// 方法Append 的接收者slice 的类型是 ByteSlice
func (slice ByteSlice) Append(data []byte) []byte {
// TODO xx
// return
}
1
2
3
4
5
6
7
8
// 如果仍然需要该方法返回更新后的切片
// 可以将接收者改为指向 ByteSlice类型数值的指针
func (p *ByteSlice) Append(data []byte) {
slice := *p
// TODO xx
// 但是没有 return
*p = slice
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// 为类型*ByteSlice 构建与标准Write类似的方法
// 类型*ByteSlice 满足了标准io.Writer接口
func (p *ByteSlice) Write(data []byte) (n int, err error) {
slice := *p
// TODO xx
*p = slice
return len(data), nil
}
// 可通过打印将内容写入
var b ByteSlice
fmt.Fprintf(&b, "This hour has %d days\n", 7)
// 只有 *ByteSlice (指针类型) 才满足 io.Writer

以指针或值为接收者的区别在于:

  • 值方法可通过指针和值调用
  • 指针方法只能通过指针来调用

指针方法 可以修改接收者
值方法 会导致方法接收到的是该值的副本,任何修改都会被丢弃

如果该值是可寻址的,当使用值调用指针方法时,Go语言会自动插入取址符
编译器会将b.Write重写为(&b).Write

在字节切片上使用Write已被bytes.Buffer实现

接口和其它类型 Interfaces and other types

接口

Go中的接口为指定的对象的行为提供了一种方法
If something can do this, then it can be used here.

  • 通过实现String方法,可以自定义打印函数
  • 通过实现Write方法,Fprintf能对任何对象产生输出

Go中,仅包含一两种方法的接口很常见,且其名称通常来自于实现它的方法
Such as io.Writer for something that implements Write.

每种类型都能实现多个接口
例如实现了sort.Interface接口的集合就可通过sort包中的例程routine进行排序,该接口包括Len()Less(i, j int) boolSwap(i, j 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
type Sequence []int
// Methods required by sort.Interface.
// 实现 sort.Interface 必需的方法
func (s Sequence) Len() int {
return len(s)
}
func (s Sequence) Less(i, j int) bool {
return s[i] < s[j]
}
func (s Sequence) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
// 用于打印的方法
// 在打印前对元素进行排序
func (s Sequence) String() string {
sort.Sort(s)
str := "["
for i, elem := range s {
if i > 0 {
str += " "
}
str += fmt.Sprint(elem)
}
return str + "]"
}

类型转换 Conversions

SequenceString方法重新实现了Sprint为切片实现的功能
若在调用Sprint之前将Sequence转换为纯粹的[]int,就能共享已实现的功能

1
2
3
4
5
6
7
8
// 通过类型转换实现在String方法中安全调用Sprintf
func (s Sequence) String() string {
sort.Sort(s)
return fmt.Sprint([]int(s))
}
// 忽略类型名的情况下
// 类型Sequence 和 类型[]int 是相同的
// 因此二者之间进行的转换也是合法的

上述类型转换过程并不会创建新值,它只是让现有的值看起来有个新类型而已
而有些合法的转换会创建新值,例如从整数转换为浮点数等

Go中,为了访问不同的方法集合,而进行类型转换的情况是非常常见的

1
2
3
4
5
// 使用 sort.intSilce 来简化
func (s Sequence) String() string {
sort.IntSlice(s).Sort()
return fmt.Sprint([]int(s))
}

不必让 Sequence 实现多个接口(排序和打印),可以通过将数据条目转换为多种类型(Sequencesort.IntSlice[]int)来使用相应的功能
这种用法不常使用,但往往很有效

接口转换

类型选择(Type switch)是类型转换的一种形式,它接受一个接口,在switch中依据类型选择对应的case,并在某种意义上将其转换为该种类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// fmt.Printf 类型选择简化版
// 将值转换为字符串
type Stringer interface {
String() string
}
// Value provided by caller.
// 调用者提供 Value
var value interface{}
switch str := Value.(type) {
case string: // 获取具体的值
return str
case Stringer: // 将该接口转换为另一个接口
return str.String()
}

当明确知道一个值的类型,使用类型断言就可以提取它
当类型选择只有一种情况(只会进入某个case)时,使用类型断言就可以了
类型断言只接受一个接口值,并从中提取明确类型的值
格式value.(typeName)
提取字符串str := value.(string),如果它所转换的值中不包含字符串,该语句就会以运行时错误崩溃
可使用comma, ok惯用测试它能安全的判断该值是否是字符串

1
2
3
4
5
6
7
8
9
// 若类型断言失败
// str将继续存在
// str将拥有字符串类型的零值-空字符串
str, ok := value.(string)
if ok {
fmt.Printf("string value is: %q\n", str)
} else {
fmt.Printf("value is not a string\n")
}
1
2
3
4
5
if str, ok := value.(string); ok {
return str
} else if str, ok := value.(Stringer); ok {
return str.String()
}

若某种现有的类型仅仅实现了一个接口,且除此之外并没有可导出的方法,则该类型本身就无需导出
仅导出该接口能让开发者更专注于其行为而非实现,其它不同属性的实现能反映出该原始类型的行为
同样也能够避免为每个通用接口的实例重复编写文档

构造函数应当返回一个接口值而非实现的类型
例如在hash库中,crc32.NewIEEEadler32.New都返回接口类型hash.Hash32,如果要使用Adler32算法替换CRC-32,只需修改构造函数调用即可, 其余代码则不受算法改变的影响
同样的方式能将crypto包中多种联系在一起的流密码算法与块密码算法分开,crypto/cipher包中的Block接口指定了块密码算法的行为,它为单独的数据块提供加密,和bufio包类型,任何实现了该接口的密码包都能被用于构造以Stream为接口表示的流密码,而无需知道块密码的细节

1
2
3
4
5
6
7
8
9
type Block interface {
BlockSize() int
Encrypt(src, dst []byte)
Decrypt(src, dst []byte)
}
type Stream interface {
XORKeyStream(dst, src []byte)
}
1
2
3
4
5
6
7
8
9
10
// 计数器模式CTR流定义
// 它将块加密改为流加密
// 块加密细节已被抽象化
// NewCTR 返回一个 Stream
// 其加密/解密使用计数器模式中给定的 Block 进行
// iv 的长度必须与 Block 的块大小相同
func NewCTR(block Block, iv []byte) Stream {
// TODO xx
}

NewCTR的应用并不仅限于特定的加密算法和数据源,它适用于任何对Block接口和Stream的实现
因为它们返回接口值,所以用其它加密模式来代替CTR只需做局部的更改
构造函数的调用过程必须被修改,但由于其周围的代码只将它看做Stream,因此它们不会注意到其中的区别

接口和方法

因为几乎任何类型都能添加方法,所以几乎任何类型都能满足一个接口

1
2
3
4
5
// http 包中定义了 Handler 接口
// 任何实现了 Handler 的对象都能处理 HTTP 请求
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}

ResponseWriter接口提供了对响应客户端请求的方法的访问
由于这些方法包含了标准的Write方法,因此http.ResponseWriter可用于任何io.Writer适用的场景
Request结构体包含已解析的客户端请求

1
2
3
4
5
6
7
8
9
10
11
// 假设所有的 HTTP 请求都是 GET
// 忽略 POST
// 以下代码用于记录某个页面被访问的次数
type Counter struct {
n int
}
func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
ctr.n++
fmt.Fprintf(w, "counter = %d\n", ctr.n)
}
1
2
3
4
5
// 将一个服务器添加到 URL 树的一个节点上
import "net/http"
// TODO xx
ctr := new(Counter)
http.Handle("/counter", ctr)

Counter不一定要是结构体,也可以是整数type Counter int
但是接收者必须是指针类型,增量操作对于调用者才是可见的

1
2
3
4
5
6
7
8
9
// 当页面被访问时
// 使用信道去通知程序更新内部状态
// 每次浏览该信道都会发送一个提醒
type Chan chan *http.Request
func (ch Chan) ServeHTTP(w http.ResponseWriter, req *http.Request) {
ch <- req
fmt.Fprint(w, "notification sent")
}
1
2
3
4
// 输出调用服务器二进制程序时使用的实参 /args
func ArgServer() {
fmt.Println(os.Args)
}
1
2
3
4
5
6
7
8
9
10
// 为函数写一个方法
// HandlerFunc 类型是一个适配器
// 它允许将普通函数用做 HTTP 处理程序
// 若 f 是具有适当签名的函数
// HandlerFunc(f) 就是调用 f 的处理程序对象
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) {
f(w, req)
}

HandlerFunc是具有ServeHTTP方法的类型,因此该类型的值就能处理HTTP请求
接收者是一个函数f,而该方法调用f
接收者变成了一个信道,而方法通过该信道发送消息

1
2
3
4
// 让 ArgServer 拥有合适的签名
func ArgServer(w http.ResponseWriter, req *http.Request) {
fmt.Fprintln(w, os.Args)
}
1
2
3
// ArgServer 和 HandlerFunc 拥有了相同的签名
// 现在可以将其转换为这种类型以访问它的方法
http.Handle("/args", http.HandlerFunc(ArgServer))

/args页面被访问时,安装到该页面的处理程序就有了值ArgServer类型HandlerFunc
HTTP服务器会以ArgServer为接收者,调用该类型的ServeHTTP方法
ServeHTTP方法会反过来调用ArgServer(通过f(c, req)),实参就会被显示出来

接口只是方法的集合,而几乎任何类型都能定义方法。
Interfaces are just sets of methods, which can be defined for (almost) any type.