Interface in Go
Contents
Interface
接口类型是其它类型的行为的抽象和概括。
Go 的接口是隐式满足的(satisfied implicitly),因此一个具体类型无需声明它所满足的所有接口,只需拥有必要的方法就可以。这种设计可以方便地创建被已有的具体类型满足的新接口类型而不需要改变已有的接口类型,对于接口类型是定义在其它包(别人写的、不受你控制的包)中的情况特别有用。
一个具体类型(concrete type)的值的表示(representation)、该类型支持的操作(如数字类型支持四则运算,切片类型的索引、append
和 range
操作)都是确定的。具体类型还可以通过方法提供额外的行为。拿到一个具体类型的值时,你可以明确地知道它是什么、你可以对它来做什么。
接口类型是抽象类型(abstract type)。一个接口类型的值不暴露出它的表示、内部结构或是支持的操作,你只知道它的方法提供了哪些行为。
接口定义了契约,它规定了类型的方法,同时保证满足规定后契约里约定的事项(方法)会被执行。
可替换性(substitutability,将一种类型替换成另一种满足同一个接口的类型的自由)是面向对象编程的标志。
// package fmt
func Fprintf(w io.Writer, format string, args ...interface{}) (int, error)
func Printf(format string, args ...interface{}) (int, error) {
return Fprintf(os.Stdout, format, args...)
}
func Sprintf(format string, args ...interface{}) string {
var buf bytes.Buffer
Fprintf(&buf, format, args...)
return buf.String()
}
// package io
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)
}
package main
import "fmt"
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) // 5
c = 0
var name = "sb"
fmt.Fprintf(&c, "hello, %s", name)
fmt.Println(c) // 9
}
package fmt
// The String method is used to print values passed
// as an operand to any format that accepts a string
// or to an unformatted printer such as Print.
type Stringer interface {
String() string
}
一个拥有某个接口类型指定的所有方法的具体类型是该接口类型的一个实例。
可以通过组合已有接口类型的方式(embedding an interface)定义新的接口类型。接口类型中方法的书写顺序不重要。
// package io
type Reader interface {
Read(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
// embedding
type ReadWriteCloser interface {
Reader
Writer
Closer
}
// 两种风格的混合
type ReadWriter interface {
Read(p []byte) (n int, err error)
Writer
}
一个具体类型是一个(“is a”)接口类型,意思是它实现了接口。
接口类型的赋值规则:只有实现了接口的类型(包括另一种接口类型)才可以赋值给接口类型。
var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = time.Second // missing Write method
var rwc io.ReadWriteCloser
rwc = os.Stdout
rwc = new(bytes.Buffer) // missing method Close
w = rwc
rwc = w // missing Close method
一个 T
类型的值不具有 *T
类型的所有方法。一个 T
类型的变量可以调用到 *T
类型的方法,这只是编译器的语法糖,编译器隐式地获取了该变量的地址。
type IntSet struct { /* ... */ }
func (*IntSet) String() string { return "" }
// * non-addressable IntSet value
var _ = IntSet{}.String() // cannot call pointer method on IntSet literal
var s IntSet
var _ = s.String()
var _ fmt.Stringer = &s
var _ fmt.Stringer = s // IntSet does not implement fmt.Stringer
具体类型赋值给接口类型后,只有通过接口暴露出来的方法允许被调用。
空接口类型没有暴露任何方法,需要先断言。
os.Stdout.Write([]byte("hello"))
os.Stdout.Close()
var w io.Writer
w = os.Stdout
w.Write([]byte("hi"))
w.Close() // type io.Writer has no field or method Close
任何类型都实现了空接口 interface{}
,所以可以将任意值赋值给空接口。
Go 的接口是隐式满足的。记录、断言期望的,但没有在程序中被执行的接口类型和具体类型之间关系有时很有用。
// 编译时断言 *bytes.Buffer 是实现 io.Writer 的
var _ io.Writer = (*bytes.Buffer)(nil)
// https://play.golang.org/p/jVEGU7lxAmp
flag.Value
// package flag
// Value is the interface to the value stored in a flag.
type Value interface {
String() string // 格式化命令行的帮助信息
Set(string) error // 解析命令参数,更新 flag 值
}
Interface value
接口类型的值(interface value)由两部分组成:
- 具体类型,称为接口的动态类型(dynamic type)。
- 具体类型的值,称为接口的动态值(dynamic value)。
静态类型语言的类型是编译期的概念,所以类型不是值。
在我们的概念模型中,一组称为类型描述符(type descriptor)值提供了每个类型的信息,如名称、方法。接口类型的值的动态类型部分也是通过类型描述符来表示。
var w io.Writer
// os.Stdout 是 *os.File 类型
w = os.Stdout // 等价于 w = io.Writer(os.Stdout)
w = new(bytes.Buffer)
w = nil
接口类型的零值的动态类型和动态值都是 nil。
interface value 是否是 nil 由它的动态类型决定。可以通过 w == nil
或 w != nil
来判断 interface value 是否是 nil。调用 nil interface value 的方法会 panic。
w = os.Stdout
涉及了具体类型到接口类型的类型转换,转换的结果是 interface value 的动态类型设置成 *os.File
的类型描述符,动态值是 os.Stdout
的一份拷贝。
w.Write([]byte("hello"))
:
总体来说,我们不能在编译期就知道一个 interface value 的动态类型会是什么,所以经由 interface value 的方法调用要通过动态派发(dynamic dispatch)。编译器必须生成代码用来从类型描述符中获取名称为 Write
的方法的地址,然后发起调用。
w = nil
将 w 的动态类型和动态值都置为 nil。
概念上来说,interface value 可以容纳任意大小的动态值。
interface value 可以通过 ==
和 !=
来比较(comparable)。两个 interface value 都是 nil interface(动态类型都是 nil,不论动态值是什么),或是他们的动态类型相同且动态值相等,则它们相等。
若两个 interface value 的动态类型相同,但是类型本身不可比较(如 slice),则它们之间的比较会 panic。
fmt
的 %T
动词通过反射机制拿到了 interface value 的动态类型。
debug
为 false
时,*bytes.Buffer
(结构体指针)的零值(值是 nil,类型是 *bytes.Buffer
)传入 f
,out
是动态类型为 *byte.Buffer
的 interface value,此时 out != nil
判断仍为真。对于 *bytes.Buffer
类型,nil 不是有效的 receiver,会导致 panic。
对于 *os.File
类型,nil 是有效的 receiver。
const debug = true
func main() {
var buf *bytes.Buffer
// var buf io.Writer // 这样写
if debug {
buf = new(bytes.Buffer)
}
f(buf)
if debug {
// ...use buf...
}
}
func f(out io.Writer) {
// ...do sth...
if out != nil {
out.Write([]byte("done\n"))
}
}
sort.Interface
sort
包提供原位排序功能。
package sort
type Interface interface {
Len() int
Less(i, j int) bool // i, j are indices of sequence elements
Swap(i, j int)
}
使用 sort
包的流程:
- 定义实现了 sequence 类型,实现
sort.Interface
; - 对该类型的实例使用
sort.Sort
。
type StringSlice []string
func (p StringSlice) Len() int { return len(p) }
func (p StringSlice) Less(i, j int) bool { return p[i] < p[j] }
func (p StringSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
// name := StringSlice{"Go", "C", "JavaScript", "Python"} // 简写
s := []string{"Go", "C", "JavaScript", "Python"}
name := StringSlice(s)
sort.Sort(name)
// sort.Strings(s) // sort 包自带
package sort
type reverse struct{ Interface } // 通过内嵌的匿名字段 Interface 获得 3 个方法
// 重载 Less 方法
func (r reverse) Less(i, j int) bool { return r.Interface.Less(j, i) }
func Reverse(data Interface) Interface { return reverse{data} }
type Track struct {
Title string
Artist string
Album string
Year int
Length time.Duration
}
type customSort struct {
t []*Track
less func(x, y *Track) bool
}
func (x customSort) Len() int { return len(x.t) }
func (x customSort) Less(i, j int) bool { return x.less(x.t[i], x.t[j]) }
func (x customSort) Swap(i, j int) { x.t[i], x.t[j] = x.t[j], x.t[i] }
func length(s string) time.Duration {
d, err := time.ParseDuration(s)
if err != nil {
panic(s)
}
return d
}
func printTracks(tracks []*Track) {
const format = "%v\t%v\t%v\t%v\t%v\t\n"
tw := new(tabwriter.Writer).Init(os.Stdout, 0, 8, 2, ' ', 0)
fmt.Fprintf(tw, format, "Title", "Artist", "Album", "Year", "Length")
fmt.Fprintf(tw, format, "-----", "------", "-----", "----", "------")
for _, t := range tracks {
fmt.Fprintf(tw, format, t.Title, t.Artist, t.Album, t.Year, t.Length)
}
tw.Flush() // calculate column widths and print table
}
var tracks = []*Track{
{"Go", "Delilah", "From the Roots Up", 2012, length("3m38s")},
{"Go", "Moby", "Moby", 1992, length("3m37s")},
{"Go Ahead", "Alicia Keys", "As I Am", 2007, length("4m36s")},
{"Ready 2 Go", "Martin Solveig", "Smash", 2011, length("4m24s")},
}
sort.Sort(customSort{tracks, func(x, y *Track) bool {
if x.Title != y.Title { // primary sort key
return x.Title < y.Title
}
if x.Year != y.Year { // secondary sort key
return x.Year < y.Year
}
if x.Length != y.Length { // tertiary sort key
return x.Length < y.Length
}
return false
}})
// https://play.golang.org/p/Y9kHsSL7HgB
http.Handler
package http
type Handler interface {
ServeHTTP(w ResponseWriter, r *Request)
}
func ListenAndServe(address string, h Handler) error
实际应用中,多个 case
的处理逻辑要放在独立的 handler 中处理,且会碰到相关的多个 URL 需要复用逻辑的情况(如 /images/*.png
)。
func main() {
db := database{"shoes": 50, "socks": 5}
log.Fatal(http.ListenAndServe("localhost:8000", db))
}
type dollars float32
func (d dollars) String() string { return fmt.Sprintf("$%.2f", d) }
type database map[string]dollars
func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
switch req.URL.Path {
case "/list":
for item, price := range db {
fmt.Fprintf(w, "%s: %s\n", item, price)
}
case "/price":
item := req.URL.Query().Get("item")
price, ok := db[item]
if !ok {
w.WriteHeader(http.StatusNotFound) // 404
fmt.Fprintf(w, "no such item: %q\n", item)
return
}
fmt.Fprintf(w, "%s\n", price)
default:
w.WriteHeader(http.StatusNotFound) // 404
fmt.Fprintf(w, "no such page: %s\n", req.URL)
}
}
net/http
提供的 ServeMux
是请求复用器(request multiplexer),它可以将一组 http.Handler
聚合成一个,用于简化 URL 和 handler 之间的关联。
package http
type HandlerFunc func(w ResponseWriter, r *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
func main() {
db := database{"shoes": 50, "socks": 5}
mux := http.NewServeMux()
/* db.list 是方法值(method value),本身没有方法,因此不满足 http.Handler 接口,
经过 http.HandleFunc 强制类型转换后满足接口。
*/
mux.Handle("/list", http.HandlerFunc(db.list))
// convenience method
mux.HandleFunc("/price", db.price)
log.Fatal(http.ListenAndServe("localhost:8000", mux))
}
type database map[string]dollars
func (db database) list(w http.ResponseWriter, req *http.Request) {
for item, price := range db {
fmt.Fprintf(w, "%s: %s\n", item, price)
}
}
func (db database) price(w http.ResponseWriter, req *http.Request) {
item := req.URL.Query().Get("item")
price, ok := db[item]
if !ok {
w.WriteHeader(http.StatusNotFound) // 404
fmt.Fprintf(w, "no such item: %q\n", item)
return
}
fmt.Fprintf(w, "%s\n", price)
}
//=====================
// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler that calls f.
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
// A Handler responds to an HTTP request.
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
http.HandlerFunc
是实现了http.Handler
接口的函数类型,它的ServeHTTP
方法的行为是调用类型的底层(underlying)函数。因此http.handlerFunc
是一个让一个函数值(function value)实现一个接口的适配器(adapter),其中函数和接口的唯一方法有相同的签名。
HTTP handlers 通常会不在多个文件,每一个都需要通过应用的 ServeMux
实例进行显式注册显得繁琐。net/http
提供了一个全局的 ServeMux
实例 DefaultServeMux
,和包级别(package-level)的函数 http.Handle
、http.HandleFunc
。
func main() {
db := database{"shoes": 50, "socks": 5}
http.HandleFunc("/list", db.list)
http.HandleFunc("/price", db.price)
// 传 nil 时默认使用 DefaultServeMux
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}
Go 的 web server 收到的每个请求都会在独立的协程由有对应的 handler 去处理。
error
type error interface {
Error() string
}
package errors
func New(text string) error { return &errorString{text} }
type errorString struct { text string }
func (e *errorString) Error() string { return e.text }
package fmt
import "errors"
func Errorf(format string, args ...interface{}) error {
return errors.New(Sprintf(format, args...))
}
表达式解析器
类型断言
类型断言 x.(T)
检查 interface value 的动态类型是否与断言的类型匹配。
- 若
T
是具体类型,则判断两个类型是否相同。- 若断言成功,得到的是
x
的动态值(x
的类型自然是T
),是具体类型; - 若断言失败,则 panic。
- 若断言成功,得到的是
- 若
T
是接口类型,则判断x
的动态类型是否实现了T
。- 若断言成功,得到的是 interface value,它的动态类型和动态值都不变,但 interface value 的类型变成了接口类型
T
。这种断言的效果是改变了x
的类型,使其暴露出一组不同的(通常也是更多的)方法,同时保持 interface value 的动态类型和动态值都不变。- 断言限制更小的(方法更少)接口类型的场景很少,因为通常赋值就可以解决(除了
x
是 nil 的情况)。
- 断言限制更小的(方法更少)接口类型的场景很少,因为通常赋值就可以解决(除了
- 断言失败
- 若断言成功,得到的是 interface value,它的动态类型和动态值都不变,但 interface value 的类型变成了接口类型
- 若
x
是 nil interface value,则不论去断言什么类型都会失败。
var w io.Writer
w = os.Stdout
rw := w.(io.ReadWriter)
// w 和 rw 承载的都是 os.Stdout;
// w 只暴露 Writer 方法,rw 暴露 Writer 和 Reader。
w = rw
w = rw.(io.Writer)
接口的类型决定了对外暴露的方法。interface value 的动态类型表示的具体类型赋值给接口类型以后,interface value 承载的值的类型。
若以接受两个返回值的方式进行类型断言,则断言不成功不会 panic。
var w io.Writer = os.Stdout
// 用相同的变量名 w,shadow 了原来的 w
if w, ok := w.(*os.File); ok { // ok == true
// ... use w...
}
b, ok := w.(*bytes.Buffer) // ok == false
type switches
接口类型的两种典型用法:
- 接口的方法表达了实现了该接口的具体类型的相似性,隐藏了差异;重点在方法上,而不是具体类型上。(subtype polymorphism)
- 传统接口(如
io.Reader
)的目的是隐藏实现接口的具体类型的细节,以支持新的实现的创建。每种具体类型都被会统一地处理。
- 传统接口(如
- 接口可以承载不同具体类型的值的特点,可以被当作这些具体类型的集合(union),使用类型断言可以动态地对这些类型进行不同的处理;重点在实现了接口的具体类型上。(ad hoc polymorphism)
- 按这种方式使用的接口称为 discriminated unions。
- 实现 discriminated unions 的具体类型是固定的,并且会被暴露出去而不是隐藏;discriminated unions 的方法较少;每种具体类型(通过 type switch)会按照不同的逻辑被处理。
switch x.(type) {
// switch x := x.(type) { /* ... */ } // reuse of variable name
case nil: // ...
case int, uint: // ...
case bool: // ...
case string: // ...
default: // ...
}
type switch 隐式新建了一个词法块(lexical block),新声明的变量 x
不会与外部词法块的 x
冲突。
实践接口
仅在两种及以上的具体类型需要以统一的方式处理时才需要定义接口,只有一种实现的接口的不必要的抽象,同时也会带来运行时的负载(run-time cost)。
所以不要实现定义一堆接口,再去定义实现接口的具体类型。
这一规则的一个例外:若一个接口只有一种具体类型实现,但这一具体类型因为依赖的缘故不能和接口放在同一个包中,此时定义一个接口是将两个包解耦的好途径。
需要限制方法或字段的可见性可以使用 export 机制。
小的接口(方法少)可以很容器地被新的类型实现。设计接口的宗旨是只要求你所需的方法(ask only for what you need)。