go接口
# 接口概念
引入一个段子:《小孩才分对错,大人只看利弊》
案例:写了一个下载器:
package main
import (
"fmt"
"io"
"io/ioutil"
"net/http"
)
func retrieve(url string) string {
resp, err := http.Get(url)
if err != nil {
panic(err)
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
panic(err)
}
}(resp.Body)
bytes, _ := ioutil.ReadAll(resp.Body)
return string(bytes)
}
func main() {
fmt.Println(retrieve("https://www.baidu.com"))
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
表面上这段代码其实确实没啥问题,但是,main
函数和retrieve
函数之间产生了耦合,main
函数必须调用这个方法才会生效。
假设我们有一个团队,专门处理网络请求或磁盘读写的功能的,我们可以进行模拟:
现在建立了一个infra
小组
package infra
import (
"io"
"io/ioutil"
"net/http"
)
type Retriever struct {
}
func (Retriever) Get(url string) string {
resp, err := http.Get(url)
if err != nil {
panic(err)
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
panic(err)
}
}(resp.Body)
bytes, _ := ioutil.ReadAll(resp.Body)
return string(bytes)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
专门用于处理网络请求。
再次对下载器进行改写:
package main
import (
"fmt"
"learngo/infra"
)
func getRetriever() infra.Retriever {
return infra.Retriever{}
}
func main() {
var retriever infra.Retriever = getRetriever()
fmt.Println(retriever.Get("https://www.baidu.com"))
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
但是呢,此时,我们还是需要使用infra.Retriever
它来进行调用,我们不能换么?
假如现在又有一个测试的目录,也有一个对应的测试的网络请求方法,返回一个假的字符串:
package testing
type Retriever struct {
}
func (Retriever) Get(url string) string {
return ""
}
2
3
4
5
6
7
8
9
此时我们在下载器代码中想要更换调用,更改的力度就很大
package main
import (
"fmt"
"learngo/testing"
)
func getRetriever() testing.Retriever {
return testing.Retriever{}
}
func main() {
var retriever = getRetriever()
fmt.Println(retriever.Get("https://www.baidu.com"))
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
近乎全改。
为什么会造成这样的我们不满意的情况?
我们直观的觉得这两个
retriever
都是做的同样的事情,应该换起来是很容易的,为什么会改这么多地方?对于静态语言来说,我们会有一些类型概念,在编译期就会知道传入的是什么类型。当我们改
retriever
,就是再换类型就会换的很麻烦。
换个想法
var retriever ? = getRetriever()
我们其实就是需要一个可以使用Get
方法去请求地址。
package main
import (
"fmt"
"learngo/testing"
)
func getRetriever() retriever {
return testing.Retriever{}
}
// ?: Something that can "Get"
type retriever interface {
Get(string) string
}
func main() {
var r retriever = getRetriever()
fmt.Println(r.Get("https://www.baidu.com"))
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
此时我们换回原先的方法调用,就很简单,只有换调用方即可。
func getRetriever() retriever {
return infra.Retriever{}
}
2
3
疑问点 ❓
从Java
语言来的小伙伴,看到这个会很懵逼,照理说写了一个interface
我们需要去实现一个它这样的接口方法,但是我们这里并没有去实现它,还能继续调用。这个就是duck typing
,即《鸭子模型》。
# 大黄鸭是鸭子吗?
- 传统类型系统:脊索动物们、脊椎动物亚门、鸟纲雁形目,不是鸭子
duck typing
:是鸭子- ”像鸭子走路,像鸭子叫(长得像鸭子),那么就是鸭子“
- 描述事物的外部行为而非内部结构
- 严格说
go
属于结构化类型系统,类似duck typing
# python 的duck typing
def download(retriever):
return retriever.get("www.baidu.com")
2
- 运行时才知道传入的
retriever
有没有get
方法 - 需要注释来说明接口
# C++中的duck typing
template <class R> string download(const R& retriever) {
return retriever.get("www.baidu.com")
}
2
3
- 编译时才知道传入的
retriever
有没有get
方法,写的时候并不知道 - 需要注释来说明接口
# java 中的类似代码
<R extends Retriever> String download(R r) {
return r.get("www.baidu.com")
}
2
3
Java
逼着我们必须实现Retriever
接口- 但是它不是
duck typing
- 不在需要注释来说明
# go 语言的duck typing
- 同时具有
python、C++
的duck typing
的灵活性 - 又具有
java
的类型检查
# 定义
- 使用者:
downloader
- 实现者:
retriever
- 接口由
使用者
定义- 接口的实现是隐式的
- 只要实现接口里的方法就可以了
package real
import (
"net/http"
"net/http/httputil"
"time"
)
type Retriever struct {
UserAgent string
TimeOut time.Duration
}
func (r Retriever) Get(url string) string {
resp, err := http.Get(url)
if err != nil {
panic(err)
}
result, err := httputil.DumpResponse(resp, true)
defer resp.Body.Close()
if err != nil {
panic(err)
}
return string(result)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
接口一般”肚子“里有它的类型
- 接口变量自带指针
- 接口变量同样采用值传递,几乎不需要使用接口的指针
- 指针接收者只能使用指针方式使用;值接收者两者都可以
# 查看接口变量的三种方式
- 表示任何类型:
interface{}
Type Assertion
Type Switch
# 接口的组合
常见的案例就是io.WriteCloser
一类的接口,他们里面包含了写读和关闭文件等多个接口。
type Retriever interface {
Get(url string) string
}
type Poster interface {
Post(url string, form map[string]string) string
}
// RetrieverPoster 接口的组合
type RetrieverPoster interface {
Retriever
Poster
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 接口也可以作为结构体的字段,我们来看一段 Go 标准库sort
的源码示例
// An implementation of Interface can be sorted by the routines in this package.
// The methods refer to elements of the underlying collection by integer index.
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
// reverse 中嵌入了 Interface 接口
type reverse struct {
Interface // 匿名字段
}
2
3
4
5
6
7
8
9
10
11
12
通过在结构体中嵌入一个接口类型,从而让该结构体类型实现了该接口类型,并且还可以改写接口的方法。
// Less 重写原Interface接口类型的Less方法
func (r reverse) Less(i, j int) bool {
return r.Interface.Less(i, j)
}
2
3
4
# 结构体内部包含匿名接口类型
- 默认就实现了该接口类型
- 可以重写接口的方法
- 但是一定要确保各个接口的字段被正确初始化,没有初始化,它就是
nil
- 接口类型的初始化 -> 找一个实现了该接口类型的变量赋值过去
# 空接口
空接口是指没有定义任何方法的接口类型。因此任意类型都可以视为实现了空接口。正因为这个特性,空接口类型的变量可以存储任意类型的值。
# 接口值
接口值除了需要记录具体值之外,还需要记录这个值属于的类型,即接口值由类型和值组成,且这两部分内容会根据存储的值不同而变化,我们称之为接口的动态类型
和动态值
type Car struct {}
var m interface{}
m = new(Car)
2
3
接口值的动态类型是*Car
,动态值为nil
,此时的接口变量m
与nil
并不相等。
# 常用的系统接口
- 类似
java
的toString
:Stringer
接口,里面有一个string()
函数 Reader/Writer
# 指针接收者和值接收者的区别
使用指针接收者实现接口:
接口变量可以接收结构体指针但不能接收结构体类型(不是任何值都能取地址)
使用值接收者实现接口:
接口变量既可以接收指针类型又能接收结构体类型(有了地址就能取值)
字面量如果使用:
变量 := 字面量类型(值)
之后,得到的变量也可以进行取地址
# 类型断言
接口值可能赋值为任意类型的值,那么如何从接口获取其存储的具体数据呢
我们可以借助标准库fmt
包的格式化打印获取到接口值的动态类型
var m Mover
m = &Dog{Name: "无解"}
fmt.Printf("%T\n", m) // *main.Dog
m = new(Car)
fmt.Printf("%T\n", m) // *main.Car
2
3
4
5
6
7
fmt
包内部其实是使用反射的机制在程序运行时获取到动态类型的名称。
想要从接口值中获取到对应的实际值需要使用类型断言,语法格式如下:
x.(T)
- x: 接口类型的变量
- T:表示断言的类型
案例:
b, ok := x.(bool)
如果ok
,就把接口变量转换为对应的类型,否则ok
为false
小技巧 下面的代码可以在程序编译阶段验证某一结构体是否满足特定的接口类型
// gin框架的routergroup.go
type IRouter interface {}
type RouterGroup struct {}
var _ IRouter = &RouterGroup{} // 确保 RouterGroup 实现了接口 IRouter
2
3
4
5
6
7