当前位置: 代码迷 >> 综合 >> Go 聚合类型 struct 和 interface:结构体与接口都实现了哪些功能?
  详细解决方案

Go 聚合类型 struct 和 interface:结构体与接口都实现了哪些功能?

热度:98   发布时间:2023-09-30 11:08:03.0

结构体


结构体定义

结构体是由一些列属性组成的复合数据类型,每个属性都具有名称、类型和值,结构体将属
性组合在一起进行由程序进行处理。
自定义类型
在 go 语言中使用 type 声明一种新的类型,语法格式为:

Go 聚合类型 struct 和 interface:结构体与接口都实现了哪些功能?

Format 可以时任意内置类型、函数签名、结构体、接口。使用自定义类型的好处是见名知道其意思。其次自定义类型可以添加方法,但是对于原始的数据类型是没有办法添加方法的。

type User map[string]stringvar c Userc = make(User)c["one"] = "1"fmt.Println(c)

也可以定义函数,对函数类型的重新定义

type Callback func()callbacks := map[string]Callback{}callbacks ["one"] = func() {fmt.Println("one")}v,ok := callbacks["one"]if ok{v()}

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

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

type person struct {name stringage uint}

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

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

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

type structName struct{fieldName typeName........}

 其中:

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

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

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

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

小提示:结构体也是一种类型,所以以后自定义的结构体,我会称为某结构体或某类型,两者是一个意思。比如 person 结构体和 person 类型其实是一个意思。 

定义好结构体后就可以使用了,因为它是一个聚合类型,所以比普通的类型可以携带更多数据。

结构体声明使用

结构体类型和普通的字符串、整型一样,也可以使用同样的方式声明和初始化。

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

var p person

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

p=person{"飞雪无情",30}

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

声明了一个结构体变量后就可以使用了,下面我们运行以下代码,验证 name 和 age 的值是否和初始化的一样。

fmt.Println(p.name,p.age)

在 Go 语言中,访问一个结构体的字段和调用一个类型的方法一样,都是使用点操作符“.”。

采用字面量初始化结构体时,初始化值的顺序很重要,必须和字段定义的顺序一致。

在 person 这个结构体中,第一个字段是 string 类型的 name,第二个字段是 uint 类型的 age,所以在初始化的时候,初始化值的类型顺序必须一一对应,才能编译通过。也就是说,在示例 {"飞雪无情",30} 中,表示 name 的字符串飞雪无情必须在前,表示年龄的数字 30 必须在后。

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

p:=person{age:30,name:"飞雪无情",}

其中,第一位我放了整型的 age,也可以编译通过,因为采用了明确的 field:value 方式进行指定,这样 Go 语言编译器会清晰地知道你要初始化哪个字段的值。

有没有发现,这种方式和 map 类型的初始化很像,都是采用冒号分隔。Go 语言尽可能地重用操作,不发明新的表达式,便于我们记忆和使用。

当然你也可以只初始化字段 age,字段 name 使用默认的零值,如下面的代码所示,仍然可以编译通过。

p:=person{age:30}

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

type person struct {name stringage uintaddr address}type address struct {province stringcity string}

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

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

    p:=person{age:30,name:"飞雪无情",addr:address{province: "北京",city:     "北京",},}

如果需要访问结构体最里层的 province 字段的值,同样也可以使用点操作符,只不过需要使用两个点,如下面的代码所示:

fmt.Println(p.addr.province)

第一个点获取 addr,第二个点获取 addr 的 province。

结构体指针


new是初始化结构体的指针,使用 new 函数进行初始化结构体指针对象

type User struct {id intname stringbirthday time.Time
}b := new(User)fmt.Printf("%T,      %v\n",b,b)*main.User,      &{0  {0 0 <nil>}}

面向对象三大思想:

封装?:在go里面实现封装使用对是结构体,在其他语言里面通过类来进行封装,通过类实现封装?

结构体 > 类(类里面一般都会有构造函数,用来创建类对应对实例的,创建结构体类型的变量可以使用New函数,看到new函数就知道是构建结构体的)

fun New(id int,name string,birthday Time) User{return User{id,name,birthday}}

如果返回的是指针类型的结构体

fun New(id int,name string,birthday Time) *User{return &User{id,name,birthday}}
type User struct {id intname stringbirthday time.Time
}func New(id int,name string,birthday time.Time) *User{return &User{id:       id,name:     name,birthday: birthday,}
}d := New(1,"jerry",time.Now())fmt.Println(d.id,d.name,d.birthday)1 jerry 2022-03-08 08:42:34.551726 +0800 CST m=+0.001138918

组合 > 继承:当前有了一个结构体,想要在之前的结构体上面扩展并且使用

type Addr struct {province string
}type User struct {id intname stringadd Addr
}

组合可以理解为结构体里面有属性定义为了另外的结构体类型。 

多态 

 

 

匿名结构体


也有匿名结构体,一般使用在项目的配置,一个项目的配置只有一份,一般会使用匿名结构体,

或者在做web前端开发,给模版上传递值。

   e := struct {id intname string}{id: 1,name: "cherry"}fmt.Println(e)

或者

 var user1 struct{id intname string}user1 = struct {id   intname string}{id: 1, name: "jerry"}fmt.Println(user1)或者直接user1 := struct {id   intname string}{id: 1, name: "jerry"}

匿名结构体一般不使用指针类型。 

 

 

接口


接口的定义

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

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

type Stringer interface {String() string}

提示:Stringer 是 Go SDK 的一个接口,属于 fmt 包。

针对 Stringer 接口来说,它会告诉调用者可以通过它的 String() 方法获取一个字符串,这就是接口的约定。至于这个字符串怎么获得的,长什么样,接口不关心,调用者也不用关心,因为这些是由接口实现者来做的。

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

func (p person) String()  string{return fmt.Sprintf("the name is %s,age is %d",p.name,p.age)}

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

注意:如果一个接口有多个方法,那么需要实现接口的每个方法才算是实现了这个接口。

实现了 Stringer 接口后就可以使用了。首先我先来定义一个可以打印 Stringer 接口的函数,如下所示: 

func printString(s fmt.Stringer){fmt.Println(s.String())}

这个被定义的函数 printString,它接收一个 Stringer 接口类型的参数,然后打印出 Stringer 接口的 String 方法返回的字符串。

printString 这个函数的优势就在于它是面向接口编程的,只要一个类型实现了 Stringer 接口,都可以打印出对应的字符串,而不用管具体的类型实现。

因为 person 实现了 Stringer 接口,所以变量 p 可以作为函数 printString 的参数,可以用如下方式打印:

printString(p)

 结果为:

the name is 飞雪无情,age is 30

现在让结构体 address 也实现 Stringer 接口,如下面的代码所示:

func (addr address) String()  string{return fmt.Sprintf("the addr is %s%s",addr.province,addr.city)}

因为结构体 address 也实现了 Stringer 接口,所以 printString 函数不用做任何改变,可以直接被使用,打印出地址,如下所示:

printString(p.addr)
//输出:the addr is 北京北京

这就是面向接口的好处,只要定义和调用双方满足约定,就可以使用,而不用管具体实现。接口的实现者也可以更好的升级重构,而不会有任何影响,因为接口约定没有变。

  相关解决方案