Go语言基础之接口

1. 介绍

在面向对象的领域里,接口一般这样定义:接口定义一个对象的行为。接口只指定了对象应该做什么,至于如何实现这个行为(即实现细节),则由对象本身去确定。

在 Go 语言中,接口就是方法签名(Method Signature)的集合。当一个类型定义了接口中的所有方法,我们称它实现了该接口。这与面向对象编程(OOP)的说法很类似。接口指定了一个类型应该具有的方法,并由该类型决定如何实现这些方法

  1. 接口是一种类型,也是一种抽象结构的概括,不会和特定的实现细节绑定在一起,不会暴露所含数据的格式、类型及结构。
  2. 很多面向对象的语言都有相似的接口概念:
  • 传统 java c#:

    • 传统的派生式接口及类关系构建的模式,让类型间拥有强耦合的父子关系。这种关系一般会以“类派生图”的方式进行。
    • 经常可以看到大型软件极复杂的派生树,随着系统的功能不断增加,这棵“派生树”会变得越来越复杂。
    • 实现者在编写方法时,无法预测未来哪些方法会变为接口。一旦某个接口创建出来,要求旧的代码来实现这个接口时,就需要修改旧的代码的派生部分,这一般会造成雪崩式的重新编译。
  • Go语言:

    • Go的接口实现是隐式的,无须让实现接口的类型写出实现了哪些接口。这个设计称为非侵入式设计。非侵入式设计让实现者的所有类型均是平行的、组合的。如何组合则留到使用者编译时再确认。
    • 这种设计可以让你创建一个新的接口类型满足已经存在的具体类型却不会去改变这些类型的定义;当我们使用的类型来自于不受我们控制的包时这种设计尤其有用。
    • Go语言的每个接口中的方法数量不要很多。Go语言希望通过一个接口精准描述它自己的功能,而通过多个接口的嵌入和组合的方式将简单的接口扩展为复杂的接口。
    • 因此,使用GO语言时,不需要同时也不可能有“类派生图”,开发者唯一需要关注的就是“我需要什么?”,以及“我能实现什么?”。

2. 接口类型

在Go语言中接口(interface)是一种类型,一种抽象的类型。

interface是一组method的集合,是duck-type programming的一种体现。接口做的事情就像是定义一个协议(规则),只要一台机器有洗衣服和甩干的功能,我就称它为洗衣机。不关心属性(数据),只关心行为(方法)。

为了保护你的Go语言职业生涯,请牢记接口(interface)是一种类型。

用来给变量、参数、返回值等设置类型。

duck typing

描述事物的外部行为而非内部结构

严格说go属于结构化类型语言,类似duck typing

  • 接口由使用者定义
  • 接口的实现是隐式的
  • 只要实现接口里面的方法

注意

  1. 方法声明的集合

  2. 任何类型的对象实现了在接口中声明的全部方法,则表明该类型实现了对应接口。

  3. 可以作为一种数据类型,实现了该接口的任何对象都可以给对应的接口类型变量赋值。

3. 为什么要使用接口

type Cat struct{}
func (c Cat) Say() string { return "喵喵喵" }
type Dog struct{}
func (d Dog) Say() string { return "汪汪汪" }
func main() {
c := Cat{}
fmt.Println("猫:", c.Say())
d := Dog{}
fmt.Println("狗:", d.Say())
}

上面的代码中定义了猫和狗,然后它们都会叫,你会发现main函数中明显有重复的代码,如果我们后续再加上猪、青蛙等动物的话,我们的代码还会一直重复下去。那我们能不能把它们当成“能叫的动物”来处理呢?

像类似的例子在我们编程过程中会经常遇到:

  • 比如一个网上商城可能使用支付宝、微信、银联等方式去在线支付,我们能不能把它们当成“支付方式”来处理呢?

  • 比如三角形,四边形,圆形都能计算周长和面积,我们能不能把它们当成“图形”来处理呢?

  • 比如销售、行政、程序员都能计算月薪,我们能不能把他们当成“员工”来处理呢?

Go语言中为了解决类似上面的问题,就设计了接口这个概念。接口区别于我们之前所有的具体类型,接口是一种抽象的类型。当你看到一个接口类型的值时,你不知道它是什么,唯一知道的是通过它的方法能做什么。

