Golang 中的 interface(一)
interface
interface 是一组方法签名,这些方法定义了 interface 的行为。
interface 基本用法
// 只定义方法,不定义实现
type Traversal interface {
Traverse() // 注意:没有 func 关键字
}
为什么需要用到 interface
我们想实现一个功能,通过给定一个 url
爬取一段网页内容:
func 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 {
}
}(resp.Body)
bytes, _ := ioutil.ReadAll(resp.Body)
return string(bytes)
}
func main() {
fmt.Println(Get("https://li2.xyz"))
}
上面代码中有一个缺点,那就是main
函数和 Get
函数是非常割裂的,两者关心的东西是不一样的。为了解决两者的耦合,就引入了接口(interface)的概念。
通过 interface 解决上面代码的耦合
// infra.go
type Retriever struct {}
func(Retriever) Get(url string) string {
// 同上面的 Get
// ...
}
func getRetriever () infra.Retriever {
return Retriever{}
}
// main.go
func main() {
var r infra.Retriever = getRetriever()
r.Get("https://li2.xyz")
}
上面代码中依旧存在瑕疵,其中 r
的类型一定是 infra.Retriever
,而不能为其他类型。比如说我们有一个测试团队,他们想要通过 mock 来实现 Reteriever
功能,他们就自己实现了一套test.Retriever
,那么就会出现问题: 即我们需要把上面的 infra.Retriever
全部手动改为 test.Retriever
:
// test.go
type Retriever struct{
Contents string
}
// returns fake content
func (r Retriever) Get(url string) string {
return r.Contents
}
func getRetriever () test.Retriever {
return Retriever{}
}
// main.go
func main() {
var r test.Retriever = getRetriever()
r.Get()
}
当 test
同样实现了一个 Retriever
的时候,其中 r
的类型就要变成 test.Retriever
类型,这种手动修改是非常不妥的。 可以观察一下上面的代码:
var r test.Retriever = getRetriever()
r
的类型必须是固定,开发团队使用的时候 r = infra.Retriver
, 测试团队使用的时候 r = test.Retriever
,那么能不能把 r
的类型抽象化一下,当使用的时候才确定类型呢?比如抽象成:
var r <?> = getRetriever()
其中 <?>
这个类型就是 interface
了,即:
type retriever interface {
Get(string) string
}
改造一下上面的功能就是:
// 省略部分代码...
type retriever interface(){
Get(string) string
}
func getRetriever() reteriever{
return test.Retriever{}
// return infra.Retriever{}
}
func main() {
var r retriever = getRetriever()
r.Get() // 此时是 test.Retriever
// r.Get("https://li2.xyz") 此时是 infra.Retriever
}
以上代码中我们只需要关心 getRetriever
返回的类型即可。 上面的代码还没只有足够好,我们下面接着看。
Go 语言中的 Duck Typing(鸭子类型)
先介绍一下 “鸭子类型”。
duck typing 概念:
- "叫起来像鸭子,走路像鸭子,那么就是鸭子"
- 描述事物的外部行为而非内部结构
- 动态类型的一种风格
INFO
由于 golang 编译时绑定,属于结构化类型系统,严格意义上来说并非 duck typing,但类似 duck typing
先看一下 JavaScript 中的 duck typing 用法:
const googleMap = {
show(){
console.log('this is google map result')
}
}
const gaodeMap = {
show(){
console.log('this is gaode map result')
}
}
const renderMap = (map) => {
map?.show()
}
renderMap(googleMap) // this is google map result
renderMap(gaodeMap) // this is gaode map result
上面这段代码中 googleMap
和 gaodeMap
都拥有 show
方法,那我么可以默认为两者都是 Map
golang 中 duck typing 的特点
- 具有动态语言(Python、JS) duck typing 的灵活性
- 拥有静态语言(Java)的类型检查
- 实现多接口组装,如
Writer
Reader
Closer
ReadWriteCloser
,可参考/usr/local/go/src/io/io.go
中的源码
Go 中 interface 的定义
其中有两个角色:
- 实现者: retriever
- 使用者: download 上面两个角色的关系可以看作是:使用者 定义 接口。
接口由使用者定义
看下使用者如何定义接口:
// main.go
// 使用者
type Retriever interface {
Get(url string) string
}
func download(r Retriever) string {
return r.Get("https://www.baidu.com")
}
// mock.go
// 实现者:只要实现 Get 方法即可
type Retriever struct {
Contents string
}
func (r *Retriever) Get(url string) string {
return r.Contents
}
// main.go
// usuage
func main() {
r := mock.Retriever{"this is mock data"}
fmt.Printf("%s:", download(r))
}
上面这个例子中,使用者(download)规定 retriever
接口有 Get 方法,实现者不关心是哪个接口,只关心实现接口中的方法即可。
如果一个类型实现了一个 interface 中所有方法,我们说类型实现了该 interface
上面例子中 Retriever struct
实现了 Retriever interface
,那么可以说 Retriever struct
和 Retriever interface
是兼容的,看一个简单的例子:
type GetSetName interface {
Get() string
Set(string)
}
type Person struct {
name string
}
func (p *Person) Get() string {
return p.name
}
func (p *Person) Set(name string) {
p.name = name
}
func showName() {
shinji := Person{name: "Ikari"}
fmt.Println(shinji.Get())
shinji.Set("Ikari")
fmt.Println(shinji.Get())
}
func showName2(g GetSetName) {
fmt.Println(g.Get())
shinji := g
fmt.Println(shinji.Get())
shinji.Set("Ikari")
fmt.Println(shinji.Get())
}
func main() {
showName()
showName2(&Person{
name: "Ayanami",
})
}
上面代码中 showName2
方法需要的是一个 interface
类型的数据结构,但实际上我们传入的是一个 Person
类型的 struct
接口的实现的特点
- 接口的实现是隐士的
- 只要实现接口里的方法