Loading...
墨滴

Bayanbulake

2021/12/11  阅读:40  主题:自定义主题1

Go语言之接口

接口

接口类型是对其他类型行为的概括与抽象。通过使用接口,我们可以写出更加灵活和通用的函数,这些函数不用绑定在一个特定类型的实现上。

很多面向对象的编程语言都有接口的概念,Go语言的接口的独特之处在于它是隐式实现的。换句话说,对于一个具体的类型,无需声明它实现了哪些接口,只要提供接口所必需的方法即可。这种设计让你无需改变已有的类型的实现,就可以为这些类型创建新的接口,对于那些不能修改包的类型,这一点特别有用。

声明接口并实现

声明一个Car接口,并new出三种车辆去实现这个接口,代码如下:

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()

}

接口即约定

接口是一种抽象类型,它并没有暴露所含数据的布局或者内部结构,当然也没有那些数据的基本操作,它所提供的仅仅是一些方法而已。

我们平时使用较多的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就抽象了所有可以关闭的类型,比如文件或者网络连接。

package io

type Reader interface{
    Read(p []byte)(n int,err error)
}

type Closer interface{
    Closer() error
}

另外,我们还可以发现通过组合一有借口得到新的接口,比如:

type ReadWriter interface{
    Reader
    Writer


type ReadWriteClooser interface{
    Reader
    Writer
    Closer


如上的语法称为嵌入式接口,与嵌入式结构类似,让我们可以直接使用一个接口,而不用逐一写出这个接口所包含的方法。

实现接口

如果一个类型实现了一个接口要求的所有方法,那么这个类型实现了这个接口。

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方法
}
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方法
}

当右侧表达式也是一个接口时,该规则也有效:

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方法
}

对于每一个具体类型T而言,部分方法的接收者就是T,而其他方法的接收者就是*T指针。同时我们对类型T的变量直接调用*T的方法也可以是合法的,只要改变量是可变的,编译器隐式地帮助我们完成了取地址的操作。但这个仅仅是语法糖,类型T的方法没有对应的*T多,所以实现的接口也可能比对应的指针少。

type IntSet struct{/*...*/}
func (*IntSet) String() string
var _ = IntSet{}.String() // 编译错误,String方法需要`*IntSet`接收者

但是可以从一个IntSet变量上调用该方法:

var s IntSet
var _ = s.String() // OK s是一个变量,&s有String()

因为只有*IntSETString(),所以也只有*IntSet实现了fmt.Stringer

var _ fmt.Stringer = &s // OK
var _ fmt.Stringer = s // 编译错误: IntSet缺少String方法

就像信封封装了信件,接口也封装了对应的类型和数据,只有通过接口暴露的方法才可以调用,类型的其他方法则无法通过接口来调用:

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方法

因为空接口类型对其实现类型没有任何要求,所以我们可以把任何值赋给空接口类型,就像下面这样:

var any interface{}
any = 123
any = "mua~"
any = map[string]int{"one"P:1}
any = new(bytes.Buffer)

使用flag.Value()来解析参数

flag.Value是Go语言里面的标准接口。

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("我睡醒了~")
}

默认的睡眠时间是3s,但是可以通过 -peroid命令行来控制。

./sleep -peroid 20ms
Sleeeping for 50ms...

接口值

从概念上来讲,一个接口类型的值(简称接口值)其实有两部分:一个具体类型和该类型的一个值。二者称为接口的动态类型和动态值。对于像Go这样的静态类型语言,类型仅仅是一个编译时的概念,所以类型不是一个值。

如下四个语句中,变量w有三个不同的值(第一个和第三个是同一个值):

var w io.writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil

接下来让我们详细查看一下在每个语句之后w的值和相关的动态行为。

  • 第一个语句声明了w:
var w io.Writer

在Go语言中,变量总是初始化为一个特定的值,接口也不例外。接口的零值就是把它的动态类型和值都设为nil

一个接口值是否是nil取决于它的动态类型,所以现在这是一个nil接口值。可以使用w == nil或者w != nil来检测一个接口值是否是nil。调用一个nil接口的任何方法都会导致崩溃:

w.Write([]byte("hello")) //崩溃: 对空指针取引用值
  • 第二个语句把一个*os.File类型赋值给了w:
w = os.Stdout

这个赋值把一个具体类型隐式转换为一个接口类型,它与对应的显式转换io.Writer(os.Stdout)等价。不管这种类型的转换是隐式地还是显式的,它都可以转换操作数的类型和值。

  • 第三个语句把一个*bytes.Buffer类型的值赋值给了接口值:
w = new(bytes.Buffer)

动态类型现在是*buyes.Buffer,动态值现在则是一个指向新分配缓冲区的指针。

调用Write方法的机制也跟第二个语句一致:

w.Write([]byte("hello")) //把"hello"写入`bytes.Buffer`
  • 第四个语句把nil赋给了接口值:

这个语句把动态类型和动态值都设置为nil,把w恢复到了它刚声明时的状态。

一个接口值可以指向多个任意大的动态值。比如,time.Time类型可以表示一个时刻,它是一个包含几个非导出字段的结构。

var x interface{} = time.Now()

值得注意的是,在比较两个接口值时,如果两个接口值的动态类型一致,但对应的动态值是不可比较的(比如:slice),那么这个比较就会以宕机而结束。

var x interface{} = []int{1,2,3}
fmt.Println(x == x) // 宕机:视图比较不可比较的类型 []int

含有空指针的非空接口

空的接口值(其中不包含任何信息)与仅仅动态值为nil的接口值是不一样的。

这种微妙的区别成为让每个Go程序员困惑过的陷阱。

示例代码如下:

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"))
    }
}

main函数调用f时,它把一个类型为*bytes.Buffer的空指针赋给了out参数,所以out的动态值为空。但它的动态类型是*bytes.Buffer,这表示out是一个包含非空指针的非空接口。所以防御性检查out != nil仍然是true

使用 sort.interface 来排序

Go语言的sort.Sort函数对序列和其中元素的布局无任何要求,它使用sort.Interface接口来指定通用排序算法和每个具体的序列类型之间的协议。

一个原地排序算法需要知道三个元素:

package sort

type Interface interface {
    Len() int //序列长度
    Less(i,j intbool // 元素的含义 i,j 是元素的下标
    Swap(i,j int// 如何交换两个元素
}

要对序列排序,需要先确定一个实现了如上三个方法的类型,接着把sort.Sort函数应用到上面这类方法的实例上。我们先实现一个最简单的例子:字符串slice。

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]
}

现在就可以对一个字符串slice进行排序,只须简单地把一个slice转换为StringSlice类型即可。

sort.Sort(StringSlice(names))

类型转换生成了一个新的slice,与原始的names有同样的长度、容量、底层数组,不同的就是额外增加了三个用排序的方法。

由于字符串排序很常用,sort包也提供了直接排序的Strings函数。上面的代码可以直接写成:

sort.String(names)

http.Handler接口

net/http

package http

type Handler interface{
    ServeHTTP (w Response, r *Request)
}

func ListenAndServe(address string, h Handler) error

ListenAndServe函数需要一个服务器地址,比如“localhost:8000”,以及一个Handler接口的实例(用来接受所有的请求)。这个函数会一直执行,直到服务出错。

假设我们有一个电子商务网站,使用一个数据库来存储商品和价格(以美元计价)的映射。如下:程序将展示一个最简单的实现,它用一个map类型(命名为database)来代表仓库,再加上一个ServeHTTP方法来满足http.Handler接口。该函数遍历整个map,并输出其中的元素:

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))
}

启动程序后,在浏览器中输入localhost:8000就可以看到遍历输出后的内容。

到现在为止,这个服务器只能列出所有的商品,对于每个请求都是如此。一个更加真实的服务器会定义多个不同的URL,每个触发不同的行为。我们把现有的功能URL设为``/list,再加上另外一个/price,用来显示单个商品的价格,商品可以在请求参数中指定/price?item=socks`

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))
}

启动程序后,请求示例如下图:

error接口

type error interface{
    Error() string
}

构造error最简单的方法就是调用error.New,它会返回一个包含指定的错误消息的心error实例。完整的error只包含四行代码:

package error

func Newtext string) error{return &errorString{text}}

type errorString struct{text string}

func (e *errorString) Error() string{return e.text}

底层的errorString类型是一个结构,而没有直接用字符串,主要是为了避免将来无意间的(或者有预谋的)布局变更。满足error接口的是*errorString指针,而不是原始的errorString,主要是为了让每次New分配的error实例互不相等。

直接调用error.New比较罕见,因为有一个更易用的封装函数fmt.Errorf,它还额外提供了字符串格式化功能。

package fmt

import "error"

func Errorf(format string, args ...interface{}) error{
    return error.New(Sprintf(format,args...))
}

Bayanbulake

2021/12/11  阅读:40  主题:自定义主题1

作者介绍

Bayanbulake