从零开发区块链应用(八)--结构体初识

2022-02-22 17:53:24 浏览数 (1)

本文作者:杰哥的技术杂货铺[1]

Go 语言中提供了对 struct 的支持,struct,中文翻译称为结构体,与数组一样,属于复合类型,并非引用类型。Go 语言的 struct,与 C 语言中的 struct 或其他面向对象编程语言中的类(class)类似,可以定义字段(属性)和方法,但也有很不同的地方,需要深入学习,才能区分他们之间的区别。

一、结构体定义

Go 语言中数组可以存储同一类型的数据,但在结构体中我们可以为不同项定义不同的数据类型。

结构体是由一系列具有相同类型或不同类型的数据构成的数据集合

结构体成员是由一系列的成员变量构成,这些成员变量也被称为“字段”

代码语言:javascript复制
type Member struct {
    id          int
    name, email string
    gender, age int
}

上面的代码中,我们定义了一个包含 5 个字段的结构体,可以看到,相同类型 name 和 email、gender 和 age 在同一行中定义,但比较好的编程习惯是每一行只定义一个字段,如:

代码语言:javascript复制
type Member struct {
    id     int
    name   string
    email  string
    gender int
    age    int
}

当然,结构体也可以不包含任何字段,称为空结构体,struct{}表示一个空的结构体,注意,直接定义一个空的结构体并没有意义,但在并发编程中,channel 之间的通讯,可以使用一个 struct{}作为信号量。

代码语言:javascript复制
ch := make(chan struct{})
ch <- struct{}{}

二、初始化结构体

上面的例子中,我们定义了 Member 结构体类型,接下就可以这个自定义的类型创建变量了。

  • 直接定义变量

直接定义变量,这个使用方式并没有为字段赋初始值,因此所有字段都会被自动赋予自已类型的零值,比如 name 的值为空字符串"",age 的值为 0。

代码语言:javascript复制
var m1 Member//所有字段均为空值

另外也有使用字面量创建变量,这种使用方式,可以在大括号中为结构体的成员赋初始值,有两种赋初始值的方式:

  • 按照顺序提供初始化值:
代码语言:javascript复制
var m2 = Member{
 1,
 "杰哥的技术杂货铺",
 "jiege@163.com",
 1,
 18,
}

或者
// 简短变量声明方式:
m2 := Member{
 1,
 "杰哥的技术杂货铺",
 "jiege@163.com",
 1,
 18
}

这种方式要求所有的字段都必须赋值,因此如果字段太多,每个字段都要赋值,会很繁琐,另一种则使用字段名为指定字段赋值,如下面代码中变量 m3 的创建,使用这种方式,对于其他没有指定的字段,则使用该字段类型的零值作为初始化值。

  • 通过 field:value 的方式初始化,这样可以任意顺序
代码语言:javascript复制
var m3 = Member{
 id:2,
 "name":"杰哥的技术杂货铺"
}

或者
// 简短变量声明方式:
m3 := Member{
 id:2,
 "name":"杰哥的技术杂货铺"
}

三、结构体的访问

通过变量名,使用符号点(.),可以访问结构体类型中的字段,或为字段赋值,也可以对字段进行取址(&)操作。

代码语言:javascript复制
package main

import "fmt"

//定义结构体
type Persion struct {
 name    string
 age     int
 sex     string
 address string
}

func main()  {
 //1.方法一
 var p1 Persion
 p1.name = "杰哥的技术杂货铺"
 p1.age = 18
 p1.sex = "男"
 p1.address = "技术博客"
 fmt.Printf("姓名:%s,年龄:%d,性别:%s,地址:%sn",p1.name,p1.age,p1.sex,p1.address)

 //2.方法二
 p2 := Persion{}
 p2.name = "黄风怪"
 p2.age = 3000
 p2.sex = "男"
 p2.address = "黄风洞"
 fmt.Printf("姓名:%s,年龄:%d,性别:%s,地址:%sn",p2.name,p2.age,p2.sex,p2.address)


 //3.方法三:创建结构体对象时,直接进行赋值
 p3 := Persion{name: "蜘蛛精",age: 500,sex: "女",address: "盘丝洞"}
 fmt.Printf("姓名:%s,年龄:%d,性别:%s,地址:%sn",p3.name,p3.age,p3.sex,p3.address)

 p4 := Persion{
  name:"黄眉大王",
  age: 1000,
  sex: "男",
  address: "小雷音寺",
 }
 fmt.Println(p4)

 //4.方法四:创建结构体对象时,不写字段名,直接赋予数值
 //此种方式需要注意顺序
 p5 := Persion{"白骨精",300,"女","白骨洞"}
 fmt.Println(p5)
}

四、结构体指针

