# 5. GO语言结构体与面向对象

Go 语言通过用自定义的方式形成新的类型,结构体是类型中带有成员的复合类型。Go 语言使用结构体和结构体成员来描述实体和实体对应的各种属性。

结构体成员是由一系列的成员变量构成,这些成员变量也被称为“字段”。字段有以下特性:

  • 字段拥有自己的类型和值。
  • 字段名必须唯一。
  • 字段的类型也可以是结构体,甚至是字段所在结构体的类型。

Go 语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。

Go 语言的结构体与“类”都是复合结构体,但 Go 语言中结构体的内嵌配合接口比面向对象具有更高的扩展性和灵活性。

Go 语言不仅认为结构体能拥有方法,且每种自定义类型也可以拥有自己的方法。

# 5.1 结构体定义与初始化

# 5.1.1 结构体定义

使用关键字 type 可以将各种基本类型定义为自定义类型,基本类型包括整型、字符串、布尔等。结构体是一种复合的基本类型,通过 type 定义为自定义类型后,使结构体更便于使用。

结构体的定义格式如下:

type 结构体名 struct {
    字段1 类型
    字段2 类型
    …
}

示例:

//定义书本结构体
type Book struct {
	title string
	author string
	id int
	page int
}

如上示例,定义了一个 Book结构体,结构体包括 title author id page四个属性。结构体的定义只是一种内存布局的描述,只有当结构体实例化时,才会真正地分配内存。

# 5.1.2 结构体实例化与初始化

结构体的实例化跟变量实例化差不多,基本格式如下:

var 结构体实例名称 结构体名称

示例:

package main

//定义书本结构体
type Book struct {
	title string
	author string
	id int
	page int
}

func main() {
	//实例化结构体
	var myBook Book

	//给结构体成员变量赋值
	myBook.title = "go语言学习"
	myBook.author = "xjx"
	myBook.id = 1
	myBook.page = 357
}

例子中,使用.来访问结构体的成员变量,如myBook.title和 myBook.author等,结构体成员变量的赋值方法与普通变量一致。

除了直接实例化结构体外,还可以通过 new 或者 & 来直接对结构体进行实例化,以便获取指针类型的结构图,示例:

package main

//定义书本结构体
type Book struct {
	title string
	author string
	id int
	page int
}

func main() {
	//使用 & 实例化结构体
	myBook := &Book{}
	myBook.title = "go语言学习"
	myBook.author = "xjx"
	myBook.id = 1
	myBook.page = 357

	//使用 new 实例化结构体
	myBook2 := new(Book)
	myBook2.title = "go语言训练"
	myBook2.author = "xjx"
	myBook2.id = 2
	myBook2.page = 400
}

结构体也可以使用“键值对”(Key value pair)初始化字段,每个“键”(Key)对应结构体中的一个字段,键的“值”(Value)对应字段需要初始化的值。键值对初始化的格式如下:

ins := 结构体类型名{
    字段1: 值,
    字段2: 值,
    …
}
或者 
ins := 结构体类型名{
    字段1值,
    字段2值,
    …
}

代码示例:

import "fmt"

//定义书本结构体
type Book struct {
	title  string
	author string
	id     int
	page   int
}

func main() {
	//初始化化结构体
	myBook := Book{
		"go语言",
		"xjx",
		1,
		22,
	}

	//初始化化结构体
	myBook2 := Book{
		title:  "go语言2",
		author: "xjx",
		id:     2,
		page:   333,
	}
	fmt.Println(myBook)
	fmt.Println(myBook2)
}

上述代码中,一种是忽略键值,直接给出初始化值,另外一种是带上键和值,初始化值。

使用其中忽略键的方式这种格式初始化时,需要注意:

  • 必须初始化结构体的所有字段。
  • 每一个初始值的填充顺序必须与字段在结构体中的声明顺序一致。
  • 键值对与值列表的初始化形式不能混用。

而带键值得方式则不需要按此规则操作。

# 5.1.3 匿名结构体

匿名结构体没有类型名称,无须通过 type 关键字定义就可以直接使用。

匿名结构体定义格式:

