介绍
- 接口是一种类型,也是一种抽象结构的概括,不会和特定的实现细节绑定在一起,不会暴露所含数据的格式、类型及结构。
- 很多面向对象的语言都有相似的接口概念:
- 传统 java c#:
- 传统的派生式接口及类关系构建的模式,让类型间拥有强耦合的父子关系。这种关系一般会以“类派生图”的方式进行。
- 经常可以看到大型软件极复杂的派生树,随着系统的功能不断增加,这棵“派生树”会变得越来越复杂。
- 实现者在编写方法时,无法预测未来哪些方法会变为接口。一旦某个接口创建出来,要求旧的代码来实现这个接口时,就需要修改旧的代码的派生部分,这一般会造成雪崩式的重新编译。
- Go语言:
- Go的接口实现是隐式的,无须让实现接口的类型写出实现了哪些接口。这个设计称为非侵入式设计。非侵入式设计让实现者的所有类型均是平行的、组合的。如何组合则留到使用者编译时再确认。
- 这种设计可以让你创建一个新的接口类型满足已经存在的具体类型却不会去改变这些类型的定义;当我们使用的类型来自于不受我们控制的包时这种设计尤其有用。
- Go语言的每个接口中的方法数量不要很多。Go语言希望通过一个接口精准描述它自己的功能,而通过多个接口的嵌入和组合的方式将简单的接口扩展为复杂的接口。
- 因此,使用GO语言时,不需要同时也不可能有“类派生图”,开发者唯一需要关注的就是“我需要什么?”,以及“我能实现什么?”。
声明
- 接口类型名:使用 type 将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加 er,如有写操作的接口叫 Writer,有字符串功能的接口叫 Stringer,有关闭功能的接口叫 Closer 等。
- 方法名:当方法名首字母是大写时,且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
- 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以被忽略。
```go type 接口类型名 interface{ 方法名1( 参数列表1 ) 返回值列表1 方法名2( 参数列表2 ) 返回值列表2 … }
// demo type Writer interface { Write(p []byte) (n int, err error) }
---<a name="0u1mV"></a>### 实现实现接口需要满足2个条件:<a name="KFJHC"></a>#### 条件1: 接口的方法与实现接口的类型方法格式一致要求按照接口中的方法名,参数列表,返回列表一致来实现。```go// 定义一个数据写入器type DataWriter interface {WriteData(data interface{}) error}// 定义文件结构,用于实现DataWritertype file struct { }// 实现DataWriter接口的WriteData方法func (d *file) WriteData(data interface{}) error {// 模拟写入数据fmt.Println("WriteData:", data)return nil}func main() {// 实例化filevar 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
条件2: 接口中所有方法均被实现
当一个接口中有多个方法时,只有这些方法都被实现了,接口才能被正确编译并使用。
为 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()。
嵌套组合
一个接口可以包含一个或多个其他的接口,这相当于直接将这些内嵌接口的方法列举在外层接口中一样。只要接口的所有方法被实现,则这个接口中的所有嵌套接口的方法均可以被调用。
Go语言的 io 包中定义了写入器(Writer)、关闭器(Closer)和写入关闭器(WriteCloser)3 个接口:
type Writer interface {Write(p []byte) (n int, err error)}type Closer interface {Close() error}type WriteCloser interface {WriterCloser}
nil 的判断
- nil 在 Go语言中只能被赋值给指针和接口。接口在底层的实现有两个部分:type 和 data。
- 显式地将 nil 赋值给接口时,接口的 type 以及 data 都将为 nil,此时接口与 nil 值判断是相等的。
- 带类型的 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 {// 赋nilvar s *MyImplement = nil// 返回变量return s}func main() {// 判断返回值是否为nilif GetStringer() == nil {fmt.Println("GetStringer() == nil")} else {fmt.Println("GetStringer() != nil")}}
GetStringer() != nil
发现 nil 类型值返回时直接返回 nil
为了避免这类误判的问题,可以在函数返回时,发现带有 nil 的指针时直接返回 nil,代码如下:func GetStringer() fmt.Stringer {var s *MyImplement = nilif s == nil {return nil}return s}
interface{} 空接口
介绍
- 空接口是接口类型的特殊形式,空接口没有任何方法,因此任何类型都无须实现空接口。从实现的角度看,任何值都满足这个接口的需求。因此空接口类型可以保存任何值,也可以从空接口中取出原值。
- 空接口类型类似于 C# 或 Java 语言中的 Object、C语言中的 void*、C++ 中的 std::any。在泛型和模板出现前,空接口是一种非常灵活的数据抽象保存和使用的方法。
- golang中的所有程序都实现了interface{}的接口。这意味着所有的类型如string,int,int64,struct,map,slice等类型都就此拥有了interface{}的接口,这种做法和java中的Object类型比较类似。空接口在Go语言中真正的意义是支持多态。
- 空接口的内部实现保存了对象的类型和指针。使用空接口保存一个数据的过程会比直接用数据对应类型的变量保存稍慢。因此在开发中,应在需要的地方使用空接口,而不是在所有地方使用空接口。
- 在一个数据通过函数的方式传进来时,且参数为空接口,也就意味着这个参数被自动的转为interface{}的类型。
值保存到空接口
```go var any interface{} any = 1 fmt.Println(any)
any = “hello” fmt.Println(any)
any = false fmt.Println(any)
// 代码输入如下: 1 hello false
<a name="WoDVx"></a>#### 从空接口获取值```go// 声明a变量, 类型int, 初始值为1var a int = 1// 声明i变量, 类型为interface{}, 初始值为a, 此时i的值变为1var i interface{} = a// 声明b变量, 尝试赋值ivar b int = i
第8行代码编译报错,不能将i变量视为int类型赋值给b:
panic:cannot use i (type interface {}) as type int in assignment: need type assertion
为了让第 8 行的操作能够完成,编译器提示我们得使用 type assertion (类型断言),请参阅下节。
空接口的值比较
类型不同的空接口间的比较结果不相同
// 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
列举类型及比较的几种情况
| 类 型 | 说 明 |
|---|---|
| map | 宕机错误,不可比较 |
| 切片([]T) | 宕机错误,不可比较 |
| 通道(channel) | 可比较,必须由同一个 make 生成,也就是同一个通道才会是 true,否则为 false |
| 数组([容量]T) | 可比较,编译期知道两个数组是否一致 |
| 结构体 | 可比较,可以逐个比较结构体的值 |
| 函数 | 可比较 |
函数类型实现接口
举例
package mainimport ("fmt")// 调用器接口type Invoker interface {// 需要实现一个Call方法Call(interface{})}// 结构体类型type Struct struct {}// 实现Invoker的Callfunc (s *Struct) Call(p interface{}) {fmt.Println("from struct", p)}// 函数定义为类型type FuncCaller func(interface{})// 实现Invoker的Callfunc (f FuncCaller) Call(p interface{}) {// 调用f函数本体f(p)}func main() {// 声明接口变量var invoker Invoker// 实例化结构体s := new(Struct)// 将实例化的结构体赋值到接口invoker = s// 使用接口调用实例化结构体的方法Struct.Callinvoker.Call("hello")// 将匿名函数转为FuncCaller类型,再赋值给接口invoker = FuncCaller(func(v interface{}) {fmt.Println("from function", v)})// 使用接口调用FuncCaller.Call,内部会调用函数本体invoker.Call("hello")}
输出如下:
from struct hellofrom function hello
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 接口进行处理。
Type Assertion 断言
介绍
- 当空接口进行转换数据类型时,go不像是其他语言可以直接转换,在go中需要使用断言,如下例子:
panic:cannot convert a (type interface{}) to type string: need type assertionfunc funcName(a interface{}) string {return string(a)}
转换
我们可以使用语法 x.(Type) 找出接口的基础动态值,其中 x 是接口,Type是实现接口x的类型。
value, ok := x.(Type)
1. 直接转换
❗️如果断言失败会导致panic的发生,示例:
var a interface{}fmt.Println("Where are you,Jonny?", a.(string))
panic: interface conversion: interface {} is string, not int
2.尝试转换
为了防止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)
3. 类型开关
type-switch 流程控制的语法或许是Go语言中最古怪的语法。 它可以被看作是类型断言的增强版。它和 switch-case 流程控制代码块有些相似。
switch t := t.(type) {default:fmt.Printf("unexpected type %T", t) // %T prints whatever type t hascase bool:fmt.Printf("boolean %t\n", t) // t has type boolcase int:fmt.Printf("integer %d\n", t) // t has type intcase io.Writer:fmt.Printf("interface %d\n", *t) // t has type interfacecase *bool:fmt.Printf("pointer to boolean %t\n", *t) // t has type *boolcase *int:fmt.Printf("pointer to integer %d\n", *t) // t has type *int}
4. 结构体转换
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)}
5. 接口转换为其他接口
package mainimport "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()}}}
sort.Interface 排序
介绍
- 排序操作和字符串格式化是很多程序经常使用的操作。但是一个健壮的实现需要更多的代码,并且我们不希望每次我们需要的时候都重写或者拷贝这些代码。幸运的是,sort 包内置的提供了根据一些排序函数来对任何序列排序的功能。
- 在很多语言中,排序算法都是和序列数据类型关联,同时排序函数和具体类型元素关联。相比之下,Go语言的 sort.Sort 函数不会对具体的序列和它的元素做任何假设。相反,它使用了一个接口类型 sort.Interface 来指定通用的排序算法和可能被排序到的序列类型之间的约定。这个接口的实现由序列的具体表示和它希望排序的元素决定,序列的表示经常是一个切片。
使用
一个内置的排序算法需要知道三个东西:序列的长度,表示两个元素比较的结果,一种交换两个元素的方式;这就是 sort.Interface 的声明: ```go package sort
type Interface interface { Len() int // 获取元素数量 Less(i, j int) bool // i,j是序列元素的指数。 Swap(i, j int) // 交换元素 }
我们可以通过实现sort.Interface来对字符串序列排序:```gopackage mainimport ("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)}}
便捷
Go语言中已经提供了一些固定模式的封装以方便开发者迅速对内容进行排序。
| 类 型 | 实现 sort.lnterface 的类型 | 直接排序方法 | 说 明 |
|---|---|---|---|
| 字符串(String) | StringSlice | sort.Strings(a [] string) | 字符 ASCII 值升序 |
| 整型(int) | IntSlice | sort.Ints(a []int) | 数值升序 |
| 双精度浮点(float64) | Float64Slice | sort.Float64s(a []float64) | 数值升序 |
常用到的 int32、int64、float32、bool 类型并没有由 sort 包实现,使用时依然需要开发者自己编写。
// demonames := sort.StringSlice{"3. Triple Kill","5. Penta Kill","2. Double Kill","4. Quadra Kill","1. First Blood",}sort.Sort(names)// demonames := []string{"3. Triple Kill","5. Penta Kill","2. Double Kill","4. Quadra Kill","1. First Blood",}sort.Strings(names)
sort.Slice 切片元素排序
- 从 Go 1.8 开始,Go语言在 sort 包中提供了 sort.Slice() 函数进行更为简便的排序方法。
- sort.Slice() 函数只要求传入需要排序的数据,以及一个排序时对元素的回调函数。
- 使用 sort.Slice() 不仅可以完成结构体切片排序,还可以对各种切片类型进行自定义排序。
例子如下:func Slice(slice interface{}, less func(i, j int) bool)
package mainimport ("fmt""sort")type HeroKind intconst (None = iotaTankAssassinMage)type Hero struct {Name stringKind 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)}}
常用接口
Writer
介绍
io 包中提供的 Writer 接口
type Writer interface {Write(p []byte) (n int, err error)}
Stringer
介绍
fmt 包中提供的 Stringer 接口,功能类似于 Java 或者 C# 语言里的 ToString 的操作。
type Stringer interface {String() string}
示例
```go 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 } ```
