Go 基础 —— (八)接口

Go基础 —— 目录

(一)Go 起步
(二)变量
(三)运算符、流程控制
(四)函数
(五)容器
(六)结构体
(七)并发
(八)接口



前言

接口本身是调用方和实现方均需要遵守的一种协议,大家按照统一的方法命名参数类型和数量来协调逻辑处理的过程。

Go 语言中使用组合实现对象特性的描述。对象的内部使用结构体内嵌组合对象应该具有的特性,对外通过接口暴露能使用的特性。

Go 语言的接口设计是非侵入式的,接口编写者无须知道接口被哪些类型实现。而接口实现者只需知道实现的是什么样子的接口,但无须指明实现哪一个接口。编译器知道最终编译时使用哪个类型实现哪个接口,或者接口应该由谁来实现。

非侵入式设计是 Go 语言设计师经过多年的大项目经验总结出来的设计之道。只有让接口和实现者真正解耦,编译速度才能真正提高,项目之间的耦合度也会降低不少。

一、声明接口

1.1 声明格式

type 接口类型名 interface{
    方法名1( 参数列表1 ) 返回值列表1
    方法名2( 参数列表2 ) 返回值列表2}
// 接口类型名:使用 type 将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加 er,如有写操作的接口叫 Writer,有字符串功能的接口叫 Stringer,有关闭功能的接口叫 Closer 等。
// 方法名:当方法名首字母是大写时,且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
// 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以被忽略
type writer interface{
    Write([]byte) error
}

1.2 开发中场景的接口及写法

// io 包中提供的 Writer 接口
type Writer interface {
    Write(p []byte) (n int, err error)
}
// 这个接口可以调用 Write() 方法写入一个字节数组([]byte),返回值告知写入字节数(n int)和可能发生的错误(err error)。
// 将一个对象以字符串形式展现的接口,只要实现了这个接口的类型,在调用 String() 方法时,都可以获得对象对应的字符串。
// 在 fmt 包中定义如下:
type Stringer interface {
    String() string
}

Go语言的每个接口中的方法数量不会很多。Go语言希望通过一个接口精准描述它自己的功能,而通过多个接口的嵌入和组合的方式将简单的接口扩展为复杂的接口。

二、实现接口的条件

如果一个任意类型 T 的方法集为一个接口类型的方法集的超集,则我们说类型 T 实现了此接口类型。T 可以是一个非接口类型,也可以是一个接口类型。

实现关系在Go语言中是隐式的。两个类型之间的实现关系不需要在代码中显式地表示出来。Go语言中没有类似于implements的关键字。 Go编译器将自动在需要的时候检查两个类型之间的实现关系。

接口定义后,需要实现接口,调用方才能正确编译通过并使用接口。接口的实现需要遵循两条规则才能让接口可用。

2.1 条件一:接口的方法与实现接口的类型方法格式一致

在类型中添加与接口签名一致的方法就可以实现该方法。签名包括方法中的名称、参数列表、返回参数列表。也就是说,只要实现接口类型中的方法的名称、参数列表、返回参数列表中的任意一项与接口要实现的方法不一致,那么接口的这个方法就不会被实现。

为了抽象数据写入的过程,定义 DataWriter 接口来描述数据写入需要实现的方法,接口中的 WriteData() 方法表示将数据写入,写入方无须关心写入到哪里。实现接口的类型实现 WriteData 方法时,会具体编写将数据写入到什么结构中。这里使用file结构体实现 DataWriter 接口的 WriteData 方法,方法内部只是打印一个日志,表示有数据写入,详细实现过程请参考下面的代码。

package main
import (
    "fmt"
)
// 定义一个数据写入器
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
    f := new(file)
    // 声明一个DataWriter的接口
    var writer DataWriter
    // 将接口赋值f,也就是*file类型
    writer = f
    // 使用DataWriter接口进行数据写入
    writer.WriteData("data")
}

2.2 条件二:接口中所有方法均被实现

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

Go语言的接口实现是隐式的,无须让实现接口的类型写出实现了哪些接口。这个设计被称为非侵入式设计。

注意:

  • 传统的派生式接口及类关系构建的模式,让类型间拥有强耦合的父子关系。这种关系一般会以“类派生图”的方式进行。经常可以看到大型软件极为复杂的派生树。随着系统的功能不断增加,这棵“派生树”会变得越来越复杂。
  • 对于Go语言来说,非侵入式设计让实现者的所有类型均是平行的、组合的。如何组合则留到使用者编译时再确认。因此,使用GO语言时,不需要同时也不可能有“类派生图”,开发者唯一需要关注的就是“我需要什么?”,以及“我能实现什么?”。