结构体与数组一样,都是值传递,比如当把数组或结构体作为实参传给函数的形参时,会复制一个副本,所以为了提高性能,一般不会把数组直接传递给函数,而是使用切片(引用类型)代替,而把结构体传给函数时,可以使用指针结构体。指针结构体,即一个指向结构体的指针,声明结构体变量时,在结构体类型前加*号,便声明一个指向结构体的指针,如:

注意,指针类型为引用类型,声明结构体指针时,如果未初始化,则初始值为 nil,只有初始化后,才能访问字段或为字段赋值。

代码语言:javascript复制
var m1 *Member
m1.name = "杰哥的技术杂货铺"//错误用法,未初始化,m1为nil

m1 = &Member{}
m1.name = "杰哥的技术杂货铺"//初始化后,结构体指针指向某个结构体地址,才能访问字段,为字段赋值。

另外,使用 Go 内置 new()函数,可以分配内存来初始化结构休,并返回分配的内存指针,因为已经初始化了,所以可以直接访问字段。

代码语言:javascript复制
var m2 = new(Member)
m2.name = "杰哥的技术杂货铺"

五、结构体可见性

上面的例子中,我们定义结构体字段名首字母是小写的,这意味着这些字段在包外不可见,因而无法在其他包中被访问,只允许包内访问。下面的例子中,我们将 Member 声明在 member 包中,而后在 main 包中创建一个变量,但由于结构体的字段包外不可见,因此无法为字段赋初始值,无法按字段还是按索引赋值,都会引发 panic 错误。

代码语言:javascript复制
package member
type Member struct {
 id     int
 name   string
 email  string
 gender int
 age    int
}
package main

fun main(){
var m = member.Member{
 1,
 "杰哥的技术杂货铺",
 "jiege@163.com",
 1,
 18
}//会引发panic错误
}

因此,如果想在一个包中访问另一个包中结构体的字段,则必须是大写字母开头的变量,即可导出的变量,如:

代码语言:javascript复制
type Member struct {
    Id     int
    Name   string
    Email  string
    Gender int
    Age    int
}

六、结构体标签

在定义结构体字段时,除字段名称和数据类型外,还可以使用反引号为结构体字段声明元信息,这种元信息称为 Tag,用于编译阶段关联到字段当中,如我们将上面例子中的结构体修改为:

代码语言:javascript复制
type Member struct {
    Id     int    `json:"id"`
    Name   string `json:"name"`
    Email  string `json:"email"`
    Gender int    `json:"gender,"`
    Age    int    `json:"age"`
}

上面例子演示的是使用 encoding/json 包编码或解码结构体时使用的 Tag 信息。

Tag 由反引号括起来的一系列用空格分隔的 key:"value"键值对组成,如:

Id int json:"id" gorm:"AUTO_INCREMENT"

七、结构体嵌套

结构体嵌套,可以理解为定义一个结构体中,其字段可以是其他的结构体,这样,不同的结构体就可以共用相同的字段。

注意, 结构体不能包含自身,但可能包含指向自身的结构体指针。

代码语言:javascript复制
package main

import (
 "fmt"
)

//1.定义一个书的结构体
type Book struct {
 bookname string
 price float64
}

//2.定义学生的结构体
type Student struct {
 name string
 age int
 book Book
}
type Student2 struct {
 name string
 age int
 book *Book  //book结构体的地址
}
func main()  {
 b1 := Book{}
 b1.bookname = "西游记"
 b1.price = 66.6

 s1 := Student{}
 s1.name = "红孩儿"
 s1.age = 18
 s1.book = b1  //值传递
 fmt.Println(b1)
 fmt.Println(s1)
 fmt.Printf("学生姓名:%s,学生年龄:%d,看的书是:《%s》,书的价格是:%.2fn",s1.name,s1.age,s1.book.bookname,s1.book.price)

 s2 := Student{name: "武松",age: 28,book: Book{bookname: "《Go语言从入门到放弃》",price: 88.8}}
 fmt.Println(s2.name,s2.age)
 fmt.Println("t",s2.book.bookname,s2.book.price)

 s3 := Student{
  name: "jack",
  age: 17,
  book: Book{
   bookname: "十万个为啥",
   price: 35.5,
  },
 }
 fmt.Println(s3.name,s3.age)
 fmt.Println("t",s3.book.bookname,s3.book.price)


 b4 := Book{bookname: "射雕英雄传",price: 76.0}
 s4 := Student2{name:"张三",age: 20,book: &b4}
 fmt.Println(b4)
 fmt.Println(s4)
 fmt.Println("t",s4.book)

 s4.book.bookname = "挪威的森林"
 fmt.Println(b4)
 fmt.Println(s4)
 fmt.Println("t",s4.book)
}

可以看到,我们定义 Student 结构体时,可以把 Book 结构体作为 Student 的字段。