4. 接口的定义

Go语言提倡面向接口编程。

每个接口由数个方法组成,接口的定义格式如下:

type 接口类型名 interface{
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2

}

其中:

  • 接口名:使用type将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er (行业规范),如有写操作的接口叫Writer,有字符串功能的接口叫Stringer,有关闭功能的接口叫 Closer 等。接口名最好要能突出该接口的类型含义。

  • 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。

  • 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。

举个例子:

type writer interface{
Write([]byte) error
}

当你看到这个接口类型的值时,你不知道它是什么,唯一知道的就是可以通过它的Write方法来做一些事情。

5. 接口的实现

一个对象只要全部实现了接口中规定的方法,那么就实现了这个接口。换句话说,接口就是一个需要实现的方法列表

实现接口需要满足2个条件:

  • 条件1: 接口的方法与实现接口的类型方法格式一致
  • 条件2: 接口中所有方法均被实现

我们来定义一个Sayer接口:

// Sayer 接口
type Sayer interface {
say()
}

定义dogcat两个结构体:

type dog struct {}
type cat struct {}

因为Sayer接口里只有一个say方法,所以我们只需要给dogcat 分别实现say方法就可以实现Sayer接口了。

// dog实现了Sayer接口
func (d dog) say() {
fmt.Println("汪汪汪")
}
// cat实现了Sayer接口
func (c cat) say() {
fmt.Println("喵喵喵")
}

接口的实现就是这么简单,只要实现了接口中的所有方法,就实现了这个接口。

注意

实现接口要求按照接口中的方法名,参数列表,返回列表一致来实现。

举个例子:

// 定义一个数据写入器
type DataWriter interface {
WriteData(data interface{}) error
}

// 定义文件结构,用于实现DataWriter
type file struct { }
// 实现DataWriter接口的WriteData方法
func (d *file) WriteData(data interface{}) error {
// 模拟写入数据
fmt.Println("WriteData:", data)
return nil
}

func main() {
// 实例化file
var f := new(file)
// 声明一个DataWriter的接口
var writer DataWriter
// 将接口赋值f,也就是*file类型
writer = f
// 使用DataWriter接口进行数据写入
writer.WriteData("data")
}

错误1:函数名不一致导致

假设修改 file 结构的 WriteData() 方法名,将这个方法签名(第9行)修改如下,然后编译:

func (d *file) WriteDataX(data interface{}) error { ... }
panic:cannot use f (type *file) as type DataWriter in assignment: *file does not implement DataWriter (missing WriteData method)

错误2:实现接口的方法签名不一致导致的报错

假设修改 file 结构的 WriteData() 方法名,把 data 参数的类型从 interface{} 修改为 int 类型,然后编译:

func (d *file) WriteData(data int) error { ... }
panic:cannot use f (type *file) as type DataWriter in assignment:

*file does not implement DataWriter (wrong type for WriteData method)

have WriteData(int) error

want WriteData(interface {}) error

接口中所有方法均被实现

当一个接口中有多个方法时,只有这些方法都被实现了,接口才能被正确编译并使用。

为 DataWriter中 添加一个CanWrite方法,代码如下,然后编译:

// 定义一个数据写入器
type DataWriter interface {
WriteData(data interface{}) error
// 能否写入
CanWrite() bool
}
panic:cannot use f (type *file) as type DataWriter in assignment:

*file does not implement DataWriter (missing CanWrite method)

需要在 file 中实现 CanWrite() 方法才能正常使用 DataWriter()。

6. 接口类型变量

那实现了接口有什么用呢?

接口类型变量能够存储所有实现了该接口的实例(可以把多个类型的变量通过是否含有某些方法而统一起来为一个接口类型)。 例如上面的示例中,Sayer类型的变量能够存储dogcat类型的变量。

接口保存的分为两部分:值的类型和值本身

type Sayer interface {
say()
}
func main() {
var x Sayer // 声明一个Sayer类型的变量x
a := cat{} // 实例化一个cat
b := dog{} // 实例化一个dog
x = a // 可以把cat实例直接赋值给x
x.say() // 喵喵喵
x = b // 可以把dog实例直接赋值给x
x.say() // 汪汪汪
}