三、类型与接口的关系

在Go语言中类型和接口之间有一对多和多对一的关系。

3.1 一个类型可以实现多个接口

一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。

网络上的两个程序通过一个双向的通信连接实现数据的交换,连接的一端称为一个 Socket。Socket 能够同时读取和写入数据,这个特性与文件类似。因此,开发中把文件和 Socket 都具备的读写特性抽象为独立的读写器概念。

Socket 和文件一样,在使用完毕后,也需要对资源进行释放。

// 把 Socket 能够写入数据和需要关闭的特性使用接口来描述
type Socket struct {
}
func (s *Socket) Write(p []byte) (n int, err error) {
    return 0, nil
}
func (s *Socket) Close() error {
    return nil
}
// Socket 结构的 Write() 方法实现了 io.Writer 接口
type Writer interface {
    Write(p []byte) (n int, err error)
}
// Socket 结构也实现了 io.Closer 接口
type Closer interface {
    Close() error
}

使用 Socket 实现的 Writer 接口的代码,无须了解 Writer 接口的实现者是否具备 Closer 接口的特性。同样,使用 Closer 接口的代码也并不知道 Socket 已经实现了 Writer 接口。

// 在代码中使用 Socket 结构实现的 Writer 接口和 Closer 接口代码如下
// 使用io.Writer的代码, 并不知道Socket和io.Closer的存在
func usingWriter( writer io.Writer){
    writer.Write( nil )
}
// 使用io.Closer, 并不知道Socket和io.Writer的存在
func usingCloser( closer io.Closer) {
    closer.Close()
}
func main() {
    // 实例化Socket
    s := new(Socket)
    usingWriter(s)
    usingCloser(s)
}
// usingWriter() 和 usingCloser() 完全独立,互相不知道对方的存在,也不知道自己使用的接口是 Socket 实现的。

3.2 多个类型可以实现相同的接口

一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。也就是说,使用者并不关心某个接口的方法是通过一个类型完全实现的,还是通过多个结构嵌入到一个结构体中拼凑起来共同实现的。