八、结构体方法

在 Go 语言中,将函数绑定到具体的类型中,则称该函数是该类型的方法,其定义的方式是在 func 与函数名称之间加上具体类型变量,这个类型变量称为方法接收器,如:

注意,并不是只有结构体才能绑定方法,任何类型都可以绑定方法,只是我们这里介绍将方法绑定到结构体中。

代码语言:javascript复制
func setName(m Member,name string){//普通函数
    m.Name = name
}

func (m Member)setName(name string){//绑定到Member结构体的方法
    m.Name = name
}

从上面的例子中,我们可以看出,通过方法接收器可以访问结构体的字段,我们可以任意命名方法接收器。

调用结构体的方法,与调用字段一样:

代码语言:javascript复制
m := Member{}
m.setName("小明")
fmt.Println(m.Name)//输出为空

上面的代码中,我们会很奇怪,不是调用 setName()方法设置了字段 Name 的值了吗?为什么还是输出为空呢?这是因为,结构体是值传递,当我们调用 setName 时,方法接收器接收到是只是结构体变量的一个副本,通过副本对值进行修复,并不会影响调用者,因此,我们可以将方法接收器定义为指针变量,就可达到修改结构体的目的了。

代码语言:javascript复制
func (m *Member)setName(name string){//将Member改为*Member
    m.Name = name
}

m := Member{}
m.setName("小明")
fmt.Println(m.Name)//小明

方法和字段一样,如果首字母为小写,则只允许在包内可见,在其他包中是无法访问的,因此,如果要在其他包中访问setName,则应该将方法名改为SetName

由此我们可以看出,要想改变结构体内容时就需要使用指针接收者。

那什么时候该使用值接收者,什么时候使用指针接收者呢,可归纳为以下几点:

  • 要更改内容的时候必须使用指针接收者
  • 值接收者是 go 语言特有,因为它函数传参过程是通过值的拷贝,因此需要考虑性能问题,结构体过大也需要考虑使用指针接收者
  • 一致性:如果有指针接收者,最好都使用指针接收者
  • 值/指针接收者均可接受值/指针,定义方法的人可以随意改动接收者的类型,这并不会改变调用方式

九、结构体特性

下面总结几点结构体的相关特性:

  • 值传递

结构体与数组一样,是复合类型,无论是作为实参传递给函数时,还是赋值给其他变量,都是值传递,即复一个副本。

  • 没有继承

Go 语言是支持面向对象编程的,但却没有继承的概念,在结构体中,可以通过组合其他结构体来构建更复杂的结构体。

  • 结构体不能包含自己

一个结构体,并没有包含自身,比如 Member 中的字段不能是 Member 类型,但却可能是*Member。


本系列文章:

从零开发区块链应用(一)--golang 配置文件管理工具 viper[2]

从零开发区块链应用(二)--mysql 安装及数据库表的安装创建[3]

从零开发区块链应用(三)--mysql 初始化及 gorm 框架使用[4]

从零开发区块链应用(四)--自定义业务错误信息[5]

从零开发区块链应用(五)--golang 网络请求[6]

从零开发区块链应用(六)--gin 框架使用[7]

从零开发区块链应用(七)--gin 框架参数获取[8]

从零开发区块链应用(八)--结构体初识[9]

从零开发区块链应用(九)--区块链结构体创建[10]

从零开发区块链应用(十)--golang 协程使用[11]

从零开发区块链应用(十一)--以太坊地址生成[12]

参考资料

[1]

杰哥的技术杂货铺: https://learnblockchain.cn/people/3835

[2]

从零开发区块链应用(一)--golang配置文件管理工具viper: https://learnblockchain.cn/article/3446

[3]

从零开发区块链应用(二)--mysql安装及数据库表的安装创建: https://learnblockchain.cn/article/3447

[4]

从零开发区块链应用(三)--mysql初始化及gorm框架使用: https://learnblockchain.cn/article/3448

[5]

从零开发区块链应用(四)--自定义业务错误信息: https://learnblockchain.cn/article/3449

[6]

从零开发区块链应用(五)--golang网络请求: https://learnblockchain.cn/article/3457

[7]

从零开发区块链应用(六)--gin框架使用: https://learnblockchain.cn/article/3480

[8]

从零开发区块链应用(七)--gin框架参数获取: https://learnblockchain.cn/article/3481

[9]

从零开发区块链应用(八)--结构体初识: https://learnblockchain.cn/article/3482

[10]

从零开发区块链应用(九)--区块链结构体创建: https://learnblockchain.cn/article/3483

[11]

从零开发区块链应用(十)--golang协程使用: https://learnblockchain.cn/article/3484

[12]

从零开发区块链应用(十一)--以太坊地址生成: https://learnblockchain.cn/article/3485

0 人点赞