Tips: 观察下面的代码,体味此处_的妙用

// 摘自gin框架routergroup.go
type IRouter interface{ ... }
type RouterGroup struct { ... }
var _ IRouter = &RouterGroup{} // 确保RouterGroup实现了接口IRouter

7. 值接收者和指针接收者实现接口的区别

定义满足接口的方法

使用值接收者实现接口和使用指针接收者实现接口有什么区别呢?接下来我们通过一个例子看一下其中的区别。

我们有一个Mover接口和一个dog结构体。

type Mover interface {
move()
}
type dog struct {}

7.1 值接收者实现接口

func (d dog) speak(){};

func (d dog) move() {
fmt.Println("狗会动")
}

此时实现接口的是dog类型:

func main() {
var x Mover
var wangcai = dog{} // 旺财是 dog类型
x = wangcai // x可以接收dog类型
var fugui = &dog{} // 富贵是 *dog类型(即dog的指针类型)
x = fugui // x可以接收*dog类型
x.move()
}

从上面的代码中我们可以发现,使用值接收者实现接口之后,不管是dog结构体还是结构体指针*dog类型的变量都可以赋值给该接口变量。因为Go语言中有对指针类型变量求值的语法糖,dog指针fugui内部会自动求值*fugui

7.2 指针接收者实现接口

func (d *dog) speak(){};

指针接受者实现接口只能存结构体指针类型的变量。

同样的代码我们再来测试一下使用指针接收者有什么区别:

func (d *dog) move() {
fmt.Println("狗会动")
}
func main() {
var x Mover
var wangcai = dog{} // 旺财是dog类型
x = wangcai // x不可以接收dog类型
var fugui = &dog{} // 富贵是*dog类型
x = fugui // x可以接收*dog类型
}

此时实现Mover接口的是*dog类型,所以不能给x传入dog类型的wangcai,此时x只能存储*dog类型的值。

8. 类型与接口的关系

8.1 一个类型实现多个接口

一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。 例如,狗可以叫,也可以动。我们就分别定义Sayer接口和Mover接口,如下: Mover接口。

// Sayer 接口
type Sayer interface {
say()
}
// Mover 接口
type Mover interface {
move()
}

dog既可以实现Sayer接口,也可以实现Mover接口。

type dog struct {
name string
}
// 实现Sayer接口
func (d dog) say() {
fmt.Printf("%s会叫汪汪汪\n", d.name)
}
// 实现Mover接口
func (d dog) move() {
fmt.Printf("%s会动\n", d.name)
}
func main() {
var x Sayer
var y Mover
var a = dog{name: "旺财"}
x = a
y = a
x.say()
y.move()
}

8.2 多个类型实现同一接口

下图即为传入含有Read方法的Reader接口类型即可,不管结构体的类型:

Go语言中不同的类型还可以实现同一接口 首先我们定义一个Mover接口,它要求必须由一个move方法。

// Mover 接口
type Mover interface {
move()
}

例如狗可以动,汽车也可以动,可以使用如下代码实现这个关系:

type dog struct {
name string
}
type car struct {
brand string
}
// dog类型实现Mover接口
func (d dog) move() {
fmt.Printf("%s会跑\n", d.name)
}
// car类型实现Mover接口
func (c car) move() {
fmt.Printf("%s速度70迈\n", c.brand)
}

这个时候我们在代码中就可以把狗和汽车当成一个会动的物体来处理了,不再需要关注它们具体是什么,只需要调用它们的move方法就可以了。

func main() {
var x Mover
var a = dog{name: "旺财"}
var b = car{brand: "保时捷"}
x = a
x.move()
x = b
x.move()
}

上面的代码执行结果如下:

# 旺财会跑
# 保时捷速度70

并且一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。

// WashingMachine 洗衣机
type WashingMachine interface {
wash()
dry()
}
// 甩干器
type dryer struct{}
// 实现WashingMachine接口的dry()方法
func (d dryer) dry() {
fmt.Println("甩一甩")
}
// 海尔洗衣机
type haier struct {
dryer //嵌入甩干器
}
// 实现WashingMachine接口的wash()方法
func (h haier) wash() {
fmt.Println("洗刷刷")
}

8.3 函数类型实现接口

其他类型能够实现接口,函数也可以:

package main
import (
"fmt"
)

// 调用器接口
type Invoker interface {
// 需要实现一个Call方法
Call(interface{})
}

// 结构体类型
type Struct struct {
}
// 实现Invoker的Call
func (s *Struct) Call(p interface{}) {
fmt.Println("from struct", p)
}

// 函数定义为类型
type FuncCaller func(interface{})
// 实现Invoker的Call
func (f FuncCaller) Call(p interface{}) {
// 调用f函数本体
f(p)
}

func main() {
// 声明接口变量
var invoker Invoker
// 实例化结构体
s := new(Struct)
// 将实例化的结构体赋值到接口
invoker = s
// 使用接口调用实例化结构体的方法Struct.Call
invoker.Call("hello")
// 将匿名函数转为FuncCaller类型,再赋值给接口
invoker = FuncCaller(func(v interface{}) {
fmt.Println("from function", v)
})
// 使用接口调用FuncCaller.Call,内部会调用函数本体
invoker.Call("hello")
}

输出如下:

from struct hello
from function hello

8.4 http 例子

HTTP 包中有 Handler 接口定义,用于定义每个 HTTP 的请求和响应的处理过程,代码如下:

type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}

同时,也可以使用处理函数实现接口,定义如下:

type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}

要使用闭包实现默认的 HTTP 请求处理,可以使用 http.HandleFunc() 函数,函数定义如下:

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}

而 DefaultServeMux 是 ServeMux 结构,拥有 HandleFunc() 方法,定义如下:

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
mux.Handle(pattern, HandlerFunc(handler))
}

上面代码将外部传入的函数 handler() 转为 HandlerFunc 类型,HandlerFunc 类型实现了 Handler 的 ServeHTTP 方法,底层可以同时使用各种类型来实现 Handler 接口进行处理。

9. 接口嵌套

接口与接口间可以通过嵌套创造出新的接口。实现接口时必须实现嵌套接口内部的所有方法。

一个接口可以包含一个或多个其他的接口,这相当于直接将这些内嵌接口的方法列举在外层接口中一样。只要接口的所有方法被实现,则这个接口中的所有嵌套接口的方法均可以被调用。

// Sayer 接口
type Sayer interface {
say()
}
// Mover 接口
type Mover interface {
move()
}
// 接口嵌套
type animal interface {
Sayer
Mover
}

嵌套得到的接口的使用与普通接口一样,这里我们让cat实现animal接口:

type cat struct {
name string
}
func (c cat) say() {
fmt.Println("喵喵喵")
}
func (c cat) move() {
fmt.Println("猫会动")
}
func main() {
var x animal
x = cat{name: "花花"}
x.move()
x.say()
}

Go语言的 io 包中定义了写入器(Writer)、关闭器(Closer)和写入关闭器(WriteCloser)3 个接口:

type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
type WriteCloser interface {
Writer
Closer
}

10. 空接口

10.1 nil 的判断

  1. nil 在 Go语言中只能被赋值给指针和接口。接口在底层的实现有两个部分:type 和 data。

  2. 显式地将 nil 赋值给接口时,接口的 type 以及 data 都将为 nil,此时接口与 nil 值判断是相等的。

  3. 带类型的 nil 赋值给接口时,接口的 type 不为 nil,data 为 nil,此时接口与 nil 值判断是不相等的。

// 定义一个结构体
type MyImplement struct{}
// 实现fmt.Stringer的String方法
func (m *MyImplement) String() string {
return "hi"
}
// 在函数中返回fmt.Stringer接口
func GetStringer() fmt.Stringer {
// 赋nil
var s *MyImplement = nil
// 返回变量
return s
}
func main() {
// 判断返回值是否为nil
if GetStringer() == nil {
fmt.Println("GetStringer() == nil")
} else {
fmt.Println("GetStringer() != nil")
}
}

输出:

GetStringer() != nil

发现 nil 类型值返回时直接返回 nil

为了避免这类误判的问题,可以在函数返回时,发现带有 nil 的指针时直接返回 nil,代码如下:

func GetStringer() fmt.Stringer {
var s *MyImplement = nil
if s == nil {
return nil
}
return s
}