Service接口定义了两个方法:一个是开启服务的方法(Start()),一个是输出日志的方法(Log())。使用GameService结构体来实现ServiceGameService自己的结构只能实现Start()方法,而Service接口中的Log()方法已经被一个能输出日志的日志器(Logger)实现了,无须再进行GameService封装,或者重新实现一遍。所以,选择将 Logger嵌入到GameService`能最大程度地避免代码冗余,简化代码结构。详细实现过程如下:

// 一个服务需要满足能够开启和写日志的功能
type Service interface {
    Start()  // 开启服务
    Log(string)  // 日志输出
}
// 日志器
type Logger struct {
}
// 实现Service的Log()方法
func (g *Logger) Log(l string) {
}
// 游戏服务
type GameService struct {
    Logger  // 嵌入日志器
}
// 实现Service的Start()方法
func (g *GameService) Start() {
}
// 实例化 GameService,并将实例赋给 Service
var s Service = new(GameService)
s.Start()
s.Log(“hello”)

// s 就可以使用 Start() 方法和 Log() 方法,其中,Start() 由 GameService 实现,Log() 方法由 Logger 实现。

四、类型断言

类型断言(Type Assertion)是一个使用在接口值上的操作,用于检查接口类型变量所持有的值是否实现了期望的接口或者具体的类型。

value, ok := x.(T)
// x 表示一个接口的类型
// T 表示一个具体的类型(也可为接口类型)

该断言表达式会返回x的值(也就是value)和一个布尔值(也就是ok),可根据该布尔值判断x是否为T类型:

  • 如果T是具体某个类型,类型断言会检查x的动态类型是否等于具体类型T。如果检查成功,类型断言返回的结果是x的动态值,其类型是T
  • 如果T是接口类型,类型断言会检查x的动态类型是否满足T。如果检查成功,x的动态值不会被提取,返回值是一个类型为T的接口值。
  • 无论T是什么类型,如果xnil接口值,类型断言都会失败。
package main
import (
    "fmt"
)
func main() {
    var x interface{}
    x = 10
    value, ok := x.(int)
    fmt.Print(value, ",", ok)
}
// 如果不接收第二个参数也就是上面代码中的 ok,断言失败时会直接造成一个 panic。如果 x 为 nil 同样也会 panic。
// 类型断言还可以配合 switch 使用
package main
import (
    "fmt"
)
func main() {
    var a int
    a = 10
    getType(a)
}
func getType(a interface{}) {
    switch a.(type) {
    case int:
        fmt.Println("the type of a is int")
    case string:
        fmt.Println("the type of a is string")
    case float64:
        fmt.Println("the type of a is float")
    default:
        fmt.Println("unknown type")
    }
}

五、排序(借助sort.Interface接口)

sort包内置的提供了根据一些排序函数来对任何序列排序的功能。它的设计非常独到。在很多语言中,排序算法都是和序列数据类型关联,同时排序函数和具体类型元素关联。

Go语言的sort.Sort函数不会对具体的序列和它的元素做任何假设。它使用了一个接口类型 sort.Interface来指定通用的排序算法和可能被排序到的序列类型之间的约定。这个接口的实现由序列的具体表示和它希望排序的元素决定,序列的表示经常是一个切片。

// 一个内置的排序算法需要知道三个东西:序列的长度,表示两个元素比较的结果,一种交换两个元素的方式;这就是 sort.Interface 的三个方法:
package sort
type Interface interface {
    Len() int            // 获取元素数量
    Less(i, j int) bool // i,j是序列元素的指数。
    Swap(i, j int)        // 交换元素
}
type MyStringList  []string
func (p MyStringList ) Len() int { return len(m) }
func (p MyStringList ) Less(i, j int) bool { return m[i] < m[j] }
func (p MyStringList ) Swap(i, j int) { m[i], m[j] = m[j], m[i] }

5.1 使用sort.Interface接口进行排序

对一系列字符串进行排序时,使用字符串切片([]string)承载多个字符串。使用type关键字,将字符串切片([]string)定义为自定义类型MyStringList。为了让sort包能识别MyStringList,能够对MyStringList 进行排序,就必须让MyStringList实现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)
    }
}

5.2 常见类型的便捷排序

通过实现 sort.Interface 接口的排序过程具有很强的可定制性,可以根据被排序对象比较复杂的特性进行定制。例如,需要多种排序逻辑的需求就适合使用 sort.Interface 接口进行排序。但大部分情况中,只需要对字符串、整型等进行快速排序。Go语言中提供了一些固定模式的封装以方便开发者迅速对内容进行排序。

5.2.1 字符串切片的便捷排序

// sort 包中有一个 StringSlice 类型,定义如下:
type StringSlice []string
func (p StringSlice) Len() int           { return len(p) }
func (p StringSlice) Less(i, j int) bool { return p[i] < p[j] }
func (p StringSlice) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }
// Sort is a convenience method.
func (p StringSlice) Sort() { Sort(p) }
// sort 包中的 StringSlice 的代码与 MyStringList 的实现代码几乎一样。因此,只需要使用 sort 包的 StringSlice 就可以更简单快速地进行字符串排序。将代码1中的排序代码简化后如下所示:
names := sort.StringSlice{
    "3. Triple Kill",
    "5. Penta Kill",
    "2. Double Kill",
    "4. Quadra Kill",
    "1. First Blood",
}
sort.Sort(names)

5.2.2 对整型切片进行排序

可以使用 sort.IntSlice 进行整型切片的排序。sort.IntSlice 的定义如下:
type IntSlice []int
func (p IntSlice) Len() int           { return len(p) }
func (p IntSlice) Less(i, j int) bool { return p[i] < p[j] }
func (p IntSlice) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }
// Sort is a convenience method.
func (p IntSlice) Sort() { Sort(p) }
package main

import (
	"fmt"
	"sort"
)

func main() {
	names := sort.IntSlice{
		3,
		5,
		2,
		4,
		11,
	}
	sort.Ints(names)
	// 遍历打印结果
	for _, v := range names {
		fmt.Printf("%d\n", v)
	}
}

5.3 对结构体数据进行排序

除了基本类型的排序,也可以对结构体进行排序。结构体比基本类型更为复杂,排序时不能像数值和字符串一样拥有一些固定的单一原则。结构体的多个字段在排序中可能会存在多种排序的规则,例如,结构体中的名字按字母升序排列,数值按从小到大的顺序排序。一般在多种规则同时存在时,需要确定规则的优先度,如先按名字排序,再按年龄排序等。

5.3.1 完整实现sort.Interface进行结构体排序

package main
import (
    "fmt"
    "sort"
)
// 声明英雄的分类
type HeroKind int
// 定义HeroKind常量, 类似于枚举
const (
    None HeroKind = iota
    Tank
    Assassin
    Mage
)
// 定义英雄名单的结构
type Hero struct {
    Name string  // 英雄的名字
    Kind HeroKind  // 英雄的种类
}
// 将英雄指针的切片定义为Heros类型
type Heros []*Hero
// 实现sort.Interface接口取元素数量方法
func (s Heros) Len() int {
    return len(s)
}
// 实现sort.Interface接口比较元素方法
func (s Heros) Less(i, j int) bool {
    // 如果英雄的分类不一致时, 优先对分类进行排序
    if s[i].Kind != s[j].Kind {
        return s[i].Kind < s[j].Kind
    }
    // 默认按英雄名字字符升序排列
    return s[i].Name < s[j].Name
}
// 实现sort.Interface接口交换元素方法
func (s Heros) Swap(i, j int) {
    s[i], s[j] = s[j], s[i]
}
func main() {
    // 准备英雄列表
    heros := Heros{
        &Hero{"吕布", Tank},
        &Hero{"李白", Assassin},
        &Hero{"妲己", Mage},
        &Hero{"貂蝉", Assassin},
        &Hero{"关羽", Tank},
        &Hero{"诸葛亮", Mage},
    }
    // 使用sort包进行排序
    sort.Sort(heros)
    // 遍历英雄列表打印排序结果
    for _, v := range heros {
        fmt.Printf("%+v\n", v)
    }
}

5.3.2 使用sort.Slice进行切片元素排序

从 Go 1.8 开始,Go语言在 sort 包中提供了sort.Slice()函数进行更为简便的排序方法。sort.Slice()函数只要求传入需要排序的数据,以及一个排序时对元素的回调函数,类型为func(i,j int)boolsort.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)
    }
}

六、接口的嵌套组合

接口与接口间也可以通过嵌套创造出新的接口。

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

6.1 系统包中的接口嵌套组合

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

// 写入器(Writer),如这个接口较为常用,常用于 I/O 设备的数据写入。
type Writer interface {
    Write(p []byte) (n int, err error)
}
// 关闭器(Closer),如有非托管内存资源的对象,需要用关闭的方法来实现资源释放。
type Closer interface {
    Close() error
}
// 写入关闭器(WriteCloser),这个接口由 Writer 和 Closer 两个接口嵌入。也就是说,WriteCloser 同时拥有了 Writer 和 Closer 的特性。
type WriteCloser interface {
    Writer
    Closer
}

6.2 在代码中使用接口嵌套组合

在代码中使用io.Writerio.Closerio.WriteCloser这 3 个接口时,只需要按照接口实现的规则实现io.Writer接口和io.Closer接口即可。而io.WriteCloser接口在使用时,编译器会根据接口的实现者确认它们是否同时实现了io.Writerio.Closer接口,详细实现代码如下:

package main
import (
    "io"
)
// 声明一个设备结构
type device struct {
}
// 实现io.Writer的Write()方法
func (d *device) Write(p []byte) (n int, err error) {
    return 0, nil
}
// 实现io.Closer的Close()方法
func (d *device) Close() error {
    return nil
}
func main() {
    // 声明写入关闭器, 并赋予device的实例
    var wc io.WriteCloser = new(device)
    // 写入数据
    wc.Write(nil)
    // 关闭设备
    wc.Close()
    // 声明写入器, 并赋予device的新实例
    var writeOnly io.Writer = new(device)
    // 写入数据
    writeOnly.Write(nil)
}

七、接口和类型之间的转换

Go语言中使用接口断言(type assertions)将接口转换成另外一个接口,也可以将接口转换为另外的类型。

7.1 类型断言的格式

类型断言是一个使用在接口值上的操作。语法上它看起来像i.(T)被称为断言类型,这里i表示一个接口的类型和T表示一个类型。一个类型断言检查它操作对象的动态类型是否和断言的类型匹配。

t := i.(T)
// i 代表接口变量
// T 代表转换的目标类型
// t 代表转换后的变量。

这里有两种可能。

第一种,如果断言的类型 T 是一个具体类型,然后类型断言检查 i 的动态类型是否和 T 相同。如果这个检查成功了,类型断言的结果是 i 的动态值,当然它的类型是 T。换句话说,具体类型的类型断言从它的操作对象中获得具体的值。如果检查失败,接下来这个操作会抛出 panic。例如:

var w io.Writer
w = os.Stdout
f := w.(*os.File) // 成功: f == os.Stdout
c := w.(*bytes.Buffer) // 死机:接口保存*os.file,而不是*bytes.buffer

第二种,如果相反断言的类型 T 是一个接口类型,然后类型断言检查是否 i 的动态类型满足 T。如果这个检查成功了,动态值没有获取到;这个结果仍然是一个有相同类型和值部分的接口值,但是结果有类型 T。换句话说,对一个接口类型的类型断言改变了类型的表述方式,改变了可以获取的方法集合(通常更大),但是它保护了接口值内部的动态类型和值的部分。

在下面的第一个类型断言后,w 和 rw 都持有 os.Stdout 因此它们每个有一个动态类型 *os.File,但是变量 w 是一个 io.Writer 类型只对外公开出文件的 Write 方法,然而 rw 变量也只公开它的 Read 方法。

var w io.Writer
w = os.Stdout
rw := w.(io.ReadWriter) // 成功:*os.file具有读写功能
w = new(ByteCounter)
rw = w.(io.ReadWriter) // 死机:*字节计数器没有读取方法

如果断言操作的对象是一个 nil 接口值,那么不论被断言的类型是什么这个类型断言都会失败。几乎不需要对一个更少限制性的接口类型(更少的方法集合)做断言,因为它表现的就像赋值操作一样,除了对于 nil 接口值的情况。

如果 i 没有完全实现 T 接口的方法,这个语句将会触发宕机。触发宕机不是很友好,因此上面的语句还有一种写法:

t,ok := i.(T)
// 这种写法下,如果发生接口未实现时,将会把 ok 置为 false,t 置为 T 类型的 0 值。正常实现时,ok 为 true。这里 ok 可以被认为是:i 接口是否实现 T 类型的结果。

7.2 将接口转换为其他接口

实现某个接口的类型同时实现了另外一个接口,此时可以在两个接口间转换。

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()
        }
    }
}

7.3 将接口转换为其他类型

p1 := new(pig)
// 由于 pig 实现了 Walker 接口,因此可以被隐式转换为 Walker 接口类型保存于 a 中。
var a Walker = p1
// 由于 a 中保存的本来就是 *pig 本体,因此可以转换为 *pig 类型。
p2 := a.(*pig)
// 对比发现,p1 和 p2 指针是相同的。
fmt.Printf("p1=%p p2=%p", p1, p2)
// 如果尝试将上面这段代码中的 Walker 类型的 a 转换为 *bird 类型,将会发出运行时错误,请参考下面的代码:
p1 := new(pig)
var a Walker = p1
p2 := a.(*bird)

接口在转换为其他类型时,接口内保存的实例对应的类型指针,必须是要转换的对应的类型指针。

注意:

  • 接口和其他类型的转换可以在Go语言中自由进行,前提是已经完全实现。
  • 接口断言类似于流程控制中的if。但大量类型断言出现时,应使用更为高效的类型分支switch特性。

八、空接口类型(interface{}

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

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

8.1 将值保存到空接口

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

8.2 从空接口获取值

// 保存到空接口的值,如果直接取出指定类型的值时,会发生编译错误
// 声明a变量, 类型int, 初始值为1
var a int = 1
// 声明i变量, 类型为interface{}, 初始值为a, 此时i的值变为1
var i interface{} = a
// 声明b变量, 尝试赋值i
var b int = i
// 应使用 type assertion 类型断言
var b int = i.(int)

8.3 空接口的值比较

空接口在保存不同的值后,可以和其他变量值一样使用==进行比较操作。空接口的比较有以下几种特性。

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

// 保存有类型不同的值的空接口进行比较时,Go语言会优先比较值的类型。因此类型不同,比较结果也是不相同的
// a保存整型
var a interface{} = 100
// b保存字符串
var b interface{} = "hi"
// 两个空接口不相等
fmt.Println(a == b)

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

// 当接口中保存有动态类型的值时,运行时将触发错误
// c保存包含10的整型切片
var c interface{} = []int{10}
// d保存包含20的整型切片
var d interface{} = []int{20}
// 这里会发生崩溃
fmt.Println(c == d)

类型的可比较性

类型说明
map宕机错误,不可比较
切片([]T)宕机错误,不可比较
通道(channel)可比较,必须由同一个 make 生成,也就是同一个通道才会是 true,否则为 false
数组([容量]T)可比较,编译期知道两个数组是否一致
结构体可比较,可以逐个比较结构体的值
函数可比较

九、类型分支(switch判断空接口中变量的类型)

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

switch t := areaIntf.(type) {
case *Square:
    fmt.Printf("Type Square %T with value %v\n", t, t)
case *Circle:
    fmt.Printf("Type Circle %T with value %v\n", t, t)
case nil:
    fmt.Printf("nil value: nothing to check?\n")
default:
    fmt.Printf("Unexpected type %T\n", t)
}

变量t得到了areaIntf的值和类型, 所有case语句中列举的类型(nil除外)都必须实现对应的接口,如果被检测类型没有在case语句列举的类型中,就会执行default语句。

如果跟随在某个case关键字后的条目为一个非接口类型(用一个类型名或类型字面表示),则此非接口类型必须实现了断言值x的(接口)类型。

9.1 类型断言的书写格式

switch 接口变量.(type) {
    case 类型1:
        // 变量是类型1时的处理
    case 类型2:
        // 变量是类型2时的处理default:
        // 变量不是所有case中列举的类型时的处理
}

9.2 使用类型分支判断基本类型

// 下面的例子将一个 interface{} 类型的参数传给 printType() 函数,通过 switch 判断 v 的类型,然后打印对应类型的提示
package main
import (
    "fmt"
)
func printType(v interface{}) {
    switch v.(type) {
    case int:
        fmt.Println(v, "is int")
    case string:
        fmt.Println(v, "is string")
    case bool:
        fmt.Println(v, "is bool")
    }
}
func main() {
    printType(1024)
    printType("pig")
    printType(true)
}

switch 的 default 也是可以使用的,功能和其他的 switch 一致。

9.3 使用类型分支判断接口类型

多个接口进行类型断言时,可以使用类型分支简化判断过程。

package main
import "fmt"
// 电子支付方式
type Alipay struct {
}
// 为Alipay添加CanUseFaceID()方法, 表示电子支付方式支持刷脸
func (a *Alipay) CanUseFaceID() {
}
// 现金支付方式
type Cash struct {
}
// 为Cash添加Stolen()方法, 表示现金支付方式会出现偷窃情况
func (a *Cash) Stolen() {
}
// 具备刷脸特性的接口
type CantainCanUseFaceID interface {
    CanUseFaceID()
}
// 具备被偷特性的接口
type ContainStolen interface {
    Stolen()
}
// 打印支付方式具备的特点
func print(payMethod interface{}) {
    switch payMethod.(type) {
    case CantainCanUseFaceID:  // 可以刷脸
        fmt.Printf("%T can use faceid\n", payMethod)
    case ContainStolen:  // 可能被偷
        fmt.Printf("%T may be stolen\n", payMethod)
    }
}
func main() {
    // 使用电子支付判断
    print(new(Alipay))
    // 使用现金判断
    print(new(Cash))
}

十、error接口:返回错误信息

Go语言中引入error接口类型作为错误处理的标准模式,如果函数要返回错误,则返回值类型列表中肯定包含errorerror处理过程类似于C语言中的错误码,可逐层返回,直到被处理。

10.1 error基本用法:errors.New

// 源码
// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
    Error() string
}

error 接口有一个签名为 Error() string 的方法,所有实现该接口的类型都可以当作一个错误类型。Error() 方法给出了错误的描述,在使用 fmt.Println 打印错误时,会在内部调用 Error() string 方法来得到该错误的描述。

一般情况下,如果函数需要返回错误,就将 error 作为多个返回值中的最后一个(但这并非是强制要求)。

// 创建一个 error 最简单的方法就是调用 errors.New 函数,它会根据传入的错误信息返回一个新的 error
package main
import (
    "errors"
    "fmt"
    "math"
)
func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return -1, errors.New("math: square root of negative number")
    }
    return math.Sqrt(f), nil
}
func main() {
    result, err := Sqrt(-13)
    if err != nil {
        fmt.Println(err)
    } else {
        fmt.Println(result)
    }
}

10.2 自定义错误类型:Error()

package main
import (
    "fmt"
    "math"
)
type dualError struct {
    Num     float64
    problem string
}
func (e dualError) Error() string {
    return fmt.Sprintf("Wrong!!!,because \"%f\" is a negative number", e.Num)
}
func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return -1, dualError{Num: f}
    }
    return math.Sqrt(f), nil
}
func main() {
    result, err := Sqrt(-13)
    if err != nil {
        fmt.Println(err)
    } else {
        fmt.Println(result)
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值