• Welcome to Journal web site.

我是 PHP 程序员

- 开发无止境 -

Next
Prev

6、truct 和 interface:结构体与接口

Data: 2019-10-28 02:36:59Form: JournalClick: 6

1、结构体

结构体是一种聚合类型,里面可以包含任意类型的值,这些值就是我们定义的结构体的成员,也称为字段。在 Go 语言中,要自定义一个结构体,需要使用 type+struct 关键字组合。

在下面的例子中,我自定义了一个结构体类型,名称为 person,表示一个人。这个 person 结构体有两个字段:name 代表这个人的名字,age 代表这个人的年龄。

在定义结构体时,字段的声明方法和平时声明一个变量是一样的,都是变量名在前,类型在后,只不过在结构体中,变量名称为成员名或字段名。

结构体的成员字段并不是必需的,也可以一个字段都没有,这种结构体成为空结构体。

根据以上信息,我们可以总结出结构体定义的表达式,如下面的代码所示:

其中:

  • type 和 struct 是 Go 语言的关键字,二者组合就代表要定义一个新的结构体类型。

  • structName 是结构体类型的名字。

  • fieldName 是结构体的字段名,而 typeName 是对应的字段类型。

  • 字段可以是零个、一个或者多个。

 

结构体声明使用

在下面的例子中,我声明了一个 person 类型的变量 p,因为没有对变量 p 初始化,所以默认会使用结构体里字段的零值

当然在声明一个结构体变量的时候,也可以通过结构体字面量的方式初始化,如下面的代码所示:

采用简短声明法,同时采用字面量初始化的方式,把结构体变量 p 的 name 初始化为“飞雪无情”,age 初始化为 30,以逗号分隔。

那么是否可以不按照顺序初始化呢?当然可以,只不过需要指出字段名称,如下所示:

当然你也可以只初始化字段 age,字段 name 使用默认的零值

 

字段结构体

结构体的字段可以是任意类型,也包括自定义的结构体类型,比如下面的代码:

在这个示例中,我定义了两个结构体:person 表示人,address 表示地址。在结构体 person 中,有一个 address 类型的字段 addr,这就是自定义的结构体。

通过这种方式,用代码描述现实中的实体会更匹配,复用程度也更高。对于嵌套结构体字段的结构体,其初始化和正常的结构体大同小异,只需要根据字段对应的类型初始化即可,如下面的代码所示:

 

struct 和 interface的差别

struct{} 和 interface{} 都是Go语言中的数据类型,但它们的用途和特性是不同的。

struct{} 是一个结构体类型,用于定义自定义的复合类型。它可以包含零个或多个具有不同类型的字段。结构体类型的值可以通过实例化一个结构体变量来创建。
interface{} 是一个接口类型,用于定义一组方法签名。接口类型的值可以存储实现该接口的任何类型的值。在Go中,通过实现接口方法,一个类型可以被视为实现了该接口。接口类型的值可以通过实例化一个实现该接口的类型变量来创建。
因此,struct{} 用于定义自定义的复合类型,而 interface{} 用于实现多态和抽象的概念,可以使不同的类型实现相同的方法,从而实现接口的多态性。

 

2、接口【相关阅读】

接口的定义

接口是和调用方的一种约定,它是一个高度抽象的类型,不用和具体的实现细节绑定在一起。接口要做的是定义好约定,告诉调用方自己可以做什么,但不用知道它的内部实现,这和我们见到的具体的类型如 int、map、slice 等不一样。

接口的定义和结构体稍微有些差别,虽然都以 type 关键字开始,但接口的关键字是 interface,表示自定义的类型是一个接口。也就是说 Stringer 是一个接口,它有一个方法 String() string,整体如下面的代码所示:

针对 Stringer 接口来说,它会告诉调用者可以通过它的 String() 方法获取一个字符串,这就是接口的约定。

 

接口的实现

接口的实现者必须是一个具体的类型,继续以 person 结构体为例,让它来实现 Stringer 接口,如下代码所示:

给结构体类型 person 定义一个方法,这个方法和接口里方法的签名(名称、参数和返回值)一样,这样结构体 person 就实现了 Stringer 接口。

 

值接收者和指针接收者【相关阅读】