10.2 什么是空接口?

  1. 空接口是接口类型的特殊形式,空接口没有任何方法,因此任何类型都无须实现空接口。从实现的角度看,任何值都满足这个接口的需求。因此空接口类型可以保存任何值,也可以从空接口中取出原值。

  2. 空接口类型类似于 C#Java 语言中的 Object、C语言中的 void*、C++ 中的 std::any。在泛型和模板出现前,空接口是一种非常灵活的数据抽象保存和使用的方法。

  3. golang中的所有程序都实现了interface{}的接口。这意味着所有的类型如string、int、int64、struct、map、slice等类型都就此拥有了interface{}的接口,这种做法和java中的Object类型比较类似。空接口在Go语言中真正的意义是支持多态。

  4. 空接口的内部实现保存了对象的类型和指针。使用空接口保存一个数据的过程会比直接用数据对应类型的变量保存稍慢。因此在开发中,应在需要的地方使用空接口,而不是在所有地方使用空接口。

  5. 在一个数据通过函数的方式传进来时,且参数为空接口,也就意味着这个参数被自动的转为interface{}的类型。

  6. 空接口 type x interface{} 在函数定义时参数可用空接口( interface{} 可匿名,没有必要起名字),可传递任何类型参数 或者map类型的value可以是任何类型。

  7. 空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。

  8. 空接口类型的变量可以存储任意类型的变量。

10.3 接口的定义

值保存到空接口

var any interface{}
any = 1
fmt.Println(any)

any = "hello"
fmt.Println(any)

any = false
fmt.Println(any)

// 代码输入如下:
1
hello
false

从空接口获取值

// 声明a变量, 类型int, 初始值为1
var a int = 1

// 声明i变量, 类型为interface{}, 初始值为a, 此时i的值变为1
var i interface{} = a

// 声明b变量, 尝试赋值i
var b int = i

第8行代码编译报错,不能将i变量视为int类型赋值给b:

panic:cannot use i (type interface {}) as type int in assignment: need type assertion

为了让第 8 行的操作能够完成,编译器提示我们得使用 type assertion (类型断言),请参阅下节。

10.4 空接口的值比较

类型不同的空接口间的比较结果不相同

// a保存整型
var a interface{} = 100
// b保存字符串
var b interface{} = "hi"
// 两个空接口不相等
fmt.Println(a == b) // false

不能比较空接口中的动态值

// c保存包含10的整型切片
var c interface{} = []int{10}
// d保存包含20的整型切片
var d interface{} = []int{20}
// 这里会发生崩溃
fmt.Println(c == d)

运行到第6行时,出现运行时错误,提示 []int 是不可比较的类型:

**panic:** runtime error: comparing uncomparable type []int

列举类型及比较的几种情况

10.5 空接口的应用(常用场景)

空接口作为函数的参数

使用空接口实现可以接收任意类型的函数参数。

// 空接口作为函数参数
func show(a interface{}) {
fmt.Printf("type:%T value:%v\n", a, a)
}

空接口作为map的值

使用空接口实现可以保存任意值的字典。

// 空接口作为map值
var studentInfo = make(map[string]interface{})
studentInfo["name"] = "沙河娜扎"
studentInfo["age"] = 18
studentInfo["married"] = false
fmt.Println(studentInfo)

11. 类型断言

空接口可以存储任意类型的值,那我们如何获取其存储的具体数据呢?

11.1 介绍

当空接口进行转换数据类型时,go不像是其他语言可以直接转换,在go中需要使用断言,如下例子:

func funcName(a interface{}) string {
return string(a)
}

panic:cannot convert a (type interface{}) to type string: need type assertion

11.2 接口值

一个接口的值(简称接口值)是由一个具体类型具体类型的值两部分组成的。这两部分分别称为接口的动态类型动态值

我们来看一个具体的例子:

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

请看下图分解:

想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:

x.(T)
// x表示未知类型并且满足接口的变量;T表示断言的类型

其中:

  • x:表示类型为interface{}的变量

  • T:表示断言x可能是的类型。

  • 该语法返回两个参数,第一个参数是x转化为T类型后的变量,第二个值是一个布尔值,若为true则表示断言成功,为false则表示断言失败。

举个例子:

func main() {
var x interface{}
x = "Hello 沙河"
v, ok := x.(string)
if ok {
fmt.Println(v)
} else {
fmt.Println("类型断言失败")
}
}