变量名 := struct {
    // 匿名结构体字段定义
    字段1 字段类型1
    字段2 字段类型2
    …
}{
    // 字段值初始化
    初始化字段1: 字段1的值,
    初始化字段2: 字段2的值,
    …
}

代码如下:


package main

import "fmt"

//接收匿名结构体,打印匿名结构体信息
func printBookInfo(bookinfo *struct {
	id    int
	title string
}) {
	fmt.Println(bookinfo.id, bookinfo.title)
}

func main() {
	//定义匿名结构体
	book := &struct {
		id    int
		title string
	}{
		1, "xxxxx",
	}
	printBookInfo(book)
}

# 5.1.4 结构体tag

结构体标签是对结构体字段的额外信息标签。但是这个字符串可以通过reflect API访问到,因此不同的包可能会赋予自己的含义。 Tag 在结构体字段后方书写的格式如下:

`key1:"value1" key2:"value2"`

结构体标签由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。键值对之间使用一个空格分隔。

代码示例:

//定义一个学生结构体
type NewStudent struct {
	Id    int    `json:"Id" id:"22"`
	Name  string `json:"Name" Name:"xjx"`
	Sex   bool   `json:"Sex" Sex:"true"`
	Class int    `json:"Class" Class:"1"`
}

编写 Tag 时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,参见下面这个例子:

package main
import (
    "fmt"
    "reflect"
)
func main() {
    type cat struct {
        Name string
        Type int `json: "type" id:"100"`
    }
    typeOfCat := reflect.TypeOf(cat{})
    if catType, ok := typeOfCat.FieldByName("Type"); ok {
        fmt.Println(catType.Tag.Get("json"))
    }
}

代码输出空字符串,并不会输出期望的 type。tag在json:"type"之间增加了一个空格。这种写法没有遵守结构体标签的规则,因此无法通过 Tag.Get 获取到正确的 json 对应的值。这个错误在开发中非常容易被疏忽,造成难以察觉的错误。

# 5.2 类型定义与类型别名

类型别名是 Go 1.9 版本添加的新功能,主要用于解决代码升级、迁移中存在的类型兼容性问题。

  • 别名定义

    定义类型别名的写法为:

    type TypeAlias = Type

    类型别名规定:TypeAlias 只是 Type 的别名,本质上 TypeAlias 与 Type 是同一个类型,就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。

    示例:

    type Myint = int //定义类型别名 Myint
    
    func main() {
    	var x int = 5
    	var y Myint = 5
    	fmt.Printf("x type is %T \r\n", x)
    	fmt.Printf("y type is %T \r\n", y)
    	fmt.Printf("%t", x == y)
    }
    

    编译运行结果:

    x type is int
    y type is int
    true
    

    结果显示 x 的类型是 int,y类型是 int,Myint类型只会在代码中存在,编译完成时,不会有Myint类型。

  • 类型定义

    类型定义的写法为:

    type TypeName Type

    TypeName 为新类型名称,代码示例为:

    type MyType int //定义类型 MyType
    
    func main() {
    	var x int = 5
    	var y MyType = 5
    	fmt.Printf("x type is %T \r\n", x)
    	fmt.Printf("y type is %T \r\n", y)
    }
    

    编译运行结果为:

    x type is int
    y type is main.MyType
    

    结果显示 MyType 为新的类型,非int类型,也不能与 int类型进行运算等操作。

# 5.3 方法与接收器

接收器类型可以是(几乎)任何类型,不仅仅是结构体类型,任何类型都可以有方法,甚至可以是函数类型,可以是 int、bool、string 或数组的别名类型,但是接收器不能是一个接口类型,因为接口是一个抽象定义,而方法却是具体实现,如果这样做了就会引发一个编译错误invalid receiver type…

接收器也不能是一个指针类型,但是它可以是任何其他允许类型的指针,一个类型加上它的方法等价于面向对象中的一个类,一个重要的区别是,在Go语言中,类型的代码和绑定在它上面的方法的代码可以不放置在一起,它们可以存在不同的源文件中,唯一的要求是它们必须是同一个包的。

