go语言圣经第七章(读书笔记)
第七章 接口
接口约定
-
接口不会暴露出它所代表的对象的内部值的结构和这个对象支持的基础操作的集合
-
接口只展示他们自己的方法
-
一个类型可以自由的使用另一个满足相同接口的类型来进行替换被称作可替换性(LSP里氏替换)。
-
接口约定:在实现接口的过程,必须准守接口中展示的方法(包括这个方法的名字,参数,返回值等)
-
io.Writer类型接口:
package io
// Writer is the interface that wraps the basic Write method.
type Writer interface {
// Write writes len(p) bytes from p to the underlying data stream.
// It returns the number of bytes written from p (0 <= n <= len(p))
// and any error encountered that caused the write to stop early.
// Write must return a non-nil error if it returns n < len(p).
// Write must not modify the slice data, even temporarily.
//
// Implementations must not retain p.
Write(p []byte) (n int, err error)
}
- 可以按照以上接口约定实现这个接口:
type ByteCounter int
func (c *ByteCounter) Write(p []byte) (int, error) {
*c += ByteCounter(len(p))
return len(p), nil
}
func main() {
var c ByteCounter
c.Write([]byte("hello"))
fmt.Println(c)
c = 0
var name = "Dolly"
fmt.Fprintf(&c, "hello, %s", name)
fmt.Println(c)
}
接口类型
- 接口类型具体描述了一系列方法的集合,一个实现了这些方法的具体类型是这个接口的实例(例如上一下节的ByteCounter)
- 接口也是可以内嵌的,用法和结构体相似
type Reader interface {
Read(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
type ReadWriter interface {
Read(p []byte) (n int, err error)
Writer
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
实现接口的条件
- 一个类型如果拥有一个接口所需要的所有方法,那么这个类型就实现了这个接口
- 接口指定的规则:表达一个类型属于某个接口只要这个类型是实现这个接口
var w io.Writer
w = os.Stdout // OK: *os.File has Write method
w = new(bytes.Buffer) // OK: *bytes.Buffer has Write method
w = time.Second // compile error: time.Duration lacks Write method
var rwc io.ReadWriteCloser
rwc = os.Stdout
// OK: *os.File has Read, Write, Close methods
rwc = new(bytes.Buffer) // compile error: *bytes.Buffer lacks Close method
- 为了说明书中的问题(gopl.pdf 237页),下面给出一个和书中例子稍有不同的例子
package main
import (
"fmt"
)
type a struct {
v int
}
func (i *a) test() {
i.v = 666
}
func main() {
t := a{1}
fmt.Println(t) //{1}
t.test()
fmt.Println(t) //{666}
p := &a{1}
fmt.Println(p.v) //1
p.test()
fmt.Println(p.v) //666
var _ = a{1}.test() //error
/*
# command-line-arguments
./a.go:27:15: cannot call pointer method on a literal
./a.go:27:15: cannot take the address of a literal
./a.go:27:20: a literal.test() used as value
*/
}
- 虽然说类型T或者类型*T调用方法时会隐式处理一些细节,但是如果像例子中最后那样调用方法是不行的,必须放在一个变量中,才能确保调用到相应的方法
- "T类型的值不拥有所有*T指针的方法,那这样它就可能只实现更少的接口"
*关于空接口interface{},实现它没有要求,所以我们可以将任意一个值赋给它
var any interface{}
any = true
any = 12.34
any = "hello"
any = map[string]int{"one": 1}
any = new(bytes.Buffer)
- 可以通过类型断言来获取interface{}中值的方法,后续小结给出解释,本小结略过
- 不必定义多一个变量来实现接口
// *bytes.Buffer must satisfy io.Writer
var w io.Writer = new(bytes.Buffer)
- 一个具体的类型可能实现了很多不相干的接口
- "每一个具体类型的组基于它们相同的行为可以表示成一个接口类型。不像基于类的语言,他们一个类实现的接口集合需要进行显式的定义,在Go语言中我们可以在需要的时候定义一个新的抽象或者特定特点的组,而不需要修改具体类型的定义。当具体的类型来自不同的作者时这种方式会特别有用。当然也确实没有必要在具体的类型中指出这些共性。"
接口值
- 从严格定义上来说,由于Go是静态类型语言,类型是编译期的概念,因此类型并非一个值。
- 在接口的概念模型中,一些提供类型信息的值被称为类型描述符,
- 接口值由两部分组成
1、接口的动态类型:具体的类型
2、动态值:那个类型的值 - 在Go中,变量总是被一个定义明确的值初始化,即使接口类型也不例外
- 以下是一个例子
var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil

- 一个接口基于它的动态类型被描述为空或非空
- 可以通过使用w == nil或者w != nil来判断上例的接口值是否为空
- 调用一个空接口的任意方法都会产生panic
- 对于w = os.Stdout语句(os.Stdout是*os.File类型)
1.实际上这个赋值过程发生了隐式转换io.Writer(os.Stdout)
2.这个接口值的动态类型被设置为*os.Stdout的类型描述符,它的动态值持有*os.Stdout的拷贝,从而可以访问到os.File的值

*关于w = new(bytes.Buffer),和上例一样,需要注意的是new操作符生成指定类型的指针,并用零值初始化相应的内存空间
- 接口值不只是可以持有指针类型,也可以持有非指针类型
var x interface{} = time.Now()

- 接口值是可以使用==或!=比较的
1.两个接口值相等但且仅当它们都是nil值或者它们的动态类型相同并且动态值也根据这个动态类型的==操作相等
2.根据接口值的可比较性,接口可以用在map的key中或者switch语句的操作数中
3.但是,如果其动态类型是不可比较的,例如slice,map,将他们进行比较会发生panic
- 以上2,3点是有区别的,请读者仔细领会
- 相较其它类型,要么是安全的可比较类型(如基本类型和指针),要么是完全不可比较的(如切片,映射类型,函数);比较接口值或者包含了接口值的聚合类型时,是有潜在panic的(如果是这样的,写程序当然尽量避免比较接口值)
- 得知接口值的动态类型是非常有帮助的,可以通过fmt包的%T参数(这里底层是用了反射机制实现的):
var w io.Writer
fmt.Printf("%T\n", w) // "<nil>"
w = os.Stdout
fmt.Printf("%T\n", w) // "*os.File"
w = new(bytes.Buffer)
fmt.Printf("%T\n", w) // "*bytes.Buffer"
警告:一个包含nil指针的接口不是nil接口
-
一个包含nil指针的接口:
![]()
-
一个nil接口
![]()
-
例子
const debug = true
func main() {
var buf *bytes.Buffer
if debug {
buf = new(bytes.Buffer) // enable collection of output
}
f(buf) // NOTE: subtly incorrect!
if debug {
// ...use buf...
}
}
// If out is non-nil, output will be written to it.
func f(out io.Writer) {
// ...do something...
if out != nil {
out.Write([]byte("done!\n"))
}
}
- 上例中,不管buf有无被初始化,作为参数传入f函数后,都将发生panic,因为传参后,只是作为含有nil指针的接口,不是nil接口
- 稍微修改:
var buf io.Writer
if debug {
buf = new(bytes.Buffer) // enable collection of output
}
f(buf) // OK
- 基于上述问题,函数传参最好是对应类型,而非接口值与其实现,从而避免隐式转换带来的panic;如果非要进行隐式转换,需要特别注意(例如net.http中的Handler接口)
类型断言
- 类型断言:x.(T)
- 一个类型断言检查它操作对象的动态类型是否和断言类型的类型匹配
1.断言类型T是一个具体类型,然后类型断言检查x的动态类型是否和T相同:检查成功,返回该类型的实例;否则抛出panic
var w io.Writer
w = os.Stdout
f := w.(*os.File)
// success: f == os.Stdout
c := w.(*bytes.Buffer) // panic: interface holds *os.File, not *bytes.Buffer
2.如果断言类型T是一个接口类型,然后类型断言检查是否x的动态类型满足T:如果成功,动态值没有获取到,结果却有了T类型(换句话说,对一个接口类型的类型断言,改变了类型的表述方式,改变了可以获取的方法集合,但是它保护了接口值内部的动态类型和值的部分)
var w io.Writer
w = os.Stdout
rw := w.(io.ReadWriter) // success: *os.File has both Read and Write
w = new(ByteCounter)
rw = w.(io.ReadWriter) // panic: *ByteCounter has no Read method
- 对于2这种情况,可以简单理解为当w被实现为*os.File类型后,通过接口断言,改变表述方式,获取部分方法集合
- 如果类型断言操作的是一个nil接口值,那么无论断言的类型是什么都会失败
w = rw
// io.ReadWriter is assignable to io.Writer
w = rw.(io.Writer) // fails only if rw == nil
- 还可以通过以下方式进行断言,这样不会抛出panic导致奔溃
var w io.Writer = os.Stdout
f, ok := w.(*os.File) // success:ok, f == os.Stdout
b, ok := w.(*bytes.Buffer) // failure: !ok, b == nil
基于类型断言区别错误类型
import (
"errors"
"syscall"
)
var ErrNotExist = errors.New("file does not exist")
// IsNotExist returns a boolean indicating whether the error is known to
// report that a file or directory does not exist. It is satisfied by
// ErrNotExist as well as some syscall errors.
func IsNotExist(err error) bool {
if pe, ok := err.(*PathError); ok {
err = pe.Err
}
return err == syscall.ENOENT || err == ErrNotExist
}
类型开关
- 一个接口的方法表达了实现这个接口的具体类型间的相思性,但是隐藏了代表的细节和这些具体类型本身的作。重点在于方法上,而不是具体的类型上
- 第二个方式利用一个接口值可以持有各种具体类型值的能力并且将这个接口认为是这些类型的union(联合)。类型断言用来动态地区别这些类型并且对每一种情况都不一样
- type switch类型开关
switch x.(type) {
case nil:
// ...
case int, uint: // ...
case bool: // ...
case string: // ...
default: // ...
}
一些建议
- 对于接口设计的一个好的标准就是 ask only for what you need(只考虑你需要的东西)



浙公网安备 33010602011771号