上面的示例中如果要断言多次就需要写多个if判断,这个时候我们可以使用switch语句来实现:

func justifyType(x interface{}) {
switch v := x.(type) {
// 根据switch语句来断言,直到本身的类型,或者转化成功之后的操作
case string:
fmt.Printf("x is a string,value is %v\n", v)
case int:
fmt.Printf("x is a int is %v\n", v)
case bool:
fmt.Printf("x is a bool is %v\n", v)
default:
fmt.Println("unsupport type!")
}
}

因为空接口可以存储任意类型值的特点,所以空接口在Go语言中的使用十分广泛。

关于接口需要注意的是,只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗。

11.3 转换

我们可以使用语法 x.(Type) 找出接口的基础动态值,其中 x 是接口,Type是实现接口x的类型。

value, ok := x.(Type)

直接转换

❗️如果断言失败会导致panic的发生,示例:

var a interface{}
fmt.Println("Where are you,Jonny?", a.(string))

panic: interface conversion: interface {} is string, not int

尝试转换

为了防止panic的发生,我们需要在断言前进行一定的判断,如果断言失败,那么ok的值将会是false,但是如果断言成功ok的值将会是true,同时value将会得到所期待的正确的值。示例:

value, ok := a.(string)
if !ok {
fmt.Println("It's not ok for type string")
return
}
fmt.Println("The value is ", value)

类型开关

type-switch 流程控制的语法或许是Go语言中最古怪的语法。 它可以被看作是类型断言的增强版。它和 switch-case 流程控制代码块有些相似。

switch t := t.(type) {
default:
fmt.Printf("unexpected type %T", t) // %T prints whatever type t has
case bool:
fmt.Printf("boolean %t\n", t) // t has type bool
case int:
fmt.Printf("integer %d\n", t) // t has type int
case io.Writer:
fmt.Printf("interface %d\n", *t) // t has type interface
case *bool:
fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool
case *int:
fmt.Printf("pointer to integer %d\n", *t) // t has type *int
}

结构体转换

type InterfaceT interface {
ToString() string
}

type T struct {}
func (t *T) ToString() string {
return "is T"
}

func main() {
// 接口方法
var in InterfaceT = &T{}
fmt.Println(in.ToString())

// 类型转回,调用方法
in2, ok := in.(*T)
fmt.Println(in2.ToString(), ok)
}

接口转换为其他接口

package main
import "fmt"
// 定义飞行动物接口
type Flyer interface {
Fly()
}
// 定义行走动物接口
type Walker interface {
Walk()
}
// 定义鸟类
type bird struct {
}
// 实现飞行动物接口
func (b *bird) Fly() {
fmt.Println("bird: fly")
}
// 为鸟添加Walk()方法, 实现行走动物接口
func (b *bird) Walk() {
fmt.Println("bird: walk")
}
// 定义猪
type pig struct {
}
// 为猪添加Walk()方法, 实现行走动物接口
func (p *pig) Walk() {
fmt.Println("pig: walk")
}
func main() {
// 创建动物的名字到实例的映射
animals := map[string]interface{}{
"bird": new(bird),
"pig": new(pig),
}
// 遍历映射
for name, obj := range animals {
// 判断对象是否为飞行动物
f, isFlyer := obj.(Flyer)
// 判断对象是否为行走动物
w, isWalker := obj.(Walker)
fmt.Printf("name: %s isFlyer: %v isWalker: %v\n", name, isFlyer, isWalker)
// 如果是飞行动物则调用飞行动物接口
if isFlyer {
f.Fly()
}
// 如果是行走动物则调用行走动物接口
if isWalker {
w.Walk()
}
}
}

12. sort.Interface 排序

12.1 介绍

  1. 排序操作和字符串格式化是很多程序经常使用的操作。但是一个健壮的实现需要更多的代码,并且我们不希望每次我们需要的时候都重写或者拷贝这些代码。幸运的是,sort 包内置的提供了根据一些排序函数来对任何序列排序的功能。
  2. 在很多语言中,排序算法都是和序列数据类型关联,同时排序函数和具体类型元素关联。相比之下,Go语言的 sort.Sort 函数不会对具体的序列和它的元素做任何假设。相反,它使用了一个接口类型 sort.Interface 来指定通用的排序算法和可能被排序到的序列类型之间的约定。这个接口的实现由序列的具体表示和它希望排序的元素决定,序列的表示经常是一个切片。