接收器的格式如下:

func (接收器变量 接收器类型) 方法名(参数列表) (返回参数) {
    函数体
}

接收器根据接收器的类型可以分为指针接收器、非指针接收器,两种接收器在使用时会产生不同的效果,根据效果的不同,两种接收器会被用于不同性能和功能要求的代码中。在结构体添加方法中将展开来讲。

# 5.3.1 为自定义类型添加方法

Go语言可以对任何类型添加方法,给一种类型添加方法就像给结构体添加方法一样,因为结构体也是一种类型。

如果我们要比较2个数的大小,按Go中使用面向过程我们会写成如下:

type MyType int //定义类型 MyType

func main() {
	var x MyType = 2 //定义x为MyType类型
	var y int = 5    //定义y为int类型
	if int(x) > y {  //比较x和y值大小
		fmt.Println("x > y")
	} else {
		fmt.Println("x < y")
	}
}

但如果把 MyType 看中一个整形对象,我们给 MyType 增加一个判断方法,如何写呢,示例:

type MyType int //定义类型 MyType

//定义两数比较方法
func (num MyType) compare(i int) bool {
	n := int(num)
	if n > i {
		return true
	} else {
		return false
	}
}
func main() {
	var x MyType = 5
	var y int = 6
	if x.compare(y) {
		fmt.Println("x > y")
	} else {
		fmt.Println("x < y")
	}
}

代码说明如下:

  • 首先定义 MyType 类型为int型
  • 通过接收器 为 MyType类型增加 对比函数 compare(i int)
  • main函数调用执行

另外需要注意的是, Type 能够随意地为各种类型起名字,是否意味着可以在自己包里为这些类型任意添加方法呢?参考下面示例:

package main
import (
    "time"
)
// 定义time.Duration的别名为MyDuration
type MyDuration = time.Duration

// 为MyDuration添加一个函数
func (m MyDuration) EasySet(a string) {
}

func main() {
}

代码说明如下:

  • 为 time.Duration 设定一个类型别名叫 MyDuration。
  • 为这个别名添加一个方法。

编译上面代码报错,信息如下:

cannot define new methods on non-local type time.Duration

编译器提示:不能在一个非本地的类型 time.Duration 上定义新方法,非本地类型指的就是 time.Duration 不是在 main 包中定义的,而是在 time 包中定义的,与 main 包不在同一个包中,因此不能为不在一个包中的类型定义方法。

解决这个问题有下面两种方法:

  • 将 type MyDuration time.Duration,也就是将 MyDuration 从别名改为类型;
  • 将 MyDuration 的别名定义放在 time 包中。

# 5.3.2 为结构体添加方法

  • 指针类型接收器添加方法

    指针类型的接收器由一个结构体的指针组成,更接近于面向对象中的 this 或者 self。由于指针的特性,调用方法时,修改接收器指针的任意成员变量,在方法结束后,修改都是有效的。

    在下面的例子,使用结构体定义一个结构体(MyPerson),为MyPerson添加 SetName() 方法以封装设置name的过程,通过属性的 getName() 方法可以重新获得属性的值,使用name属性时,通过 SetName() 方法的调用,可以达成修改属性值的效果。

    //定义 MyPerson 结构体
    type MyPerson struct {
    	name string //定义name属性
    	age  int    //定义age属性
    }
    
    //定义setName方法,设置人物姓名
    func (p *MyPerson) setName(name string) {
    	p.name = name
    }
    
    //定义getName方法,获取人物姓名
    func (p *MyPerson) getName() string {
    	return p.name
    }
    
    //定义getAge方法,设置人物年龄
    func (p *MyPerson) setAge(age int) {
    	p.age = age
    }
    
    //定义getAge方法,获取人物年龄
    func (p *MyPerson) getAge() int {
    	return p.age
    }
    
    func main() {
    	Person := new(MyPerson)
    	Person.setName("xjx")
    	Person.setAge(30)
    	fmt.Println(Person.getAge())
    	fmt.Println(Person.getName())
    }
    

    编译执行结果:

    30
    xjx
    
  • 非指针类型接收器添加方法

    当方法作用于非指针接收器时,Go语言会在代码运行时将接收器的值复制一份,在非指针接收器的方法中可以获取接收器的成员值,但修改后无效。

    点(Point)使用结构体描述时,为点添加 Add() 方法,这个方法不能修改 Point 的成员 X、Y 变量,而是在计算后返回新的 Point 对象,Point 属于小内存对象,在函数返回值的复制过程中可以极大地提高代码运行效率,详细过程请参考下面的代码。

    // 定义点结构
    type Point struct {
        X int
        Y int
    }
    // 非指针接收器的加方法
    func (p Point) Add(other Point) Point {
        // 成员值与参数相加后返回新的结构
        return Point{p.X + other.X, p.Y + other.Y}
    }
    func main() {
        // 初始化点
        p1 := Point{1, 1}
        p2 := Point{2, 2}
        // 与另外一个点相加
        result := p1.Add(p2)
        // 输出结果
        fmt.Println(result)
    }
    

    代码输出如下:

    {3 3}
    

    在计算机中,小对象由于值复制时的速度较快,所以适合使用非指针接收器,大对象因为复制性能较低,适合使用指针接收器,在接收器和参数间传递时不进行复制,只是传递指针。

