第七章 接口
- 接口类型是对其它类型行为的抽象和概括,接口只会展示出它们自己的方法
- Go语言中接口类型的独特之处在于它是满足隐式实现的
- 接口是抽象的,接口的调用是依赖于实现了这些方法的类型操作的。也就是接口在方法或函数间的参数传递都是通过实现了该接口的类型传递的。
7.1接口约定
划重点
- 一个类型可以自由的使用另一个满足相同接口的类型来进行替换被称作可替换性(LSP里氏替换)。
常用库及方法
os.Stdout
os.File
bytes.Buffer
io.Writer
utf8.DecodeRune
unicode.IsSpace
7.2接口类型
划重点
- 接口类型具体描述了一系列方法的集合,一个实现了这些方法的具体类型是这个接口类型的实例。
- 接口内嵌的语法和结构内嵌相似,我们可以用这种方式以一个简写命名另一个接口,而不用声明它所有的方法。这种方式本称为接口内嵌
- 实现接口的方法的接收器是指针,则该类型的指针实现了该接口,而类型则没有。
常用库及方法 copy
strings.NewReader
bytes.Buffer.ReadFrom
bytes.String
7.3实现接口的条件
划重点
- 一个类型如果拥有一个接口需要的所有方法,那么这个类型就实现了这个接口
- 接口指定的规则非常简单:表达一个类型属于某个接口只要这个类型实现这个接口
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
- 这个规则甚至适用于等式右边本身也是一个接口类型
w = rwc // OK: io.ReadWriteCloser has Write method rwc = w // compile error: io.Writer lacks Close method
- T类型的值不拥有所有T指针的方法,所以T类型和T实现的接口分别是T和&T,进行变量承载时是不一样的。
type IntSet struct { /* ... */ } func (*IntSet) String() string var _ = IntSet{}.String() // compile error: String requires >*IntSet receiver
但是可以在一个IntSet值上调用这个方法,这是编译器自动转换的:
var s IntSet var _ = s.String() // OK: s is a variable and &s has a String >method
由于只有IntSet类型有String方法,所有也只有IntSet类型实现了fmt.Stringer接口
var _ fmt.Stringer = &s // OK var _ fmt.Stringer = s // compile error: IntSet lacks String >method
godoc -analysis=typetool(§10.7.4)
展示每个类型的方法和具体类型和接口之间的关系- 即使具体类型有其它的方法也只有接口类型暴露出来的方法会被调用到
os.Stdout.Write([]byte("hello")) // OK: *os.File has Write method os.Stdout.Close() // OK: *os.File has Close method var w io.Writer w = os.Stdout w.Write([]byte("hello")) // OK: io.Writer has Write method w.Close() // compile error: io.Writer lacks Close method
interface{}
被称为空接口类型是不可或缺的。因为空接口类型对实现它的类型没有要求,所以我们可以将任意一个值赋给空接口类型- 用类型断言来获取
interface{}
中值的方法,(§7.10) - 下面的定义在编译期断言一个
*bytes.Buffer
的值实现了io.Write
r接口类型:
// *bytes.Buffer must satisfy io.Writer var w io.Writer = new(bytes.Buffer) // *bytes.Buffer must satisfy io.Writer var _ io.Writer = (*bytes.Buffer)(nil)
- 不像基于类的语言,他们一个类实现的接口集合需要进行显式的定义,在Go语言中我们可以在需要的时候定义一个新的抽象或者特定特点的组,而不需要修改具体类型的定义其他语言??不了解
7.4flag.Value接口
常用库及方法
flag.Value
flag.Duration
flag.Parse
flag.CommandLine.Var
time.Sleep
7.5接口值
- 接口值,由两个部分组成,一个具体的类型和那个类型的值,称为接口的动态类型和动态值
- Go语言概念模型中,一些提供每个类型信息的值被称为类型描述符,比如类型的名称和方法。在一个接口值中,类型部分代表与之相关类型的描述符。
- 给出下面四个语句:
var w io.Writer w = os.Stdout w = new(bytes.Buffer) w = nil
- 接口的零值就是它的类型和值的部分都是
nil
,
- 接口值的动态类型被设为*os.Stdout指针的类型描述符,它的动态值持有os.Stdout的拷贝;这是一个代表处理标准输出的os.File类型变量的指针
- 第三个语句
- 第四个语句和第一个相同,赋为
nil
- 一个接口值可以持有任意大的动态值,
var x interface{} = time.Now()
,概念上的模型为:
- 接口值可以使用
==
和!=
来进行比较。两个接口值相等仅当它们都是nil
值或者它们的动态类型相同并且动态值也根据这个动态类型的==
操作相等。因为接口值是可比较的,所以它们可以用在map
的键或者作为switch
语句的操作数。 - 如果两个接口值的动态类型相同,但是这个动态类型是不可比较的(比如切片),将它们进行比较就会失败并且panic
- 接口的可比较性是不确定的;基本类型和指针是确定课比较的;切片,映射类型,和函数是确定不可比较的。在比较接口值或者包含了接口值的聚合类型,以及使用接口作为map的键或者switch的操作数时,我们必须要意识到潜在的panic。
- 处理错误或者调试的过程中,可以使用
fmt
包的%T
获得接口值的动态类型。
7.5警告:一个包含nil指针的接口不是nil接口
划重点
- 一个不包含任何值的nil接口值和一个刚好包含nil指针的接口值是不同的。
7.6sort.Interface接口
划重点
常用库及方法
sort.Sort
sort.Interface
sort.Reverse
sort.IsSorted
sort.IntsAreSorted
sort.Ints
sort.IntSlice
sort.Stable
time.Duration
time.ParseDuration()
tabwriter.Writer
tw:=new(tabwriter.Writer).Init
tw.Flush()
7.7http.Handler接口
划重点
net/http
包提供了一个全局的ServeMux
实例DefaultServerMux
和包级别的http.Handle
和http.HandleFunc
函数。- 一个类型的方法如果实现了某个接口类似行为的函数是不能直接传给这个接口的,因为这个类型的方法没有方法,必须经过显式的转换给一个具有该行为的类型才可以。比如文中:
db.list
的调用会援引一个接收者是db
的database.list
方法。所以db.list
是一个实现了handler
类似行为的函数,但是因为它没有方法,所以它不满足http.Handler
接口并且不能直接传给mux.Handle
。语句http.HandlerFunc(db.list)
是一个转换而非一个函数调用,因为http.HandlerFunc
是一个类
型。HandlerFunc
是一个让函数值满足一个接口的适配器
package http
type HandlerFunc func(w ResponseWriter, r *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
- web服务器在一个新的协程中调用每一个handler,所以当handler获取其它协程或者这个handler本身的其它请求也可以访问的变量时一定要使用预防措施比如锁机制。
常用库及方法
http.ListenAndServe
req.URL.Path
req.URL.Query().Get
w.WriteHeader()
http.StatusNotFound
http.Error
r.FormValue
ServeMux
http.NewServeMux()
mux.Handle
strconv.Atoi
7.8error接口
划重点
- 预定义error类型,实际上它就是interface类型,这个类型有一个返回错误信息的单一方法
type error interface { Error() string }
- 创建一个
error
最简单的方法就是调用errors.New
函数,它会根据传入的错误信息返回一个新的error
。 - 指针类型
*errorString
满足error
接口而非errorString
类型,所以每个New
函数的调用都分配了一个独特的和其他错误不相同的实例
fmt.Println(errors.New("EOF") == errors.New("EOF")) // "false"
- 调用
errors.New
函数是非常稀少的,因为有一个方便的封装函数fmt.Errorf
,它还会处理字符串格式化,fmt.Errorf
调用了errors.New
- 除了
errors.New
,syscall
包提供了Go语言底层系统调用API,它定义一个实现error
接口的数字类型Errno
,Unix平台上,Errno
的Error
方法会从一个字符串表中查找错误消息
7.9示例: 表达式求值
划重点
go test -v gopl.io/ch7/eval
,-v
标识可以让我们看到测试用例打印的输出
常用库及方法
pow(x,y)
,sin(x)
,sqrt(x)
testing.Errof
strings.ContainsRune
7.10类型断言
划重点
- 类型断言是一个使用在接口值上的操作。语法上它看起来像
x.(T)
被称为断言类型,这里x
表示一个接口的类型和T表示一个类型。一个类型断言检查它操作对象的动态类型是否和断言的类型匹配。 - 一,如果断言的类型
T
是一个具体类型,然后类型断言检查x
的动态类型是否和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
- 二,如果相反断言的类型
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
- 将断言赋值给两个结果,这时候断言失败不会出现
panic
,但是会把这个结果标识给第二个布尔类型的返回值,失败了将返回零值,false
;
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
7.11基于类型断言区别错误类型
划重点
- os包中定义了一个PathError类型来描述在文件路径操作中涉及到的失败,像Open或者Delete操作,并且定义了一个叫LinkError的变体来描述涉及到两个文件路径的操作,像Symlink和Rename。
常用库及方法
os.Open
os.IsExist
os.IsNotExist
os.IsPermission
os.PathError
``
7.12通过类型断言询问行为
划重点
string
转[]byte
,转换分配内存并且做一个拷贝,但是这个拷贝在转换后几乎立马就被丢弃掉- 许多满足
io.Writer
接口的重要类型同时也有WriteString
方法,包括*bytes.Buffer,*os.File和*bufio.Writer
。) io.WriteString
函数提供。这是向一个io.Writer
接口写入字符串的推荐方法。
7.13类型开关
划重点
- 接口被以两种不同的方式使用,:
- 以
io.Reader,io.Writer,fmt.Stringer,sort.Interface,http.Handler,和error
为典型,重点在于方法上,而不是具体的类型上。–(子类型多态) - 利用一个接口值可以持有各种具体类型值的能力并且将这个接口认为是这些类型的union(联合),,重点在于具体的类型满足这个接口,而不是在于接口的方法。–(非参数多态)
- 总结,一种作为一个函数的抽象;一种作为一个变量的抽象;
- 以
- 运算对象是
x.(type)
-它使用了关键词字面量type
-并且每个case
有一到多个类型。一个类型开关基于这个接口值的动态类型使一个多路分支有效。
switch x.(type) { case nil: // ... case int, uint: // ... case bool: // ... case string: // ... default: // ... }
常用库及方法
sql.DB
db.Exec
7.14示例: 基于标记的XML解码
划重点
encoding/xml
包也提供了一个更低层的基于标记的API用于XML解码。- 在基于标记的样式中,解析器消费输入和产生一个标记流;四个主要的标记类型-
StartElement,EndElement,CharData,和Comment
-每一个都是encoding/xml
包中的具体类型。 - 满足可识别联合的具体类型的集合被设计确定和暴露,而不是隐藏。可
识别的联合类型几乎没有方法;操作它们的函数使用一个类型开关的case集合来进行表述;这个case集合中每一个case中有不同的逻辑。
常用库及方法
(*xml.Decoder).Token
xml.NewDecoder
dec.Token()
xml.StartElement
xml.EndElement
xml.CharData
tok.Name.Local
io.EOF
7.15一些建议
划重点
- 不要一开始创建一个接口的集合然后后面定义满足它们的具体类型。这种方式的结果就是有很多的接口,它们中的每一个仅只有一个实现。
- 接口只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要。
- 接口设计的一个好的标准就是 ask only for what you need(只考虑你需要的东西)