12.2 使用

一个内置的排序算法需要知道三个东西:序列的长度,表示两个元素比较的结果,一种交换两个元素的方式;这就是 sort.Interface 的声明:

package sort

type Interface interface {
Len() int // 获取元素数量
Less(i, j int) bool // i,j是序列元素的指数。
Swap(i, j int) // 交换元素
}

我们可以通过实现sort.Interface来对字符串序列排序:

package main
import (
"fmt"
"sort"
)
// 将[]string定义为MyStringList类型
type MyStringList []string
// 实现sort.Interface接口的获取元素数量方法
func (m MyStringList) Len() int {
return len(m)
}
// 实现sort.Interface接口的比较元素方法
func (m MyStringList) Less(i, j int) bool {
return m[i] < m[j]
}
// 实现sort.Interface接口的交换元素方法
func (m MyStringList) Swap(i, j int) {
m[i], m[j] = m[j], m[i]
}
func main() {
// 准备一个内容被打乱顺序的字符串切片
names := MyStringList{
"3. Triple Kill",
"5. Penta Kill",
"2. Double Kill",
"4. Quadra Kill",
"1. First Blood",
}
// 使用sort包进行排序
sort.Sort(names)
// 遍历打印结果
for _, v := range names {
fmt.Printf("%s\n", v)
}
}

12.3 便捷

Go语言中已经提供了一些固定模式的封装以方便开发者迅速对内容进行排序。

常用到的 int32、int64、float32、bool 类型并没有由 sort 包实现,使用时依然需要开发者自己编写。

// demo
names := sort.StringSlice{
"3. Triple Kill",
"5. Penta Kill",
"2. Double Kill",
"4. Quadra Kill",
"1. First Blood",
}
sort.Sort(names)

// demo
names := []string{
"3. Triple Kill",
"5. Penta Kill",
"2. Double Kill",
"4. Quadra Kill",
"1. First Blood",
}
sort.Strings(names)

12.4 sort.Slice 切片元素排序

  1. 从 Go 1.8 开始,Go语言在 sort 包中提供了 sort.Slice() 函数进行更为简便的排序方法。

  2. sort.Slice() 函数只要求传入需要排序的数据,以及一个排序时对元素的回调函数。

  3. 使用 sort.Slice() 不仅可以完成结构体切片排序,还可以对各种切片类型进行自定义排序。

func Slice(slice interface{}, less func(i, j int) bool)

例子如下:

package main
import (
"fmt"
"sort"
)
type HeroKind int
const (
None = iota
Tank
Assassin
Mage
)
type Hero struct {
Name string
Kind HeroKind
}
func main() {
heros := []*Hero{
{"吕布", Tank},
{"李白", Assassin},
{"妲己", Mage},
{"貂蝉", Assassin},
{"关羽", Tank},
{"诸葛亮", Mage},
}
sort.Slice(heros, func(i, j int) bool {
if heros[i].Kind != heros[j].Kind {
return heros[i].Kind < heros[j].Kind
}
return heros[i].Name < heros[j].Name
})
for _, v := range heros {
fmt.Printf("%+v\n", v)
}
}

13. 常用接口

13.1 Writer

io 包中提供的 Writer 接口

type Writer interface {
Write(p []byte) (n int, err error)
}

13.2 Stringer

fmt 包中提供的 Stringer 接口,功能类似于 Java 或者 C# 语言里的 ToString 的操作。

type Stringer interface {
String() string
}

示例:

type Student struct {
name string
age int
}

func (s Student) String() string {
return fmt.Sprintf("Student: name: %s, age:%d",s.name, s.age)
}

func main() {
student := Student{name: "王", age: 18}
fmt.Println(student) // Student: name: 王, age:18
}

参考感谢
interface 接口 · 语雀 (yuque.com)
Go语言基础之接口 · 语雀 (yuque.com)
07.接口(interface) · 语雀 (yuque.com)
接口 · 语雀 (yuque.com)
Go基础—— Interface · 语雀 (yuque.com)