接口类型表示对其他类型的行为的概括或抽象。通过抽象,接口让我们可以编写更灵活和适应性更强的函数,因为接口类型与特定实现的细节无关。
Go语言的接口与其他语言中的接口的区别是:它满足隐式实现【satisfied implicitly】
换句话说,不需要声明给定具体类型满足的所有接口;仅仅拥有必要的方法就足够了。这种设计允许您在不更改现有类型的情况下创建由现有具体类型所满足的新接口,这对于在包中定义的您无法控制的类型特别有用。
换句话说,我们不需要为给定的具体类型定义其所满足的所有接口;简单的拥有一些必要的方法即可
7.1接口约定
到目前为止,我们所见的都是具体的类型。具体的类型指定其值的精确表示,并公开该类型的一些内部操作,例如数字运算,或对切片的索引、append和range。一个具体的类型可以通过其方法来提供额外的行为。当您有一个具体类型的值时,您确切地知道它是什么以及您可以使用它做什么。
Go中还有另外一种类型,称为接口类型【 interface type】,接口是一个抽象类型【abstract type】。它不公开其值的表示形式或内部结构,或它们所支持的基本操作集;它只暴露他们的的一些方法。当你有一个接口类型的值时候,你并不知道它是什么,你只知道它能做什么,或者更准确地说,即它的方法提供了什么行为。
7.2 接口类型
接口类型指定一组方法,具体类型必须具有这些方法才能被认为是该接口的实例。
io.Writer类型是使用最广泛的接口之一,因为它提供了可以写入字节的所有类型的抽象,这些类型包括文件、内存缓冲区、网络连接、HTTP客户机、归档程序、散列表等等。
type Writer interface {
Write(p []byte) (n int, err error)
}
再进一步的,我们发现新接口类型的声明可以是现有接口类型的组合。
这里有两个例子:
type ReadWriter interface {
Reader
Writer
}
上面的语法与结构体内嵌相似,我们可以使用这种方式,以简写的形式命名一个接口,而不需要重复的声明其所有的方法(类似于Java中的继承)。这种方式称为接口内嵌。
当然那我们亦可以不通过接口内嵌来声明这个接口:
//重复声明型
type ReadWriter interface {
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
}
//混合型
type ReadWriter interface {
Reader
Write(p []byte) (n int, err error)
}
7.3 接口实现
如果某类型拥有某个接口所需的所有方法,那么该类型就实现/满足了该接口。
接口的分配规则:只有当表达式的类型实现/满足接口时,才可以将表达式分配给该接口:
var ios io.Writer
ios = os.Stdout
ios = new(bytes.Buffer)
ios = time.Second //error! time.Second类型并未实现io.Writer
对于具名类型T,它的一些方法的接收器是T类型本身,而有的方法的接收器是T的指针。在类型为T的参数上调用接收器是T的方法是合法的,只要该参数是变量即可;编译器隐式地获取它的地址。但这只是一个语法糖。T类型的值并不会处理接收器是T的方法。因此T类型可能满足的接口类型会更少(因为还有的很多方法的接收器是*T)。
type IntSet struct {
Cap int
Len int
}
//该方法接收器是指针
func (is *IntSet) String() string {
return fmt.Sprintf("Cap = %d,Len = %d",is.Cap,is.Len)
}
func main(){
var is interfaces.IntSet
is = interfaces.IntSet{Cap:10,Len:10}
format := is.String()
fmt.Println(format)
//format = interfaces.IntSet{}.String() //erro
var _ fmt.Stringer = &is //String()方法的接收器是*T,因此&IntSet实现了fmt.Stringer
var _ fmt.Stringer = is //error ! 但是IntSet并未实现fmt.Stringer
}
接口包装和隐藏了真实类型及其持有的值,只能调用接口类型所揭露的方法,即使具体的类型有其他方法。
interface{}没有任何方法,也被称为空接口【 empty interface】类型。因为空接口类型对于实现它的类型没有任何要求,因此我们可以将任意类型赋值给空接口类型。
var any interface{}
any = 10
any = "123"
any = interfaces.IntSet{Cap:10,Len:10}
由于接口实现仅依赖于所涉及的这两个类型的方法,因此无需声明具体类型与其满足的接口之间的关系。
下面的声明语句在编译期间断言 *bytes.Buffer类型的值实现/满足io.Writer:
// *bytes.Buffer must satisfy io.Writer
var w io.Writer = new(bytes.Buffer)
我们不需要分配一个新的变量,因为任何 bytes.Buffer 类型的值,甚至显示使用(*bytes.Buffer)(nil)转换了的nil,都实现了该接口。
一个具体的类型可能会实现了很多不相关的接口。考虑这么一个程序,他是一个在线销售数字化文化产品如书、电影等:
Album
Book
Movie
Magazine
Podcast
TVEpisode
Track
我们可以将每个感兴趣的抽象表示为一个接口。一些特征对于所有的工件都是通用的,例如标题、创作日期和创作者列表(作者或艺术家)。
type Artifact interface {
Title() string
Creators() []string
Created() time.Time
}
还有一些特性只对特定类型文化产品才有用。比如和book/Magazine相关的文字打印问题,和TVEpisode相关的分辨率等。
type Text interface {
Pages() int
Words() int
PageSize() int
}
type Audio interface {
Stream() (io.ReadCloser, error)
RunningTime() time.Duration
Format() string // e.g., "MP3", "WAV"
}
type Video interface {
Stream() (io.ReadCloser, error)
RunningTime() time.Duration
Format() string // e.g., "MP4", "WMV"
Resolution() (x, y int)
}
这些接口只是将相关的具体类型组合在一起,并表达它们共有的方面的一种有用方法。如果我们发现我们需要以相同的方式处理Audio和Video,我们可以定义一个Streamer来表示他们的共同之处,而不会改变任何既有的类型。
type Streamer interface {
Stream() (io.ReadCloser, error)
RunningTime() time.Duration
Format() string
}
任何一个基于它们共同行为的具体类型分组,都可以表示为一个接口类型。与基于类的语言不同,在Go中,我们可以在需要时定义新的抽象或构建感兴趣的内容,而无需修改具体类型的声明。当具体类型来自不同作者编写的包时,这一点特别有用。当然,这些具体类型之间确实需要有潜在的共性。
7.5 接口值
概念上讲,接口类型的值,或者说是接口值【interface value】,它由两个组件构成,一个是具体类型的,另一个是该类型的值。它们也被称为动态类型【dynamic type】和动态值【dynamic value】.
对于像Go语言这种静态类型的语言而言。类型是一个编译期的概念,所以类型并不是一个值。一组称为类型描述符[type descriptors]的值提供了关于每种类型的信息,比如它的名称和方法。在接口值【interface value】中,类型组件则是由适当的类型描述符来表示的。
var ios io.Writer
ios = os.Stdout
ios = new(bytes.Buffer)
ios = nil
- var ios io.Writer
Go中的变量总是初始化为一个定义良好的值,接口也不例外。接口的零值对应的类型组件和值组件都是nil.如下
一个接口值根据其动态类型而被描述为nil或非nil,因此这是一个nil接口值,我们可以使用ios == nil或者ios != nil来判断。对nil接口值调用任何方法都会引起恐慌。
2. ios = os.Stdout
这个赋值涉及到从具体类型到接口类型的隐式转换,等效于显方式的转换:ios = io.Writer(os.Stdout)
这种转换无论是显式还是隐式,都会获取其操作数的类型和值,该接口值的动态类型【dynamic type】设置为*os.File指针类型的类型描述符,而动态值【dynamic value】持有的则是os.Stdout的拷贝,该拷贝是一个指向os.File变量的指针,表示进程的标准输出
当我们在包含*os.File指针的接口值【interface value】,上调用write方法时,会引发对(*os.File).Write被调用
w.Write([]byte("hello")) // "hello"
通常在编译期,我们无法知道接口值【interface value】的动态类型所指,因此在接口上的调用必须是动态分发【dynamic dispatch】的。因为这不是直接调用,编译器必须生产代码从类型描述符中获取名为Write的方法的地址,然后向该地址发起间接调用。该调用的接收器实参是接口的动态值【dynamic value】的拷贝,即os.Stdout
3. w = new(bytes.Buffer)
动态值目前是*bytes.Buffer,而动态类型则是新分配的缓冲区的指针:
- w = nil
该赋值操作使得接口值得组件均变为nil,恢复为初始化状态。
一个接口值可以持有任意大的动态值【dynamic value】。例如time.Time类型,它代表的事一个瞬间的时间,它是一个结构体,包含多个非导出的属性,如果创建一个接口值:
var x interface{} = time.Now()
从概念上讲,不管它的类型有多大,动态值【dynamic value】总可以容纳下。(这只是一个概念的模型;现实的实现是非常不同的)
接口值可以使用== 或者 !=来比较。
相等的条件:
- 要么两个接口值均是nil
- 或者两个接口值的动态类型相同,且动态值通过==比较亦相等
因为接口值具有可比较性,因此可以作为map的key或者switch语句的操作值
PS:如果两个待比较的接口值的动态类型是一种无法比较的类型,例如slice,那么比较会引发恐慌
var x interface{} = []int{1, 2, 3}
fmt.Println(x == x) // panic: comparing uncomparable type []int
在调试Bug时候,我们可以使用fmt包的%T来打印接口值的动态类型:
var x interface{} = []int{1, 2, 3}
fmt.Printf("%T",x) //interfaces.IntSet[]int
7.5.1 警告:包含nil指针的接口是非nil
一个不包含任何值得nil接口值,不等于一个包含nil指针的接口值的:
func main(){
var buf*bytes.Buffer //包含nil指针的接口值
Writes(buf) //panic: runtime error: invalid memory address or nil pointer dereference
}
func Writes(out io.Writer) {
if out != nil {
out.Write([]byte("done!"))
}
}
当main函数执行调用Writes函数时候,会将bytes.Buffer类型的nil指针分配给Writes函数的out入参,因此out它的动态值是nil,动态类型则是bytes.Buffer。这意味着out并不是一个nil接口,它包含一个nil指针的值,因此out != nil条件判断的结果是true,如下图:
动态分发机制决定了(bytes.Buffer).Write必然会被调用,但是接收器确是nil。虽然对于某些类型,例如os,File,nil是一个有效的接收器。但是*bytes.Buffer不这样。这个方法会被地阿偶用,但是当访问缓冲区时候回引起恐慌。
原因是尽管 一个nil的*bytes.Buffer指针实现了接口,但是它并不满足这个接口行为上的需要。特别是这个调用违反了(*bytesBuffer).Write方法的接收器非空的隐含先决条件,因此将nil指针赋给这个接口是错误的。
解决方法是将buf的类型改为io.Writer,因此可以避免在一开始就将一个不正常的值赋值给这个接口。
7.6 sort.Interface
Go中的排序抽象是sort.Interface,该接口共有三个方法:
- Len () int
- Less (i,j int) bool
- Swap (i,j int)
当需要为某个序列执行排序时候,我们只需要定义一个实现了这三个函数的类型,然后将其作为sort.Sort方法的入即可。
例如一个简单的字符串排序:
type StringSlice []string
func (ss StringSlice) Len() int {
return len(ss)
}
func (ss StringSlice) Less(i, j int) bool {
x1 := ss[i]
x2 := ss[j]
return x1 < x2
}
func (ss StringSlice) Swap(i, j int) {
ss[j],ss[i] = ss[i],ss[j]
}
7.7 http.Handle
package http
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
func ListenAndServe(address string, h Handler) error
ListenAndServe 函数需要
- 一个服务地址,例如“localhost:8080”
- 以及一个会分发所有的请求的http.Handler类型的实例
我们来实现一个简单的电子商务系统,它使用一个数据库系统,在商品与价格之间做映射,准备工作如下:
type RMB float32 //货币类型
func (rmb RMB) String() string { return fmt.Sprintf("¥%.2f", rmb) }
type Database map[string]RMB //简版的数据库系统
//为数据库系统增加HTTP服务能力
func (database Database) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
switch req.URL.Path {
case "/list":
for item,price := range database {
fmt.Fprintf(rw, "%s: %s\n", item, price)
}
case "/price":
item := req.URL.Query().Get("item")
price, ok := database[item]
if !ok {
//rw.WriteHeader(http.StatusNotFound) // 404,注意必须在写入errorMsg之前先写Http状态
//fmt.Fprintf(rw, "no such item: %q\n", item)
//等同于下面的形式
error := fmt.Sprintf("no such item: %q\n", item);
http.Error(rw,error,http.StatusNotFound)
return
}
fmt.Fprintf(rw, "%s\n", price)
default:
rw.WriteHeader(http.StatusNotFound) // 404
fmt.Fprintf(rw, "no such page: %s\n", req.URL)
}
}
启动HTTPServer:
db := interfaces.Database{"java in action":18.4,"apache flink":22}
log.Fatal(http.ListenAndServe("localhost:8080",db))
7.8 Error接口
type error interface {
Error() string
}
创建一个error的的方式:
- errors.New
- fmt.Errorf
e := errors.New("my error msg")
如下便是errors包的所有代码:
package errors
// New returns an error that formats as the given text.
func New(text string) error {
return &errorString{text}
}
// errorString is a trivial implementation of error.
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
底层的errorString是结构体类型,而不是string类型,保护其所代表的值不受无意的(或有预谋的)更新的影响。而为什么New函数返回的是指针类型errorString,而不仅仅是errorString,是因为errorString实现了error接口的方法,而不是errorString。因此每次对New函数的调用都会分配一个不同的错误实例,彼此不相等。因为我们不希望区分不出像io.EOF等这样的error与仅仅是消息相同的其他errorr 。
fmt.Println(errors.New("EOF") == errors.New("EOF")) // "false"
尽管*errorString可能是最简单的错误类型,但它绝不是唯一的错误类型。例如,syscall包提供了Go的低级系统调用API。在多数平台上,Go定义了一个满足/实现了error接口的数值类型Errno。在Unix平台上,Errno ’s Error方法会在如下这样的字符串表中进行查找:
package syscall
type Errno uintptr
var errors = [...]string{
1: "operation not permitted", // EPERM
2: "no such file or directory", // ENOENT
3: "no such process", // ESRCH
// ...
}
func (e Errno) Error() string {
if 0 <= int(e) && int(e) < len(errors) {
return errors[e]
}
return fmt.Sprintf("errno %d", e)
}
下面的语句创建了一个Errno:
var err error = syscall.Errno(2)
fmt.Println(err.Error()) // "no such file or directory"
fmt.Println(err) // "no such file or directory"
下面是err接口值的组件图:
Errno是对从有限集中提取的系统调用错误的有效表示,它满足/实现了标准的error接口。
7.10 类型断言
类型断言是一个应用于接口值得操作。
语法上讲,看起来像:x.(T)
x是一个代表接口值的表达式,而T则是我们断言的类型。
类型断言会检查操作值的动态类型是否匹配于断言类型:
- 当断言类型T是一个具体类型时,类型断言会姜茶x的动态类型是否等于T。如果检查成功,那么类型断言的结果,其值为x的动态值,而类型则当然就就是T了。换句话说,针对具体类型的类型断言,会从其从操作数中提取其真实的值。如果检查失败,则会恐慌:
var writes io.Writer
writes = os.Stdout
f := writes.(*os.File) //success, f == os.Stdout
c := writes.(*bytes.Buffer) //panic,interface holds *os.File, not *bytes.Buffer
- 如果断言类型T是一个接口类型,那么类型断言会检查x的动态类型是否满足/实现了T。如果检查成功,动态值并不会被抽取;结果仍然是一个接口值,动态值和动态类型不改变。换句话说,接口类型的类型断言改变了表达式的类型,使一组不同(通常更大)的方法变得可访问,但它保留了接口值中的动态类型和值组件。
var w io.Writer //表达式类型是 io.Writer
w = os.Stdout
fmt.Printf("%T \n",w) // *os.File
rw := w.(io.ReadWriter) // success: *os.File has both Read and Write,rw表达式类型变为ReadWriter
fmt.Printf("%T",rw) // *os.File
经过类型断言后,w和rw均持有 os.Stdout,因此动态类型均为*os.File ,但是w的表达式类型是io.Writer,值对外暴露了Write方法,而rw的表达式类型是ReadWriter,则其所暴露的方法是Write和Read.
PS:如果操作数是nil接口值,那么任何断言都将失败
一版情况,我们都是向上断言,向下断言虽然不会出错,但是我们可以使用更简的赋值语句来实现:
w = rw // io.ReadWriter被分配给io.Writer,此时w的表达式类型是io.Writer,而rw的表达式类型是io.ReadWriter
w = rw.(io.Writer) //等同于赋值语句
类型断言亦可以有一个元组返回值:
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
当类型断言失败时,使用如上的形式,则不会触发panic,而是返回一个额外的第二个结果,表示成功与否。如果类型断言失败,那么ok = false,而首属性的值则是断言类型T的零值,例子中则是一个nil *bytes.Buffer。
当ok结果直接用于决定下一步做什么时候,那么我可以使用下面的简单语法:
if f, ok := w.(*os.File); ok {
// ...use f...
}
如果类型断言的操作数是一个变量,而不是为新的局部变量创建另一个名称,您有时会看到原来的名称被重用,遮盖掉原来的,如下所示:
if w, ok := w.(*os.File); ok {
// ...use w...
}
7.11 用类型断言识别错误
考虑os包中文件操作返回的错误集。I/O可能因为任何原因而失败,但是有三种失败通常必须以不同的方式处理:
- 文件已经存在(对于创建操作),
- 文件未找到(对于读操作),
- 拒绝访问。
os包提供了3个帮助办法,同于根据给定的error值对故障进行分类:
package os
func IsExist(err error) bool
func IsNotExist(err error) bool
func IsPermission(err error) bool
这些谓词的最简单的实现可以是去检查错误消息是否包含某个子字符串:
func IsNotExist(err error) bool {
// NOTE: not robust!
return strings.Contains(err.Error(), "file does not exist")
}
但是,由于处理I/O error的逻辑在不同的平台之间可能会有所不同,因此这种方法不够健壮,同样的失败可能会被报告为各种不同的错误消息。在测试期间,检查错误消息的子字符串可能很有用,以确保函数以预期的方式失败,但它不适用于生产代码。
更可靠的方法是使用专用类型表示结构化的error值。os包定义了一个名为PathError的类型,用于描述在涉及文件路径上的操作(如打开或删除)的故障,以及一种名为LinkError的变体,用于描述涉及两个文件路径的操作(如Symlink和Rename)的故障。下面是os.PathError:
pakage os
// PathError records an error and the operation and file path that caused it.
type PathError struct {
Op string
Path string
Err error
}
func (e *PathError) Error() string {
return e.Op + " " + e.Path + ": " + e.Err.Error()
}
大多数客户端都不关心 PathError,通过调用Error方法以统一的方式处理所有的error。虽然PathError的Error方法通过简单地拼接字段来组成一条错误消息,但通过PathError的结构可以看出,它保留了error的底层组件。需要在各种error之间做区分的客户端,可以使用类型断言来检测error的特定类型;特定类型比简单的字符串提供更多的信息。
_, err := os.Open("/no/such/file")
fmt.Println(err)
fmt.Printf("%#v\n", err)
log日志:
*os.Fileopen /no/such/file: The system cannot find the path specified.
&os.PathError{Op:"open", Path:"/no/such/file", Err:0x3}
这就是三个帮助函数的工作原理。例如,下面显示的IsNotExist报告error是否等于syscall.ENOENT(§7.8)或os.ErrNotExist,或者是一个* PathError,其底层的error是这二者之一。
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
}
下面是如何使用:
_, err := os.Open("/no/such/file")
fmt.Println(os.IsNotExist(err)) // "true"
当然,如果错误消息被合并成一个更大的字符串,例如通过调用fmt.Errorf,就会丢失PathError的结构。错误识别通常必须在操作失败后立即执行,然后将错误传播给调用者。
7.12 使用接口类型断言查询行为
下面的逻辑与负责编写HTTP header头属性(如“Content-type: text/html”)的net/http web服务器部分类似。io.Writer w 表示HTTP响应;写入它的字节最终被发送到某人的web浏览器上。
func writeHeader(w io.Writer, contentType string) error {
if _, err := w.Write([]byte("Content-Type: ")); err != nil {
return err
}
if _, err := w.Write([]byte(contentType)); err != nil {
return err
}
// ...
}
因为Write方法需要一个byte切片作为参数,而我们待写入的是一个字符串,因此需要[]byte(…)来做转换。该转换会分配一段内存,并产生一个拷贝,但是该拷贝几乎立即就被扔掉了。我们假设这是web服务器的核心部分,我们的分析显示,内存分配正在减慢服务器的速度。那么我们可以避免内存分配吗??
io.Writer接口只告诉我们关于w持有的具体类型的一个事实:字节可能被写入它。如果我们看一下net/http包的幕后,我们会发现w在这个程序中持有的动态类型中也有WriteString方法,它允许高效地将字符串写到它,避免了分配临时拷贝的需要。(这看起来像是瞎猜,但有一些满足/实现了io.Writer接口的重要的类型还有一个WriteString方法,这些类型包括 *bytes.Buffer , os.File和bufio.Writer)。
我们不能假设一个任意的io.Writer w也有WriteString方法。但是我们可以定义一个只有这个方法的新接口,并使用类型断言来测试w的动态类型是否满足/实现了这个新接口。
// writeString writes s to w.
// If w has a WriteString method, it is invoked instead of w.Write.
func writeString(w io.Writer, s string) (n int, err error) {
type stringWriter interface {
WriteString(string) (n int, err error)
}
if sw, ok := w.(stringWriter); ok {
return sw.WriteString(s) // avoid a copy
}
return w.Write([]byte(s)) // allocate temporary copy
}
func writeHeader(w io.Writer, contentType string) error {
if _, err := writeString(w, "Content-Type: "); err != nil {
return err
}
if _, err := writeString(w, contentType); err != nil {
return err
}
// ...
}
为了避免重复,我们将该检查移动到通用函数writeString中,但是它非常有用,标准库将它作为io.WriteString提供了出来。这是向io.Writer写入字符串的推荐方式。
这个示例中令人好奇的是,没有标准接口定义WriteString方法并指定其所需的行为。此外,一个具体的类型是否满足stringWriter接口只取决于它的方法,而不取决于它与接口类型之间声明的任何关系。这意味着,上面的技术依赖于这样的假设:如果一个类型满足/实现了下面的接口,那么WriteString(s)的效果必须与Write([]byte(s))相同。
interface {
io.Writer
WriteString(s string) (n int, err error)
}
尽管io.WriteString文档化了它的设想,很少有调用它的函数会记录它们也做了同样的设想。定义特定类型的方法被视为是对某种行为约定的隐式同意。新学习Go的开发者,尤其是那些有强类型语言背景的开发者,可能会发现这种缺乏明确的关系声明会令人不安,但在实践中起始很少出现问题。除了空接口interface{}之外,接口类型很少会因意外的巧合而被实现/满足。
上面的writeString函数使用类型断言来查看通用接口类型的值是否也实现了更特定的接口类型,如果是,则使用特定接口的行为。无论被查询的接口是否像io.ReadWriter那样是标准的,或者是用户自定义的类似于stringWriter的类型,这种技术都可以得到很好的应用。
这就是fmt。Fprintf可以区分error以及fmt.Stringer的原因。在fmt.Fprintf中,有一个步骤可以将一个操作数转换成一个字符串,像这样:
package fmt
func formatOneValue(x interface{}) string {
if err, ok := x.(error); ok {
return err.Error()
}
if str, ok := x.(Stringer); ok {
return str.String()
}
// ...all other types...
}
如果x满足这两个接口中的任何一个,就决定了值的格式。如果不是,默认情况下需要使用反射或多或少地统一处理所有其他类型。
同样,这也假设任何具有String方法的类型都满足了fmt.Stringer的行为约定,而该方法会返回一个适合打印的字符串。
7.13 类型切换
接口以两种不同的方式使用:
- 第一种风格,以io.Reader ,io.Writer ,fmt.Stringer , sort.Interface ,http.Handler ,以及error为代表。interface的方法表达了实现了该接口的具体类型的相似之处,但隐藏了这些具体类型的表示细节和内部操作。重点是方法,而不是具体的类型。
- 第二种风格利用接口值可以持有各种具体类型的值的能力(接口是这些类型的组合)。类型断言用于动态区分这些类型,并以不同的方式对待每种情况。在这种风格中,重点是实现了接口的具体类型,而不是接口的方法(如果确实有的话),并且没有信息隐藏。我们称这种方式使用的接口为“discriminated unions【识别联合】”
如果您熟悉面向对象编程,您可能会将这两种样式识别为子类型多态性【subtype polymorphism】和重载【ad hoc polymorphism】,但是您不需要记住这些术语。在本章的其余部分,我们将展示第二种风格的例子。
Go为查询SQL数据库而开发的API,与其他语言差不多,让我们能够清晰地将查询的固定部分与变量部分分离开来。示例客户端可能如下所示:
import "database/sql"
func listTracks(db sql.DB, artist string, minYear, maxYear int) {
result, err := db.Exec(
"SELECT * FROM tracks WHERE artist = ? AND ? <= year AND year <= ?",
artist, minYear, maxYear)
// ...
}
Exec方法将查询字符串中的“?”替换为表示其对应值的SQL字面量,该值可以是布尔值、数字、字符串或nil。以这种方式构造查询有助于避免SQL注入攻击,在这种攻击中,其中攻击者通过利用输入数据的不正确引用来控制查询。在Exec中,我们可能会找到如下所示的函数,它将每个参数值转换为SQL表示法的字面量。
func sqlQuote(x interface{}) string {
if x == nil {
return "NULL"
} else if _, ok := x.(int); ok {
return fmt.Sprintf("%d", x)
} else if _, ok := x.(uint); ok {
return fmt.Sprintf("%d", x)
} else if b, ok := x.(bool); ok {
if b {
return "TRUE"
}
return "FALSE"
} else if s, ok := x.(string); ok {
return sqlQuoteString(s) // (not shown)
} else {
panic(fmt.Sprintf("unexpected type %T: %v", x, x))
}
}
switch语句简化了执行一系列值相等测试的if-else链。类似的类型切换语句简化了类型断言的if - else链。
在最简单的形式中,类型切换看起来像一个普通的switch语句,其中操作数是x.(type)——字面上是关键字类型——每种case可以有一个或多个这种类型。类型切换支持基于接口值的动态类型的多路分支。如果x == nil,则nil匹配,如果没有其他情况匹配,则default被匹配。sqlQuote的类型切换将具有以下情况:
switch x.(type) {
case nil: // ...
case int, uint: // ...
case bool: // ...
case string: // ...
default: // ...
}
与一个普通的switch语句(§1.8),case被认为是顺序的,当找到匹配项,case的代码体被执行。当一个或多个case所匹配的类型是接口类型时,case的顺序变得非常重要,因为有可能出现两个case均匹配。default case的位置无关紧要,它总是会在其他case均不匹配的条件下,才会被执行。
Go的switch还有一个fallthrough关键字,它可以强制执行后面的case代码,fallthrough不会判断下一条case的expr结果是否为true。
需要注意的是,在原始的函数中,bool和string case的逻辑是需要访问类型断言抽取出的值得。由于这很典型,type switch语句有一种扩展形式,它将抽取出的值绑定到每种case下的新变量:
switch x := x.(type) { /* ... */ }
这里我们也叫新变量为x;与类型断言中使用的一样,在这里,变量名的重用也很常见。与switch语句一样,类型切换隐式地创建一个词法块,因此新变量x的声明不会与外部块中的变量x冲突。每种case还隐式地创建一个单独的词法块。
我们以这种拓展形式,重写之前的sqlQuote:
func sqlQuote(x interface{}) string {
switch x := x.(type) {
case nil:
return "NULL"
case int, uint:
return fmt.Sprintf("%d", x) // x has type interface{} here.
case bool:
if x {
return "TRUE"
}
return "FALSE"
case string:
return sqlQuoteString(x) // (not shown)
default:
panic(fmt.Sprintf("unexpected type %T: %v", x, x))
}
}
在这个版本中,在每个单类型的case的块中,变量x具有与case相同的类型。例如,x在bool case中有则是bool类型,在string case中则是string类型。在所有其他的case下,x则是switch的操作数的类型,在本例中则为interface{}。当多个case(如int和uint)使用的是相同的操作时,类型切换使得组合它们变得很容易。
虽然sqlQuote接受任何类型的参数,但是只有当参数的类型匹配到类型切换中的一种case时,函数才能运行到完成;否则,它会恐慌。虽然x的类型是interface{},但我们认为它是int、uint、bool、string和nil的“discriminated unions【识别联合】”