# 5.4 结构体嵌套

结构体可以包含一个或多个匿名(或内嵌)字段,即这些字段没有显式的名字,只有字段的类型是必须的,此时类型也就是字段的名字。匿名字段本身可以是一个结构体类型,即结构体可以包含内嵌结构体。

同样地结构体也是一种数据类型,所以它也可以作为一个匿名字段来使用,如同上面例子中那样。外层结构体内嵌直接进入内层结构体的字段,内嵌结构体甚至可以来自其他包。内层结构体被简单的插入或者内嵌进外层结构体。这个简单的“继承”机制提供了一种方式,使得可以从另外一个或一些类型继承部分或全部实现。例如:

//定义S1结构体
type s1 struct {
	name string
	age  int
}

//定义S2结构体
type s2 struct {
	int        //匿名int字段
	string     //匿名string字段
	s1         //内嵌s1结构体
	data   int //名为data的int类型
}

func main() {
	s := s2{1, "zzzzz", s1{name: "qqq", age: 13}, 12} //初始化结构体
	fmt.Println(s)
}

运行结果如下所示:

{1 zzzzz {qqq 13} 12}

Go语言的结构体内嵌有如下特性:

  • 在一个结构体中对于每一种数据类型只能有一个匿名字段。

  • 内嵌的结构体可以直接访问其成员变量

  • 内嵌结构体的字段名是它的类型名

在面向对象思想中,实现对象关系需要使用“继承”特性。例如,人类不能飞行,鸟类可以飞行。人类和鸟类都可以继承自可行走类,但只有鸟类继承自飞行类。

Go语言的结构体内嵌特性就是一种组合特性,使用组合特性可以快速构建对象的不同特性。

人和鸟的特性代码示例:

package main
import "fmt"
// 可飞行的
type Flying struct{}
func (f *Flying) Fly() {
    fmt.Println("can fly")
}
// 可行走的
type Walkable struct{}
func (f *Walkable) Walk() {
    fmt.Println("can calk")
}
// 人类
type Human struct {
    Walkable // 人类能行走
}
// 鸟类
type Bird struct {
    Walkable // 鸟类能行走
    Flying   // 鸟类能飞行
}
func main() {
    // 实例化鸟类
    b := new(Bird)
    fmt.Println("Bird: ")
    b.Fly()
    b.Walk()
    // 实例化人类
    h := new(Human)
    fmt.Println("Human: ")
    h.Walk()
}

运行代码,输出如下:

Bird:
can fly
can calk
Human:
can calk

使用Go语言的内嵌结构体实现对象特性,可以自由地在对象中增、删、改各种特性。Go语言会在编译时检查能否使用这些特性。

# 5.5 接口

接口类型是对其它类型行为的抽象和概括;因为接口类型不会和特定的实现细节绑定在一起,通过这种抽象的方式我们可以让我们的函数更加灵活和更具有适应能力。

