1. 接口
1.1 基本介绍
接口中到底应该定义些什么?
在Go语言中,接口是声明函数的集合,但只有函数签名,没有具体的功能。
属于是面向对象中,行为的约束,面向对象中的类有自己的属性(可以当成数据成员或字段)、方法(可以当成函数,函数有自己的操作或行为),如下代码:
type 接口名称 interface {
dog() // 方法1签名
cat() // 方法2签名
方法1 (参数列表1) 返回值列表1
方法2 (参数列表2) 返回值列表2
}
上面这个接口A,要求实现者,必须实现方法1和方法2(就是命名函数时,只能用接口中的函数名命名)。
也就是说,如果有谁声称实现了某某接口,那就必须实现该接口中的所有方法。
1.2 使用接口注意事项
- 接口命名习惯在接口名后面加上er后缀。
- 参数列表、返回值列表参数名可以不写。
- 如果要在包外使用接口,接口名应该首字母大写,方法要在包外使用,方法名首字母也要大写。
- 接口中的方法应该设计合理,不要太多,甚至越少越好。
2. 接口实现
如果一个结构体实现了一个接口声明的所有方法,就说结构体实现了该接口。
一个结构体可以实现多个接口。
2.1 实现单个接口
package main
import "fmt"
// 定义一个"运动"接口
type Sport interface {
// Go倾向小接口,只有一个方法都行
run() // 跑步
jump() // 跳跃
}
// 该接口由"人"来实现
type Person struct {
name string
age int
}
// 实现接口
///实现接口方法1
func (*Person) run() {
fmt.Println("跑步")
}
///实现接口方法2
func (*Person) jump() {
fmt.Println("跳跃")
}
func main() {
p1 := new(Person) // 创建一个新的指针实例,减少数据拷贝次数
p1.jump()
p1.run()
}
==============调试结果==============
跳跃
跑步
再来看一个特殊示例
package main
import "fmt"
type Sport interface {
run() // 跑步
jump() // 跳跃
}
type Person struct {
name string
age int
}
func (*Person) run() {
fmt.Println("跑步")
}
func (*Person) jump() {
fmt.Println("跳跃")
}
// 我这里新加了一个成员方法,请问Person结构体实现了Sport接口吗?
// 当然实现了,上面两个成员方法已经实现了Sport接口,我当然可以继续定义Person结构体的其他方法。
func (t *Person) swim() {
fmt.Println("游泳")
}
func main() {
p1 := new(Person)
p1.jump()
p1.run()
p1.swim()
}
==============调试结果==============
跳跃
跑步
游泳
2.1.1 把接口类型赋值给变量
package main
import "fmt"
type Sport interface {
run()
jump()
}
type Person struct {
name string
age int
}
func (*Person) run() {
fmt.Println("跑步")
}
func (*Person) jump() {
fmt.Println("跳跃")
}
func (t *Person) swim() {
fmt.Println("游泳")
}
func main() {
p1 := new(Person)
p1.jump()
p1.run()
p1.swim()
fmt.Println("-------------------")
var a Sport = p1 // 把接口类型赋值给变量,该变量就是个接口类型的变量了
fmt.Println(a)
a.jump() // 可以直接调用接口中的方法
a.run()
}
==============调试结果==============
跳跃
跑步
游泳
-------------------
&{ 0}
跳跃
跑步
3. 接口嵌入(组合)
除了结构体可以嵌套,接口也可以。接口嵌套组合成了新接口。
type Reader interface {
Read(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
type ReadCloser interface {
Reader
Closer
}
这样写的好处,就是如果需要同时调用Reader和Closer,直接调用ReadCloser就行了。
4. 空接口
4.1 什么是空接口
在 Go 语言中,空接口(interface{})是一个没有方法的接口。这意味着任何类型都可以实现空接口(或者说空接口可以匹配任意类型的数据),因为它们不需要满足任何方法的要求。
空接口可以存储任何类型的值,这使得它在需要存储未知类型数据时非常有用。
4.2 定义空接口
4.2.1 基本定义
func main() {
var b interface{} // 空接口类型,注意是空接口类型,这部分interface{}表示数据类型
var c any // 这样也行,any是interface{}的别名(鼠标移到any上能看到)
}
4.2.2 空接口和任意数据类型组合
func main() {
var a = 500
var b interface{} // 空接口,b的类型是接口类型,但是个空接口
// var b any // 或者这样声明,更好理解,any表示任意数据类型
// 为什么b能等于a?因为interface{}是空接口,里面没有任何方法。
// 换句话说,不管什么类型,都能实现空接口。
b = a
fmt.Printf("b的类型:%T\nb的值:%+[1]v\n", b)
fmt.Println("-----------------------------------")
var c = "abc"
b = c
fmt.Printf("b的类型:%T\nb的值:%+[1]v\n", b)
}
================调试结果================
b的类型:int
b的值:500
-----------------------------------
b的类型:string
b的值:abc
4.3 为什么要用空接口
比如我有这样一个需求:我需要一个容器,该容器能存储任意类型的数据,并且能够混装,注意是能混装!
这咋整呢?用结构体吗?不行,结构体虽然能混装不同的数据类型,但它有个前提,必须要有明确的字段。
所以这种情况下只有空接口interface{}是最合适的。
var d = []any{1, 2.1, true, "abc"}
// []interface{},表示数据类型,后面的{}表示元素字面量定义
var e = []interface{}{1, 2.1, true, "abc"}
fmt.Printf("d的类型:%T|d的值:%+[1]v\n", d)
fmt.Printf("e的类型:%T|e的值:%+[1]v\n", e)
================调试结果================
d的类型:[]interface {}|d的值:[1 2.1 true abc] // 返回值为任意数据类型的切片
e的类型:[]interface {}|e的值:[1 2.1 true abc]
5. 接口类型断言
5.1 基本介绍
在 Go 语言中,当你有一个接口变量时,你可以通过 类型断言 来检查它实际存储的类型,注意只能配合接口使用。
类型断言的基本语法是 变量.(数据类型)。
5.2 示例
5.2.1 基本使用
var a = 500
var b interface{} = a
// v, ok := b.(int) 含义:把b接口中存储的数据当成int,可以吗?
// 可以:ok返回true,v返回对应的值
// 不可以:ok返回false,v返回对应数据类型的默认值(int:0,str: 空串)
v, ok := b.(int)
fmt.Printf("v=%v ok=%v\n", v, ok)
v1, ok1 := b.(string)
fmt.Printf("v=%v ok=%v\n", v1, ok1)
================调试结果================
v=500 ok=true
v= ok=false // 注意这里的v是空串
5.2.2 结合if判断
string类型,断言失败会返回空串,但万一默认就是空串呢?所以这里最好结合if一起使用,判断ok的值来判断类型断言是否成功,如下代码:
var a = 500
var b interface{} = a
if v, ok := b.(string); ok {
fmt.Printf("断言成功,v的类型为:%T 值为:%[1]v", v)
} else {
fmt.Printf("断言失败,v的类型为:%T 值为:%[1]v", v)
}
================调试结果================
断言失败,v的类型为:string 值为:
5.3 type-switch
当需要判断多个原始数据类型时,写if的话,需要写很长的代码,这里可以使用type-switch来替代if,更加简洁。
注意:type-switch只能配合接口类型断言使用。
var a = 500
var b interface{} = a
// b.(type)的意思,是取b接口中数据的类型
switch b.(type) { // 这个b.(type)可以用变量接住,值会赋值给该变量
case int:
fmt.Println("int,值=",b)
case string:
fmt.Println("string")
case bool:
fmt.Println("bool")
case nil: // 在判断接口或指针类型时,最好加上nil判断,防止出现空接口或空指针
fmt.Println("nil 空接口")
default:
fmt.Println("其他")
}
================调试结果================
int,值= 500
6. 接口输出格式化
6.1 普通方式格式化
先看一段代码:
package main
import (
"fmt"
)
type Sport interface {
run()
jump()
}
type Person struct {
name string
age int
}
func (*Person) run() {
fmt.Println("跑步")
}
func (*Person) jump() {
fmt.Println("跳跃")
}
func (t *Person) swim() {
fmt.Println("游泳")
}
func main() {
var p = new(Person)
fmt.Printf("%+v\n", p)
}
================调试结果================
&{name: age:0}
可以看到上述代码默认输出格式,看起来不太美观,可以如下优化一下:
package main
import (
"fmt"
)
type Sport interface {
run()
jump()
}
type Person struct {
name string
age int
}
func (*Person) run() {
fmt.Println("跑步")
}
func (*Person) jump() {
fmt.Println("跳跃")
}
func (t *Person) swim() {
fmt.Println("游泳")
}
func Print(p *Person) string {
return fmt.Sprintf("我叫%s,我的年龄是%d", p.name, p.age)
}
func NewPerson(name string, age int) *Person {
return &Person{name, age}
}
func main() {
var p = NewPerson("Tom", 21)
fmt.Println(Print(p))
}
================调试结果================
我叫Tom,我的年龄是21
6.2 方法格式化
package main
import (
"fmt"
)
type Sport interface {
run()
jump()
}
type Person struct {
name string
age int
}
func (*Person) run() {
fmt.Println("跑步")
}
func (*Person) jump() {
fmt.Println("跳跃")
}
func (t *Person) swim() {
fmt.Println("游泳")
}
func (p *Person) String() string {
return fmt.Sprintf("我叫%s,我的年龄是%d", p.name, p.age)
}
func NewPerson(name string, age int) *Person {
return &Person{name, age}
}
func main() {
var p = NewPerson("Tom", 21)
fmt.Printf("%+v\n", p)
fmt.Printf("%v\n", p)
fmt.Println(p)
fmt.Print(p)
}
================调试结果================
我叫Tom,我的年龄是21
我叫Tom,我的年龄是21
我叫Tom,我的年龄是21
我叫Tom,我的年龄是21