接口
接口类型是对其他类型行为的概括与抽象。通过使用接口,我们可以写出更加灵活和通用的函数,这些函数不用绑定在一个特定类型的实现上。
很多面向对象的编程语言都有接口的概念,Go语言的接口的独特之处在于它是隐式实现的。换句话说,对于一个具体的类型,无需声明它实现了哪些接口,只要提供接口所必需的方法即可。这种设计让你无需改变已有的类型的实现,就可以为这些类型创建新的接口,对于那些不能修改包的类型,这一点特别有用。
声明接口并实现
声明一个Car
接口,并new
出三种车辆去实现这个接口,代码如下:
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`package main
import (
"fmt"
)
type Car interface {
run()
}
type BuckCar struct {
}
type AudiCar struct {
}
type BMWCar struct {
}
func (buck BuckCar) run() {
fmt.Println("I am BuckCar, I can run!")
}
func (audicar AudiCar) run() {
fmt.Println("I am AudiCar, I can run!")
}
func (bmwCar BMWCar) run() {
fmt.Println("I am BMWCar, I can run!")
}
func main() {
var car Car
car = new(BuckCar)
car.run()
car = new(AudiCar)
car.run()
car = new(BMWCar)
car.run()
}` </pre>
接口即约定
接口是一种抽象类型,它并没有暴露所含数据的布局或者内部结构,当然也没有那些数据的基本操作,它所提供的仅仅是一些方法而已。
我们平时使用较多的fmt.Printf()
和fmt.Sprintf()
,前者负责把结果发到标准输出(标准输出其实是一个文件),后者把结果以string
类型返回。格式化是这两个函数中最复杂的部分,这两个函数的很类似,但是是有差异的,但是如果因为这一点差异就把这两个函数的格式化重新写一遍,那就太繁琐了!
恰好接口机制就可以解决这个问题。下图就是,Go SDK关于fmt.Printf()
和fmt.Sprintf()
,封装fmt.Fprintf()
的代码。
Fprintf
的前缀F
指文件,表示格式化的输出会写入第一个实参所指代的文件。对于Pringtf
,第一个实参就是os.Stdout
,它属于*os.File
类型。对于Sprintf
,尽管第一个实参不是文件,但它模拟了一个文件:&buf
就是一个指向内存缓冲区的指针,与文件类似,这个缓冲区也可以写入多个字节。
io.Writer
接口定义了Fprintf
和调用者之间的约定。一方面,这个约定要求调用者提供的具体类型(比如:*os.File
或者*byte.Buffer
)包含一个与其签名和行为一致的Writer
方法。另一方面,这个约定保证了Fprintf
能使用任何满足io.Writer
接口的参数。Fprintf
只需要能调用参数的Write
函数,无需假设它写入的是一个文件还是一段内存。因为fmt.Fprintf
仅依赖io.Writer
接口约定的方法,对参数的具体类型没有要求。
接口类型
一个接口类型定义了一套方法,如果一个具体类型要实现该接口,那么必须实现接口类型中的所有方法。
io.Writer
是一个广泛使用的接口,它负责所有可以写入字节的类型抽象,包括文件、内存缓冲区、网络连接、HTTP客户端、打包器(archiver)、散列器(hasher)等。io
包还定义了很多接口。Reader
就抽象了所有可以读取字节的类型,Closer
就抽象了所有可以关闭的类型,比如文件或者网络连接。
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`package io
type Reader interface{
Read(p []byte)(n int,err error)
}
type Closer interface{
Closer() error
}` </pre>
另外,我们还可以发现通过组合一有借口得到新的接口,比如:
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`type ReadWriter interface{
Reader
Writer
}
type ReadWriteClooser interface{
Reader
Writer
Closer
}` </pre>
如上的语法称为嵌入式接口,与嵌入式结构类似,让我们可以直接使用一个接口,而不用逐一写出这个接口所包含的方法。
实现接口
如果一个类型实现了一个接口要求的所有方法,那么这个类型实现了这个接口。
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`package main
import (
"bytes"
"io"
"os"
"time"
)
var w io.Writer
func main() {
w = os.Stdout // OK os.File有write方法
w = new(bytes.Buffer) // OK /bytes.Buffer有write方法
w = time.Second // 编译错误,time.Duration缺少write方法
}` </pre>
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`package main
import (
"bytes"
"io"
"os"
)
var rwc io.ReadWriteCloser
func main() {
rwc = os.Stdout //OK io.ReadWriteCloser有write方法
rwc = new(bytes.Buffer) // 编译错误,bytes.Buffer缺少Close方法
}` </pre>
当右侧表达式也是一个接口时,该规则也有效:
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`package main
import (
"bytes"
"io"
"os"
"time"
)
var w io.Writer
var rwc io.ReadWriteCloser
func main() {
w = os.Stdout // OK os.File有write方法
w = new(bytes.Buffer) // OK /bytes.Buffer有write方法
w = time.Second // 编译错误,time.Duration缺少write方法
rwc = os.Stdout //OK io.ReadWriteCloser有write方法
rwc = new(bytes.Buffer) // 编译错误,bytes.Buffer缺少Close方法
w =rwc // OK io.ReadWriteCloser有write方法
rwc = w //编译错误,io.Writer没有Close方法
}` </pre>
对于每一个具体类型T而言,部分方法的接收者就是T,而其他方法的接收者就是*T
指针。同时我们对类型T的变量直接调用*T
的方法也可以是合法的,只要改变量是可变的,编译器隐式地帮助我们完成了取地址的操作。但这个仅仅是语法糖,类型T的方法没有对应的*T
多,所以实现的接口也可能比对应的指针少。
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">type IntSet struct{/*...*/} func (*IntSet) String() string var _ = IntSet{}.String() // 编译错误,String方法需要
*IntSet接收者
</pre>
但是可以从一个IntSet
变量上调用该方法:
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">var s IntSet var _ = s.String() // OK s是一个变量,&s有String()
</pre>
因为只有*IntSET
有String()
,所以也只有*IntSet
实现了fmt.Stringer
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">var _ fmt.Stringer = &s // OK var _ fmt.Stringer = s // 编译错误: IntSet缺少String方法
</pre>
就像信封封装了信件,接口也封装了对应的类型和数据,只有通过接口暴露的方法才可以调用,类型的其他方法则无法通过接口来调用:
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`os.Stdout.Write([]byte("hello")) //OK: os.File有Write()
os.Stdout.Close() //os.File有Close方法
var w io.Writer
w = os.Stdout
w.Write([]byte("hello")) // OK: io.Writer有Write方法
w.Close() // 编译错误: io.Writer 缺少close方法` </pre>
因为空接口类型对其实现类型没有任何要求,所以我们可以把任何值赋给空接口类型,就像下面这样:
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">var any interface{} any = 123 any = "mua~" any = map[string]int{"one"P:1} any = new(bytes.Buffer)
</pre>
使用flag.Value()来解析参数
flag.Value
是Go语言里面的标准接口。
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`var peroid = flag.Duration("peroid",3*time.Second,"sleep.peroid")
func main(){
flag.Parse()
fmt.Printf("我睡着了~ Sleeping for %v...",peroid)
time.Sleep(peroid)
fmt.Println("我睡醒了~")
}` </pre>
默认的睡眠时间是3s,但是可以通过 -peroid
命令行来控制。
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">./sleep -peroid 20ms Sleeeping for 50ms...
</pre>
接口值
从概念上来讲,一个接口类型的值(简称接口值)其实有两部分:一个具体类型和该类型的一个值。二者称为接口的动态类型和动态值。对于像Go这样的静态类型语言,类型仅仅是一个编译时的概念,所以类型不是一个值。
如下四个语句中,变量w有三个不同的值(第一个和第三个是同一个值):
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">var w io.writer w = os.Stdout w = new(bytes.Buffer) w = nil
</pre>
接下来让我们详细查看一下在每个语句之后w的值和相关的动态行为。
- 第一个语句声明了w:
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">var w io.Writer
</pre>
在Go语言中,变量总是初始化为一个特定的值,接口也不例外。接口的零值就是把它的动态类型和值都设为nil
。
一个接口值是否是nil
取决于它的动态类型,所以现在这是一个nil
接口值。可以使用w == nil
或者w != nil
来检测一个接口值是否是nil
。调用一个nil
接口的任何方法都会导致崩溃:
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">w.Write([]byte("hello")) //崩溃: 对空指针取引用值
</pre>
- 第二个语句把一个
*os.File
类型赋值给了w:
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">w = os.Stdout
</pre>
这个赋值把一个具体类型隐式转换为一个接口类型,它与对应的显式转换io.Writer(os.Stdout)
等价。不管这种类型的转换是隐式地还是显式的,它都可以转换操作数的类型和值。
- 第三个语句把一个
*bytes.Buffer
类型的值赋值给了接口值:
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">w = new(bytes.Buffer)
</pre>
动态类型现在是*buyes.Buffer
,动态值现在则是一个指向新分配缓冲区的指针。
调用Write
方法的机制也跟第二个语句一致:
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">w.Write([]byte("hello")) //把"hello"写入
bytes.Buffer`` </pre>
- 第四个语句把
nil
赋给了接口值:
这个语句把动态类型和动态值都设置为nil
,把w恢复到了它刚声明时的状态。
一个接口值可以指向多个任意大的动态值。比如,time.Time
类型可以表示一个时刻,它是一个包含几个非导出字段的结构。
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">var x interface{} = time.Now()
</pre>
值得注意的是,在比较两个接口值时,如果两个接口值的动态类型一致,但对应的动态值是不可比较的(比如:slice),那么这个比较就会以宕机而结束。
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">var x interface{} = []int{1,2,3} fmt.Println(x == x) // 宕机:视图比较不可比较的类型 []int
</pre>
含有空指针的非空接口
空的接口值(其中不包含任何信息)与仅仅动态值为nil
的接口值是不一样的。
这种微妙的区别成为让每个Go程序员困惑过的陷阱。
示例代码如下:
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`const debug = true
func mian(){
var buf *bytes.Buffer
if debug {
buf = new(bytes.Buffer)
}
f(buf) //注意:微妙的错误
if debug {
//....
}
}
// 如果out
不是nil
,那么会向其写入输出的数据
func f(out io.writer){
// ...
if out != nil{
out.Write([]byte("done!\n"))
}
}` </pre>
当main
函数调用f时,它把一个类型为*bytes.Buffer
的空指针赋给了out
参数,所以out
的动态值为空。但它的动态类型是*bytes.Buffer
,这表示out
是一个包含非空指针的非空接口。所以防御性检查out != nil
仍然是true
。
使用 sort.interface 来排序
Go语言的sort.Sort
函数对序列和其中元素的布局无任何要求,它使用sort.Interface
接口来指定通用排序算法和每个具体的序列类型之间的协议。
一个原地排序算法需要知道三个元素:
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`package sort
type Interface interface {
Len() int //序列长度
Less(i,j int) bool // 元素的含义 i,j 是元素的下标
Swap(i,j int) // 如何交换两个元素
}` </pre>
要对序列排序,需要先确定一个实现了如上三个方法的类型,接着把sort.Sort
函数应用到上面这类方法的实例上。我们先实现一个最简单的例子:字符串slice。
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`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]
}` </pre>
现在就可以对一个字符串slice
进行排序,只须简单地把一个slice
转换为StringSlice
类型即可。
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">sort.Sort(StringSlice(names))
</pre>
类型转换生成了一个新的slice
,与原始的names
有同样的长度、容量、底层数组,不同的就是额外增加了三个用排序的方法。
由于字符串排序很常用,sort
包也提供了直接排序的Strings
函数。上面的代码可以直接写成:
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">sort.String(names)
</pre>
http.Handler接口
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`net/http
package http
type Handler interface{
ServeHTTP (w Response, r *Request)
}
func ListenAndServe(address string, h Handler) error` </pre>
ListenAndServe
函数需要一个服务器地址,比如“localhost:8000”
,以及一个Handler
接口的实例(用来接受所有的请求)。这个函数会一直执行,直到服务出错。
假设我们有一个电子商务网站,使用一个数据库来存储商品和价格(以美元计价)的映射。如下:程序将展示一个最简单的实现,它用一个map
类型(命名为database)来代表仓库,再加上一个ServeHTTP
方法来满足http.Handler
接口。该函数遍历整个map,并输出其中的元素:
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`package main
import (
"fmt"
"log"
"net/http"
)
type doolars float32
func (d doolars) String() string {
return fmt.Sprintf("$%.2f", d)
}
type database map[string]doolars
func (db database) ServeHTTP(w http.ResponseWriter, rep *http.Request) {
for item, price := range db {
fmt.Fprintf(w, "%s: %s \n", item, price)
}
}
func main() {
db := database{"shoes": 15, "Shirts": 10, "hat": 12}
log.Fatal(http.ListenAndServe("localhost:8000", db))
}` </pre>
启动程序后,在浏览器中输入localhost:8000
就可以看到遍历输出后的内容。
到现在为止,这个服务器只能列出所有的商品,对于每个请求都是如此。一个更加真实的服务器会定义多个不同的URL,每个触发不同的行为。我们把现有的功能URL设为``/list,再加上另外一个
/price,用来显示单个商品的价格,商品可以在请求参数中指定
/price?item=socks`
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`package main
import (
"fmt"
"log"
"net/http"
)
type doolars float32
func (d doolars) String() string {
return fmt.Sprintf("$%.2f", d)
}
type database map[string]doolars
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)
}
}
func main() {
db := database{"shoes": 15, "Shirts": 10, "hat": 12}
log.Fatal(http.ListenAndServe("localhost:8000", db))
}` </pre>
启动程序后,请求示例如下图:
error接口
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">type error interface{ Error() string }
</pre>
构造error
最简单的方法就是调用error.New
,它会返回一个包含指定的错误消息的心error
实例。完整的error
只包含四行代码:
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`package error
func New(text string) error{return &errorString{text}}
type errorString struct{text string}
func (e *errorString) Error() string{return e.text}` </pre>
底层的errorString
类型是一个结构,而没有直接用字符串,主要是为了避免将来无意间的(或者有预谋的)布局变更。满足error
接口的是*errorString
指针,而不是原始的errorString
,主要是为了让每次New
分配的error
实例互不相等。
直接调用error.New
比较罕见,因为有一个更易用的封装函数fmt.Errorf
,它还额外提供了字符串格式化功能。
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`package fmt
import "error"
func Errorf(format string, args ...interface{}) error{
return error.New(Sprintf(format,args...))
}` </pre>