接口是双方约定的一种合作协议。接口实现者不需要关心接口会被怎样使用,调用者也不需要关心接口的实现细节。接口是一种类型,也是一种抽象结构,不会暴露所含数据的格式、类型及结构。

接口是一组仅包含方法名、参数、返回值的未具体实现的方法的集合。

# 5.5.1 接口的定义

接口声明格式:

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

对各个部分的说明:

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

代码示例:

//定义一个 Animal 接口
type Animal interface {
    Speak() string //定义一个返回值为string的方法Speak()
}

# 5.5.2 接口的实现

# 5.5.2.1 接口实现

接口定义后,需要实现接口, 实现接口需要遵循两条规则才能让接口可用:

  1. 接口的方法与实现接口的类型方法格式必须一致
  2. 接口中所有的方法均被实现

只要满足了上述两个条件,接口才能可用。

我们将实现一个返回学生信息接口,其中接口中函数返回年龄和返回姓名的函数,我们直接上代码示例:

package main

import "fmt"

//定义学生信息接口
type StudentInfo interface {
	ReturnName() string
	ReturnAge() int
}

//定义学生结构体
type MyStudent struct {
	name string
	age  int
}

//基于MyStudent实现 StudentInfo中的ReturnName接口
func (s MyStudent) ReturnName() string {
	return s.name
}

//基于MyStudent实现 StudentInfo中的ReturnAge接口
func (s MyStudent) ReturnAge() int {
	return s.age
}

func main() {
	s1 := MyStudent{"xjx", 13}                   //初始化s1
	fmt.Println(s1.ReturnName(), s1.ReturnAge()) //打印s1对象信息
	s2 := MyStudent{"hello", 11}                 //初始化s2
	fmt.Println(s2.ReturnName(), s2.ReturnAge()) //打印s2对象信息
}

代码说明如下:

  • 定义接口 StudentInfo, 接口StudentInfo含有2个函数 ReturnName()ReturnAge()
  • 定义结构体 MyStudent ,其中该结构体含有2个属性 nameage
  • 实现 接口StudentInfo 中的 ReturnName() 函数,该函数作用是返回学生姓名
  • 实现 接口StudentInfo 中的 ReturnAge() 函数,该函数作用是返回学生年龄
  • main中调用接口实现的函数

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

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

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

# 5.5.2.2 一个类型实现多个接口

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

我们举个车子启和熄火关闭的例子,首先把 CarOp 能够启动车子和关闭车子的特性使用接口来描述,请参考下面的代码:

//定义汽车结构体
type CarOp struct {
}

//实现 EngineStart 接口
func (c CarOp) EngineStart() {
	fmt.Println("车子启动了")
}

//实现 EngineClose 接口
func (c CarOp) EngineClose() {
	fmt.Println("车子关闭熄火了")
}

CarOp 结构体的 CarStart() 方法实现了 CarStart 接口:

//定义汽车启动接口
type CarStart interface {
	EngineStart()
}

CarOp 结构体的 CarClose() 方法实现了 CarClose接口:

//定义汽车熄火关闭接口
type CarClose interface {
	EngineClose()
}

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

// 使用EngineStart 的代码, 并不知道CarOp和EngineClose的存在
func usingStartEngine(carStarter CarStart) {
	carStarter.EngineStart()
}

// 使用EngineClose 的代码, 并不知道CarOp和EngineStart的存在
func usingClosedEngine(CarCloseder CarClose) {
	CarCloseder.EngineClose()
}

func main() {
	op := new(CarOp)
	usingStartEngine(op)
	usingClosedEngine(op)
}

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

# 5.5.2.3 多个类型实现相同接口

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

Service 接口定义了两个方法:一个是开启服务的方法(Start()),一个是输出日志的方法(Log())。使用 GameService 结构体来实现 Service,GameService 自己的结构只能实现 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 实现。