我们已经知道,如果要实现一个接口,必须实现这个接口提供的所有方法,有值类型接收者和指针类型接收者两种。二者都可以调用方法,因为 Go 语言编译器自动做了转换,所以值类型接收者和指针类型接收者是等价的。但是在接口的实现中,值类型接收者和指针类型接收者不一样,下面我会详细分析二者的区别。

在上一小节中,已经验证了结构体类型实现了 Stringer 接口,那么结构体对应的指针是否也实现了该接口呢?我通过下面这个代码进行测试:


测试后会发现,把变量 p 的指针作为实参传给 printString 函数也是可以的,编译运行都正常。这就证明了以值类型接收者实现接口的时候,不管是类型本身,还是该类型的指针类型,都实现了该接口

示例中值接收者(p person)实现了 Stringer 接口,那么类型 person 和它的指针类型*person就都实现了 Stringer 接口。

现在,我把接收者改成指针类型,如下代码所示:


修改成指针类型接收者后会发现,示例中这行 printString(p) 代码编译不通过,提示如下错误:


意思就是类型 person 没有实现 Stringer 接口。这就证明了以指针类型接收者实现接口的时候,只有对应的指针类型才被认为实现了该接口。

我用如下表格为你总结这两种接收者类型的接口实现规则:

可以这样解读:

  • 当值类型作为接收者时,person 类型和*person类型都实现了该接口。

  • 当指针类型作为接收者时,只有*person类型实现了该接口。

可以发现,实现接口的类型都有*person,这也表明指针类型比较万能,不管哪一种接收者,它都能实现该接口。

 

工厂函数

工厂函数一般用于创建自定义的结构体,便于使用者调用,我们还是以 person 类型为例,用如下代码进行定义:

我定义了一个工厂函数 NewPerson,它接收一个 string 类型的参数,用于表示这个人的名字,同时返回一个*person。

通过工厂函数创建自定义结构体的方式,可以让调用者不用太关注结构体内部的字段,只需要给工厂函数传参就可以了。

用下面的代码,即可创建一个*person 类型的变量 p1:

工厂函数也可以用来创建一个接口,它的好处就是可以隐藏内部具体类型的实现,让调用者只需关注接口的使用即可。

现在我以 errors.New 这个 Go 语言自带的工厂函数为例,演示如何通过工厂函数创建一个接口,并隐藏其内部实现,如下代码所示:

其中,errorString 是一个结构体类型,它实现了 error 接口,所以可以通过 New 工厂函数,创建一个 *errorString 类型,通过接口 error 返回。

这就是面向接口的编程,假设重构代码,哪怕换一个其他结构体实现 error 接口,对调用者也没有影响,因为接口没变。

 

类型断言【相关阅读】

有了接口和实现接口的类型,就会有类型断言。通过方法类型断言,我们可以在值和指针之间自由转换,从而简化编程过程。
也可以用类型断言用来判断一个接口的值是否是实现该接口的某个具体类型。

还是以我们上面小节的示例演示,我们先来回忆一下它们,如下所示:

可以看到,*person 和 address 都实现了接口 Stringer,然后我通过下面的示例讲解类型断言:

如上所示,接口变量 s 称为接口 fmt.Stringer 的值,它被 p1 赋值。然后使用类型断言表达式 s.(person),尝试返回一个 p2。如果接口的值 s 是一个person,那么类型断言正确,可以正常返回 p2。如果接口的值 s 不是一个 *person,那么在运行时就会抛出异常,程序终止运行。

在上面的示例中,因为 s 的确是一个 *person,所以不会异常,可以正常返回 p2。但是如果我再添加如下代码,对 s 进行 address 类型断言,就会出现一些问题:

这个代码在编译的时候不会有问题,因为 address 实现了接口 Stringer,但是在运行的时候,会抛出如下异常信息:

这显然不符合我们的初衷,我们本来想判断一个接口的值是否是某个具体类型,但不能因为判断失败就导致程序异常。考虑到这点,Go 语言为我们提供了类型断言的多值返回,如下所示:

类型断言返回的第二个值“ok”就是断言是否成功的标志,如果为 true 则成功,否则失败。

总结

这节课虽然只讲了结构体和接口,但是所涉及的知识点很多,整节课比较长,希望你可以耐心地学完。

结构体是对现实世界的描述,接口是对某一类行为的规范和抽象。通过它们,我们可以实现代码的抽象和复用,同时可以面向接口编程,把具体实现细节隐藏起来,让写出来的代码更灵活,适应能力也更强。

Name:
<提交>