天天看点

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

文章目录

  • ​​一、结构体定义​​
  • ​​二、初始化结构体​​
  • ​​三、结构体的访问​​
  • ​​四、结构体指针​​
  • ​​五、结构体可见性​​
  • ​​六、结构体标签​​
  • ​​七、结构体嵌套​​
  • ​​八、结构体方法​​
  • ​​九、结构体特性​​

Go语言中提供了对struct的支持,struct,中文翻译称为结构体,与数组一样,属于复合类型,并非引用类型。

Go语言的struct,与C语言中的struct或其他面向对象编程语言中的类(class)类似,可以定义字段(属性)和方法,但也有很不同的地方,需要深入学习,才能区分他们之间的区别。

一、结构体定义

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

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

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

type Member struct {
    id          int
    name, email string
    gender, age int
}      

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

type Member struct {
    id     int
    name   string
    email  string
    gender int
    age    int
}      

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

ch := make(chan struct{})
ch <- struct{}{}      

二、初始化结构体

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

  • 直接定义变量

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

var m1 Member//所有字段均为空值      

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

  • 按照顺序提供初始化值:
var m2 = Member{
  1,
  "杰哥的技术杂货铺",
  "[email protected]",
  1,
  18,
}

或者
// 简短变量声明方式:
m2 := Member{
  1,
  "杰哥的技术杂货铺",
  "[email protected]",
  1,
  18
}      

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

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

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

三、结构体的访问

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

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,地址:%s\n",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,地址:%s\n",p2.name,p2.age,p2.sex,p2.address)


  //3.方法三:创建结构体对象时,直接进行赋值
  p3 := Persion{name: "蜘蛛精",age: 500,sex: "女",address: "盘丝洞"}
  fmt.Printf("姓名:%s,年龄:%d,性别:%s,地址:%s\n",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,只有初始化后,才能访问字段或为字段赋值。
var m1 *Member
m1.name = "杰哥的技术杂货铺"//错误用法,未初始化,m1为nil

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

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

var m2 = new(Member)
m2.name = "杰哥的技术杂货铺"      

五、结构体可见性

上面的例子中,我们定义结构体字段名首字母是小写的,这意味着这些字段在包外不可见,因而无法在其他包中被访问,只允许包内访问。

下面的例子中,我们将Member声明在member包中,而后在main包中创建一个变量,但由于结构体的字段包外不可见,因此无法为字段赋初始值,无法按字段还是按索引赋值,都会引发panic错误。

package member
type Member struct {
  id     int
  name   string
  email  string
  gender int
  age    int
}
package main

fun main(){
var m = member.Member{
  1,
  "杰哥的技术杂货铺",
  "[email protected]",
  1,
  18
}//会引发panic错误
}      

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

type Member struct {
    Id     int
    Name   string
    Email  string
    Gender int
    Age    int
}      

六、结构体标签

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

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"​

七、结构体嵌套

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

注意, 结构体不能包含自身,但可能包含指向自身的结构体指针。
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》,书的价格是:%.2f\n",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与函数名称之间加上具体类型变量,这个类型变量称为方法接收器,如:

注意,并不是只有结构体才能绑定方法,任何类型都可以绑定方法,只是我们这里介绍将方法绑定到结构体中。
func setName(m Member,name string){//普通函数
    m.Name = name
}

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

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

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

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

上面的代码中,我们会很奇怪,不是调用setName()方法设置了字段Name的值了吗?为什么还是输出为空呢?

这是因为,结构体是值传递,当我们调用setName时,方法接收器接收到是只是结构体变量的一个副本,通过副本对值进行修复,并不会影响调用者,因此,我们可以将方法接收器定义为指针变量,就可达到修改结构体的目的了。

func (m *Member)setName(name string){//将Member改为*Member
    m.Name = name
}

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

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

​setName​

​​,则应该将方法名改为​

​SetName​

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

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

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

九、结构体特性

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

  • 值传递

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

  • 没有继承

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

  • 结构体不能包含自己

继续阅读