# 5.5.3 接口的nil判断与空接口类型(interface{})

  • 接口nil判断

    nil 在 Go语言中只能被赋值给指针和接口。接口在底层的实现有两个部分:type 和 data。在源码中,显式地将 nil 赋值给接口时,接口的 type 和 data 都将为 nil。此时,接口与 nil 值判断是相等的。但如果将一个带有类型的 nil 赋值给接口时,只有 data 为 nil,而 type 为 nil,此时,接口与 nil 判断将不相等。

    我们以学生信息为例子。直接看代码:

    //定义学生结构体
    type MyStudent struct {
    	id, age int
    	name    string
    }
    
    //定义学生信息接口
    type StudentInfo interface {
    	getInfo() string
    }
    
    //实现学生接口 getInfo函数
    func (s *MyStudent) getInfo() string {
    	return s.name
    }
    
    //获取学生信息
    func getStudentInfo() StudentInfo {
    	var s *MyStudent = nil
    	return s
    }
    
    func main() {
    	s := getStudentInfo()
    	if s == nil {
    		fmt.Println("s is nil")
    	} else {
    		fmt.Println("s not nil")
    	}
    }
    

    调试结果为:

    s not nil
    

    使用 getStudentInfo() 的返回值与 nil 判断时,虽然接口里的 s 值为 nil,但 type 带有 *MyStudent 信息,使用 == 判断相等时,依然不为 nil。

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

    func getStudentInfo() StudentInfo {
    	var s *MyStudent = nil
    	if s == nil {
    		return nil
    	}
    	return s
    }
    
  • 空接口类型(interface{})

    将值保存到空接口

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

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

    空接口的赋值如下:

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

fmt.Println(any)


代码输出如下:

```go
1
hello
false

从空接口获取值

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

func main() {
	var a int = 1
	var b interface{} = a
	var c int = b
	fmt.Println(c)
}

代码编译运行:

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

编译器告诉我们,不能将i变量视为int类型赋值给b, 为了让操作能够完成,编译器提示我们得使用 type assertion,意思就是类型断言。

我们讲代码修改为:

func main() {
	var a int = 1
	var b interface{} = a
	var c int = b.(int)
	fmt.Println(c)
}

代码编译运行:

1

空接口值比较

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

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

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

​ 编译运行结果: false

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

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

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

编译运行结果:panic: runtime error: comparing uncomparable type []int

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

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

# 5.5.4 类型断言

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

在Go语言中类型断言的语法格式如下:

value, ok := x.(T)

其中,x 表示一个接口的类型,T 表示一个具体的类型(也可为接口类型)。示例代码如下:

import "fmt"

func main() {
	var x interface{}
	x = 5
	value, ok := x.(int)
	fmt.Println(value, ok)
}

运行结果如下:

5 true

类型断言中,需要注意的是:

  • 类型断言的对象一定是 接口类型
  • 例如例子中的 value, ok := x.(int), 如果不接收第二个参数也就是上面代码中的 ok,断言失败时会直接造成一个 panic。如果 x 为 nil 同样也会 panic。

我们对需要注意的第二点举例说明:

import "fmt"

func main() {
	var x interface{}
	x = "xjx"
	value := x.(int)
	fmt.Println(value)
}

我们将 变量 x 替换为一个字符串, 在用int类型去断言,不接收ok这个参数,运行编译结果为:

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

在流程控制环节,谈到switch配合断言使用,这边再次举例说明,加深印象。代码如下:

import "fmt"

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 float64")
	default:
		fmt.Println("the type of a is default")
	}
}

func main() {
	a := 5
	getType(a)
}

将函数的参数定义为 interface{} 类型,配合接口的断言功能,巧妙的判断了变量类型, 上例子运行结果如下:the type of a is int

# 5.5.5 接口的嵌套

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

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

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

//定义A接口
type A interface {
	A()
}

//定义B接口
type B interface {
	B()
}

//定义C接口
type C interface {
	A
	B
}

//定义S结构体
type S struct {
}

func (s *S) A() {
	fmt.Println("A")
}

func (s *S) B() {
	fmt.Println("B")
}

func main() {
	var s C = new(S)
	s.A()
	s.B()
}

编译结果:

A
B
最后更新时间: 3/2/2023, 4:55:04 PM