首页 >> 大全

Go第七篇之规范的接口

2024-01-07 大全 38 作者:考证青年

每个接口类型由数个方法组成。接口的形式代码如下:

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

对各个部分的说明:

type {Write([]byte) error}

开发中常见的接口及写法

Go 语言提供的很多包中都有接口,例如 io 包中提供的 接口:

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

这个接口可以调用 Write() 方法写入一个字节数组([]byte),返回值告知写入字节数(n int)和可能发生的错误(err error)。

类似的,还有将一个对象以字符串形式展现的接口,只要实现了这个接口的类型,在调用 () 方法时,都可以获得对象对应的字符串。在 fmt 包中定义如下:

type { () }

接口在 Go 语言中的使用频率非常高,功能类似于 Java 或者 C# 语言里的 的操作。

Go 语言的每个接口中的方法数量不会很多。Go 语言希望通过一个接口精准描述它自己的功能,而通过多个接口的嵌入和组合的方式将简单的接口扩展为复杂的接口。本章后面的小节中会介绍如何使用组合来扩充接口。

Go语言实现接口的条件

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

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

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

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

数据写入器的抽象:

( "fmt")// 定义一个数据写入器type { (data {}) error}// 定义文件结构,用于实现 file {}// 实现接口的方法func (d *file) (data {}) error { // 模拟写入数据 fmt.(":", data) nil}func main() { // 实例化file f := new(file) // 声明一个的接口 var // 将接口赋值f,也就是*file类型 = f // 使用接口进行数据写入 .("data")}

代码说明如下:

运行代码,输出如下:

: data

本例中调用及实现关系如下图所示。

图:的实现过程

当类型无法实现接口时,编译器会报错,下面列出常见的几种接口无法实现的错误。

1) 函数名不一致导致的报错

在以上代码的基础上尝试修改部分代码,造成编译错误,通过编译器的报错理解如何实现接口的方法。首先,修改 file 结构的 () 方法名,将这个方法签名(第17行)修改为:

func (d *file) (data {}) error {

编译代码,报错:

use f (type *file) as type in :

*file does not ( )

报错的位置在第 33 行。报错含义是:不能将 f 变量(类型*file)视为 进行赋值。原因:*file 类型未实现 接口(丢失 方法)。

方法的签名本身是合法的。但编译器扫描到第 33 行代码时,发现尝试将 *file 类型赋值给 时,需要检查 *file 类型是否完全实现了 接口。显然,编译器因为没有找到 需要的 () 方法而报错。

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

将修改的代码恢复后,再尝试修改 () 方法,把 data 参数的类型从 {} 修改为 int 类型,代码如下:

func (d *file) (data int) error {

编译代码,报错:

use f (type *file) as type in :

*file does not (wrong type for )

have (int) error

want ( {}) error

这次未实现 的理由变为(错误的 () 方法类型)发现 (int)error,期望 ({})error。

这种方式的报错就是由实现者的方法签名与接口的方法签名不一致导致的。

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

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

在本节开头的代码中,为 中 添加一个方法,代码如下:

// 定义一个数据写入器type { (data {}) error // 能否写入 () bool}

新增 () 方法,返回 bool。此时再次编译代码,报错:

use f (type *file) as type in :

*file does not ( )

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

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

实现者在编写方法时,无法预测未来哪些方法会变为接口。一旦某个接口创建出来,要求旧的代码来实现这个接口时,就需要修改旧的代码的派生部分,这一般会造成雪崩式的重新编译。

提示

传统的派生式接口及类关系构建的模式,让类型间拥有强耦合的父子关系。这种关系一般会以“类派生图”的方式进行。经常可以看到大型软件极为复杂的派生树。随着系统的功能不断增加,这棵“派生树”会变得越来越复杂。

对于 Go 语言来说,非侵入式设计让实现者的所有类型均是平行的、组合的。如何组合则留到使用者编译时再确认。因此,使用 GO 语言时,不需要同时也不可能有“类派生图”,开发者唯一需要关注的就是“我需要什么?”,以及“我能实现什么?”。

Go语言类型与接口的关系

类型和接口之间有一对多和多对一的关系,下面将列举出这些常见的概念,以方便读者理解接口与类型在复杂环境下的实现关系。

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

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

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

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

把 能够写入数据和需要关闭的特性使用接口来描述,请参考下面的代码:

type {}func (s *) Write(p []byte) (n int, err error) { 0, nil}func (s *) Close() error { nil}

结构的 Write() 方法实现了 io. 接口:

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

同时, 结构也实现了 io. 接口:

type { Close() error}

使用 实现的 接口的代码,无须了解 接口的实现者是否具备 接口的特性。同样,使用 接口的代码也并不知道 已经实现了 接口,如下图所示。

图:接口的使用和实现过程

在代码中使用结构实现的接口和接口代码如下:

// 使用io.的代码, 并不知道和io.的存在func ( io.){ .Write( nil )}// 使用io., 并不知道和io.的存在func ( io.) { .Close()}func main() { // 实例化 s := new() (s) (s)}

() 和 () 完全独立,互相不知道对方的存在,也不知道自己使用的接口是 实现的。

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

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

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

// 一个服务需要满足能够开启和写日志的功能type { Start() // 开启服务 Log() // 日志输出}// 日志器type {}// 实现的Log()方法func (g *) Log(l ) {}// 游戏服务type { // 嵌入日志器}// 实现的Start()方法func (g *) Start() {}

代码说明如下:

此时,实例化 ,并将实例赋给 ,代码如下:

var s = new()s.Start()s.Log(“hello”)

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

示例:Go语言实现日志系统

日志可以用于查看和分析应用程序的运行状态。日志一般可以支持输出多种形式,如命令行、文件、网络等。

本例将搭建一个支持多种写入器的日志系统,可以自由扩展多种日志写入设备。

日志对外接口

本例中定义一个日志写入器接口(),要求写入设备必须遵守这个接口协议才能被日志器()注册。日志器有一个写入器的注册方法( 的 () 方法)。

日志器还有一个 Log() 方法,进行日志的输出,这个函数会将日志写入到所有已经注册的日志写入器()中,详细代码实现请参考下面的代码。

复制纯文本复制

main// 声明日志写入器接口type {Write(data {}) error}// 日志器type {// 这个日志器用到的日志写入器 []}// 注册一个日志写入器func (l *) ( ) {l. = (l., )}// 将一个data类型的数据写入日志func (l *) Log(data {}) {// 遍历所有注册的写入器for _, := range l. {// 将日志输出到每一个写入器中.Write(data)}}// 创建日志器的实例func () * { &{}}

代码说明如下:

第 4 行,声明日志写入器接口。这个接口可以被外部使用。日志的输出可以有多种设备,这个写入器就是用来实现一个日志的输出设备。

第 9 行,声明日志器结构。日志器使用 记录输出到哪些设备上。

第 15 行,使用日志器方法 () 将一个日志写入器()注册到日志器()中。注册的意思就是将日志写入器的接口添加到 中。

第 20 行,日志器的 Log() 方法可以将 {} 类型的 data 写入到注册过的日志写入器中。

第 23 行,遍历日志器拥有的所有日志写入器。

第 26 行,将本次日志的内容写入日志写入器。

第 31 行,创建日志器的实例。

这个例子中,为了最大程度地展示接口的用法,仅仅只是将数据直接写入日志写入器中。复杂一些的日志器还可以将日期、级别等信息合并到数据中一并写入日志。

文件写入器

文件写入器()是众多日志写入器()中的一种。文件写入器的功能是根据一个文件名创建日志文件( 的 方法)。在有日志写入时,将日志写入文件中。

文件写入器代码:

("""fmt""os")// 声明文件写入器type {file *os.File}// 设置文件写入器写入的文件名func (f *) ( ) (err error) {// 如果文件已经打开, 关闭前一个文件if f.file != nil {f.file.Close()}// 创建一个文件并保存文件句柄f.file, err = os.()// 如果创建的过程出现错误, 则返回错误 err}// 实现的Write()方法func (f *) Write(data {}) error {// 日志文件可能没有创建成功if f.file == nil {// 日志文件没有准备好 .New("file not ")}// 将数据序列化为字符串str := fmt.("%v\n", data)// 将数据以字节数组写入文件中_, err := f.file.Write([]byte(str)) err}// 创建文件写入器实例func () * { &{}}

代码说明如下:

在操作文件时,会出现文件无法创建、无法写入等错误。开发中尽量不要忽略这些底层报出的错误,应该处理可能发生的所有错误。

文件使用完后,要注意使用 os.File 的 Close() 方法进行及时关闭,否则文件再次访问时会因为其属性出现无法读取、无法写入等错误。

提示

一个完备的文件写入器会提供多种写入文件的模式,例子中使用的模式是将日志添加到日志文件的尾部。随着文件越来越大,文件的访问效率和查看便利性也会大大降低。此时,就需要另外一种写入模式:滚动写入文件。

滚动写入文件模式也是将日志添加到文件的尾部,但当文件达到设定的期望大小时,会自动开启一个新的文件继续写入文件,最终将获得多个日志文件。

日志文件名不仅可以按照文件大小进行分割,还可以按照日期范围进行分割。在到达设定的日期范围,如每天、每小时的周期范围时,日志器会自动创建新的日志文件。这种日志文件创建方法也能方便开发者按日志查看日志。

命令行写入器

在 UNIX 的思想中,一切皆文件。文件包括内存、磁盘、网络和命令行等。这种抽象方法方便我们访问这些看不见摸不着的虚拟资源。命令行在 Go 中也是一种文件,os. 对应标准输出,一般表示屏幕,也就是命令行,也可以被重定向为打印机或者磁盘文件;os. 对应标准错误输出,一般将错误输出到日志中,不过大多数情况,os. 会与 os. 合并输出;os.Stdin 对应标准输入,一般表示键盘。os.、os.、os.Stdin 都是 *os.File 类型,和文件一样实现了 io. 接口的 Write() 方法。

下面的代码展示如何将命令行抽象为日志写入器:

("fmt""os")// 命令行写入器type {}// 实现的Write()方法func (f *) Write(data {}) error {// 将数据序列化为字符串str := fmt.("%v\n", data)// 将数据以字节数组写入命令行中_, err := os..Write([]byte(str)) err}// 创建命令行写入器实例func () * { &{}}

代码说明如下:

除了命令行写入器()和文件写入器(),读者还可以自行使用 net 包中的 封装实现网络写入器 ,让日志可以写入远程的服务器中或者可以跨进程进行日志保存和分析。

使用日志

在程序中使用日志器一般会先通过代码创建日志器(),为日志器添加输出设备(、等)。这些设备中有一部分需要一些参数设定,如文件日志写入器需要提供文件名( 的 () 方法)。

下面代码中展示了使用日志器的过程:

接口规范和接口文档_接口规范怎么写_

"fmt"// 创建日志器func () * {// 创建日志器l := ()// 创建命令行写入器cw := ()// 注册命令行写入器到日志器中l.(cw)// 创建文件写入器fw := ()// 设置文件名if err := fw.("log.log"); err != nil {fmt.(err)}// 注册文件写入器到日志器中l.(fw) l}func main() {// 准备日志器l := ()// 写一个日志l.Log("hello")}

代码说明如下:

编译整个代码并运行,输出如下:

hello

同时,当前目录的 log.log 文件中也会出现 hello 字符。

提示

Go 语言的 log 包实现了一个小型的日志系统。这个日志系统可以在创建日志器时选择输出设备、日志前缀及 flag,函数定义如下:

 

func New(out io., , flag int) * { &{out: out, : , flag: flag}}

在 flag 中,还可以定制日志中是否输出日期、日期精度和详细文件名等。

这个日志器在编写时,也最大程度地保证了输出的效率,如果读者对日志器的编写比较感兴趣,可以在 log 包的基础上进行扩展,形成方便自己使用的日志库。

Go语言排序

排序是常见的算法之一,也是常见的面试题之一,程序员对各种排序算法也是津津乐道。实际使用中,语言的类库会为我们提供健壮、高性能的排序算法库,开发者在了解排序算法基本原理的基础上,应该避免“造轮子”,直接使用已有的排序算法库,以缩短开发周期,提高开发效率。

Go语言中在排序时,需要使用者通过 sort. 接口提供数据的一些特性和操作方法。接口定义代码如下:

type {// 获取元素数量Len() int// 小于比较Less(i, j int) bool// 交换元素Swap(i, j int)}

代码说明如下:

这个接口需要实现者实现的方法就是排序的经典操作:数量(Len)、比较(Less)、交换(Swap)。

使用sort.接口进行排序

对一系列字符串进行排序时,使用字符串切片([])承载多个字符串。使用 type 关键字,将字符串切片([])定义为自定义类型 。为了让 sort 包能识别 ,能够对 进行排序,就必须让 实现 sort. 接口。

下面是对字符串排序的详细代码(代码1):

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

代码输出结果:

1. First Blood

2. Kill

3. Kill

4. Kill

5. Penta Kill

代码说明如下:

常见类型的便捷排序

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

1) 字符串切片的便捷排序

sort 包中有一个 类型,定义如下:

type [] (p ) Len() int { len(p) }func (p ) Less(i, j int) bool { p[i] < p[j] }func (p ) Swap(i, j int) { p[i], p[j] = p[j], p[i] }// Sort is a .func (p ) Sort() { Sort(p) }

sort 包中的 的代码与 的实现代码几乎一样。因此,只需要使用 sort 包的 就可以更简单快速地进行字符串排序。将代码1中的排序代码简化后如下所示:

names := sort.{"3. Kill","5. Penta Kill","2. Kill","4. Kill","1. First Blood",}sort.Sort(names)

简化后,只要两句代码就实现了字符串排序的功能。

2) 对整型切片进行排序

除了字符串可以使用 sort 包进行便捷排序外,还可以使用 sort. 进行整型切片的排序。sort. 的定义如下:

type [] (p ) Len() int { len(p) }func (p ) Less(i, j int) bool { p[i] < p[j] }func (p ) Swap(i, j int) { p[i], p[j] = p[j], p[i] }// Sort is a .func (p ) Sort() { Sort(p) }

sort 包在 sort. 对各类型的封装上还有更进一步的简化,下面使用 sort. 继续对代码1进行简化,代码如下:

names := []{"3. Kill","5. Penta Kill","2. Kill","4. Kill","1. First Blood",}sort.(names)// 遍历打印结果for _, v := range names {fmt.("%s\n", v)}

代码说明如下:

3) sort包内建的类型排序接口一览

Go 语言中的 sort 包中定义了一些常见类型的排序方法,如下表所示。

sort 包中内建的类型排序接口 类 型实现 sort. 的类型直接排序方法说 明

字符串()

sort.(a [] )

字符 ASCII 值升序

整型(int)

sort.Ints(a []int)

数值升序

双精度浮点()

sort.(a [])

数值升序

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

对结构体数据进行排序

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

1) 完整实现sort.进行结构体排序

将一批英雄名单使用结构体定义,英雄名单的结构体中定义了英雄的名字和分类。排序时要求按照英雄的分类进行排序,相同分类的情况下按名字进行排序,详细代码实现过程如下。

结构体排序代码(代码2):

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

代码输出如下:

&{Name:关羽 Kind:1}

&{Name:吕布 Kind:1}

&{Name:李白 Kind:2}

&{Name:貂蝉 Kind:2}

&{Name:妲己 Kind:3}

&{Name:诸葛亮 Kind:3}

代码说明如下:

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

从 Go 1.8 开始,Go 语言在 sort 包中提供了 sort.Slice() 函数进行更为简便的排序方法。sort.Slice() 函数只要求传入需要排序的数据,以及一个排序时对元素的回调函数,类型为 func(i,j int)bool,sort.Slice() 函数的定义如下:

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

使用 sort.Slice() 函数,对代码2重新优化的完整代码如下:

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

第 33 行到第 39 行加粗部分是新添加的 sort.Slice() 及回调函数部分。对比前面的代码,这里去掉了 Heros 及接口实现部分的代码。

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

Go语言接口的嵌套组合

在 Go 语言中,不仅结构体与结构体之间可以嵌套,接口与接口间也可以通过嵌套创造出新的接口。

接口与接口嵌套组合而成了新接口,只要接口的所有方法被实现,则这个接口中的所有嵌套接口的方法均可以被调用。

系统包中的接口嵌套组合

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

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

代码说明如下:

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

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

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

代码说明如下:

为了整理思路,将上面的实现、调用关系使用图方式来展现,参见图 1 和图 2。

1) io.的实现及调用过程如图 1 所示。

图1:io. 的实现及调用过程

2) io. 的实现调用过程如图 2 所示。

图2:io.Write 的实现及调用过程

给 io. 或 io. 更换不同的实现者,可以动态地切换实现代码。

Go语言接口和类型之间的转换

Go 语言中使用接口断言(type )将接口转换成另外一个接口,也可以将接口转换为另外的类型。接口的转换在开发中非常常见,使用也非常频繁。

类型断言的格式

类型断言的基本格式如下:

t := i.(T)

其中,i 代表接口变量,T 代表转换的目标类型,t 代表转换后的变量。

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

t,ok := i.(T)

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

将接口转换为其他接口

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

鸟和猪具有不同的特性,鸟可以飞,猪不能飞,但两种动物都可以行走。如果使用结构体实现鸟和猪,让它们具备自己特性的 Fly() 和 Walk() 方法就让鸟和猪各自实现了飞行动物接口(Flyer)和行走动物接口()。

将鸟和猪的实例创建后,被保存到 {} 类型的 map 中。{} 类型表示空接口,意思就是这种接口可以保存为任意类型。对保存有鸟或猪的实例的 {} 变量进行断言操作,如果断言对象是断言指定的类型,则返回转换为断言对象类型的接口;如果不是指定的断言类型时,断言的第二个参数将返回 false。例如下面的代码:

var obj = new(bird)f, := obj.(Flyer)

代码中,new(bird) 产生 *bird 类型的 bird 实例,这个实例被保存在 {} 类型的 obj 变量中。使用 obj.(Flyer) 类型断言,将 obj 转换为 Flyer 接口。f 为转换成功时的 Flyer 接口类型, 表示是否转换成功,类型就是 bool。

下面是详细的代码(代码1):

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

代码说明如下:

代码输出如下:

name: pig : false : true

pig: walk

name: bird : true : true

bird: fly

bird: walk

将接口转换为其他类型

在代码 1 中,可以实现将接口转换为普通的指针类型。例如将 接口转换为 *pig 类型,请参考下面的代码:

p1 := new(pig)var a = p1p2 := a.(*pig)fmt.("p1=%p p2=%p", p1, p2)

对代码的说明如下:

如果尝试将上面这段代码中的 类型的 a 转换为 *bird 类型,将会发出运行时错误,请参考下面的代码:

p1 := new(pig)var a = p1p2 := a.(*bird)

运行时报错:

panic: : main. is *main.pig, not *main.bird

报错意思是:接口转换时,main. 接口的内部保存的是 *main.pig,而不是 *main.bird。

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

总结

接口和其他类型的转换可以在 Go 语言中自由进行,前提是已经完全实现。

接口断言类似于流程控制中的 if。但大量类型断言出现时,应使用更为高效的类型分支 特性。

Go语言空接口类型

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

提示

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

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

将值保存到空接口

空接口的赋值如下:

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

代码输出如下:

hello

false

对代码的说明:

从空接口获取值

保存到空接口的值,如果直接取出指定类型的值时,会发生编译错误,代码如下:

// 声明a变量, 类型int, 初始值为1var a int = 1// 声明i变量, 类型为{}, 初始值为a, 此时i的值变为1var i {} = a// 声明b变量, 尝试赋值ivar b int = i

第8行代码编译报错:

use i (type {}) as type int in : need

编译器告诉我们,不能将i变量视为int类型赋值给b。

在代码第 15 行中,将 a 的值赋值给 i 时,虽然 i 在赋值完成后的内部值为 int,但 i 还是一个 {} 类型的变量。类似于无论集装箱装的是茶叶还是烟草,集装箱依然是金属做的,不会因为所装物的类型改变而改变。

为了让第 8 行的操作能够完成,编译器提示我们得使用 type ,意思就是类型断言。

使用类型断言修改第 8 行代码如下:

var b int = i.(int)

修改后,代码可以编译通过,并且 b 可以获得 i 变量保存的 a 变量的值:1。

空接口的值比较

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

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

保存有类型不同的值的空接口进行比较时,Go 语言会优先比较值的类型。因此类型不同,比较结果也是不相同的,代码如下:

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

代码输出如下:

false

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

当接口中保存有动态类型的值时,运行时将触发错误,代码如下:

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

代码运行到第8行时发生崩溃:

panic: error: type []int

这是一个运行时错误,提示 []int 是不可比较的类型。下表中列举出了类型及比较的几种情况。

类型的可比较性 类 型 说 明

map

宕机错误,不可比较

切片([]T)

宕机错误,不可比较

通道()

可比较,必须由同一个 make 生成,也就是同一个通道才会是 true,否则为 false

数组([容量]T)

可比较,编译期知道两个数组是否一致

结构体

可比较,可以逐个比较结构体的值

函数

可比较

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

空接口可以保存任何类型这个特性可以方便地用于容器的设计。下面例子使用 map 和 {} 实现了一个字典。字典在其他语言中的功能和 map 类似,可以将任意类型的值做成键值对保存,然后进行找回、遍历操作。详细实现过程请参考下面的代码。

"fmt"// 字典结构type {data map[{}]{} // 键值都为{}类型}// 根据键获取值func (d *) Get(key {}) {} { d。data[key]}// 设置键值func (d *) Set(key {}, value {}) {d。data[key] = value}// 遍历所有的键值,如果回调返回值为false,停止遍历func (d *) Visit( func(k, v {}) bool) {if == nil {}for k, v := range d。data {if !(k, v) {}}}// 清空所有的数据func (d *) Clear() {d。data = make(map[{}]{})}// 创建一个字典func () * {d := &{}// 初始化mapd。

Clear() d}func main() {// 创建字典实例dict := ()// 添加游戏数据dict。Set("My ", 60)dict。Set("Terra Craft", 36)dict。Set("Don't ", 24)// 获取值及打印值 := dict。Get("Terra Craft")fmt。(":", )// 遍历所有的字典元素dict。Visit(func(key, value {}) bool {// 将值转为int类型,并判断是否大于40if value。(int) > 40 {// 输出很贵fmt。(key, "is ") true}// 默认都是输出很便宜fmt。(key, "is cheap") true})}

值设置和获取

字典内部拥有一个 data 字段,其类型为 map。这个 map 的键和值都是 {} 类型,也就是实现任意类型关联任意类型。字典的值设置和获取通过 Set() 和 Get() 两个方法来完成,参数都是 {}。详细实现代码如下:

// 字典结构type {data map[{}]{} // 键值都为{}类型}// 根据键获取值func (d *) Get(key {}) {} { d.data[key]}// 设置键值func (d *) Set(key {}, value {}) {d.data[key] = value}

代码说明如下:

遍历字段的所有键值关联数据

每个容器都有遍历操作。遍历时,需要提供一个回调返回需要遍历的数据。为了方便在必要时终止遍历操作,可以将回调的返回值设置为 bool 类型,外部逻辑在回调中不需要遍历时直接返回 false 即可终止遍历。

的 Visit() 方法需要传入回调函数,回调函数的类型为 func(k,v {})bool。每次遍历时获得的键值关联数据通过回调函数的 k 和 v 参数返回。Visit 的详细实现请参考下面的代码:

// 遍历所有的键值, 如果回调返回值为false, 停止遍历func (d *) Visit( func(k, v {}) bool) {if == nil {}for k, v := range d.data {if !(k, v) {}}}

代码说明如下:

初始化和清除

字典结构包含有 map,需要在创建 实例时初始化 map。这个过程通过 的 Clear() 方法完成。在 中调用 Clear() 方法避免了 map 初始化过程的代码重复问题。请参考下面的代码:

// 清空所有的数据func (d *) Clear() {d.data = make(map[{}]{})}// 创建一个字典func () * {d := &{}// 初始化mapd.Clear() d}

代码说明如下:

使用字典

字典实现完成后,需要经过一个测试过程,查看这个字典是否存在问题。

将一些字符串和数值组合放入到字典中,然后再从字典中根据键查询出对应的值,接着再遍历一个字典中所有的元素。详细实现过程请参考下面的代码:

func main() {// 创建字典实例dict := ()// 添加游戏数据dict.Set("My ", 60)dict.Set("Terra Craft", 36)dict.Set("Don't ", 24)// 获取值及打印值 := dict.Get("Terra Craft")fmt.(":", )// 遍历所有的字典元素dict.Visit(func(key, value {}) bool {// 将值转为int类型, 并判断是否大于40if value.(int) > 40 {// 输出“很贵”fmt.(key, "is ") true}// 默认都是输出“很便宜”fmt.(key, "is cheap") true})}

代码说明如下:

运行代码,输出如下:

: 36

My is

Terra Craft is cheap

Don't is cheap

Go语言类型分支

Go 语言的 不仅可以像其他语言一样实现数值、字符串的判断,还有一种特殊的用途——判断一个接口内保存或实现的类型。

类型断言的书写格式

实现类型分支时的写法格式如下:

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

对各个部分的说明:

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

下面的例子将一个 {} 类型的参数传给 () 函数,通过 判断 v 的类型,然后打印对应类型的提示,代码如下:

("fmt")func (v {}) { v.(type) {case int:fmt.(v, "is int")case :fmt.(v, "is ")case bool:fmt.(v, "is bool")}}func main() {(1024)("pig")(true)}

代码输出如下:

1024 is int

pig is

true is bool

代码第 9 行中,v.(type) 就是类型分支的典型写法。通过这个写法,在 的每个 case 中写的将是各种类型分支。

代码经过 时,会判断 v 这个 {} 的具体类型从而进行类型分支跳转。

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

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

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

现在电子支付逐渐成为人们普遍使用的支付方式,电子支付相比现金支付具备很多优点。例如,电子支付能够刷脸支付,而现金支付容易被偷等。使用类型分支可以方便地判断一种支付方法具备哪些特性,具体请参考下面的代码。

电子支付和现金支付:

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

代码说明如下:

运行代码,输出如下:

*main. can use

*main.Cash may be

关于我们

最火推荐

小编推荐

联系我们


版权声明:本站内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 88@qq.com 举报,一经查实,本站将立刻删除。备案号:桂ICP备2021009421号
Powered By Z-BlogPHP.
复制成功
微信号:
我知道了