Go 编程 | 连载 16 - 结构体 Struct

1,501 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第13天,点击查看活动详情

一、type 关键字的作用

type 定义别名

在基本数据类型中的 byterune 其实就是 uint8int32 的别名,在源码中这些别名就是使用 type 关键字定义的,当然我们也可以自己定义别名。

// 定义别名,将原名字使用 = 赋值给别名
type alias = oldName
func main(){

   type bigInt = int64

   var balance bigInt = 100000000

   fmt.Printf("%T", balance) // int64
}

基于已有类型定义新的类型

定义新的类型要在已有类型的基础上进行定义,注意与定义类型别名作区分。

func main(){

   type tinyInt int8

   var age tinyInt = 8

   fmt.Printf("%T", age) // main.tinyInt
}

type 关键字还可以为函数定义别名、定义结构体以及定义接口。

先来看看什么是结构体以及如何使用 type 关键字定义结构体。

二、结构体 struct

在 Go 中没有类和对象的概念,但是并不代表 Go 无法实现面向对象的三大特征。Go 中通过结构体来实现。

Go 结构体要解决的问题就是如何实现面向对象的三个基本特征 封装、继承、多态 以及 方法重载抽象基类 等特征。

结构体的定义

定义结构体除了使用 type 关键字外,还需要用到结构的标识符 struct

type StructName struct{
    attr1 attrType
    attr2 attrType
    ...
}

注意结构体的属性之间是没有逗号的,不要与 Map 混淆。

func main() {
   
   // 实例化结构体
   var tom Male = Male{
      name:    "tom",
      age:     30,
      address: "tomcat",
   }

   fmt.Println(tom.name, tom.age, tom.address) // tom 30 tomcat
}

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

Go 是区分大小写的,大写的变量、函数名、接口名或者结构体名等表示可以被外界访问,如果是下小写的则表示私有的,无法被外界(其他包)访问到。Go 的大小写影响其可见性。

一般情况下都会将结构体的属性名大写,使其对外可见。

结构体的实例化

上面的代码中演示过了一种结构体实例化的方式,起始结构体实例化时还可以将属性名省略,但是要保证赋值的顺序与定义的顺序是一一对应的。

func main() {

   // 第二种实例化方式,省略属性名
   allen := Male{"allen", 18, "NYC"}

   fmt.Println(allen.name, allen.age, allen.address) // allen 18 NYC
}

结构体实例的属性值不仅可以通过结构体实例来获取,也可以通过结构体指针来直接获取

func main() {

   // 定义一个结构体指针变量
   pennyPoi := &Female{"penny", 18, "Miramar, Florida"}

   fmt.Printf("%T\n", pennyPoi)
   // 通过结构体指针获取结构体的属性值
   fmt.Println(pennyPoi.Name, pennyPoi.Age, pennyPoi.Address)
   // 通过指针获取到结构体实例,再通过结构体实例获取属性值
   penny := *pennyPoi
   fmt.Println(penny.Name, penny.Age, penny.Address)
}

type Female struct {
   Name string
   Age int
   Address string
}

执行上述代码,输出结果如下:

*main.Female
penny 18 Miramar, Florida
penny 18 Miramar, Florida

注意通过结构体指针获取结构体实例的属性值时不要通过 *pennyPoi.Name 这种方式来获取,因为这样会被误认为 pennyPoi.Name 是一个指针,要么添加括号 (*pennyPoi).Name,要么先将通过结构体指针获取到的结构体实例赋值到一个变量中,通过变量再去获取属性值。

直接通过结构体指针获取结构体的属性值其实是 Go 的一个语法糖,Go 内部会将 *pennyPoi.Name 转换为 *pennyPoi.Name

结构体实例化时如果为空值,那么结构体属性的值会赋值指定类型的默认值

func main() {

   // 零值
   nancy := Female{}

   fmt.Println(nancy.Name)
   fmt.Println(nancy.Age)
   fmt.Println(nancy.Address)
}

执行上述代码,输出结果如下:


0


除了上述方式外,还有其他方式可以零值初始化

func main() {

   var nancy2 Female
   fmt.Println(nancy2.Age) // 0
   var nancy3Poi *Female = new(Female)
   fmt.Println(nancy3Poi.Age) // 0
   // 这种方式会引起报错
   // panic: runtime error: invalid memory address or nil pointer dereference
   //var nancy4Poi *Female
   //fmt.Println(nancy4Poi.Age)

}

指针如果只声明不赋值默认是 nil,会报错,需要通过 new 函数申请内存。而结构体则可以直接通过 var 关键字初始化结构体并自动分配内存。

除了指针之外,还有 Slice Map 初始化时不会自动分配内存的,要使用 new 函数来分配内存。

结构体是值类型

从上面的代码可以确定结构体类型是可以直接通过 var 关键字直接初始化并自动分配内存的,类似的还有 数组 Array,整型 Int,浮点型 Float 以及字符串 String 多可以直接初始化并自动分配内存。

数组 Array,整型 Int,浮点型 Float 以及字符串 String 都是值类型,而 结构体 Struct 是不是也是值类型?

func main() {

   // 结构体是值类型
   p1 := Female{"Penny", 23, "NYC"}
   p2 := p1

   fmt.Printf("%v\n", &(p1.Name))
   fmt.Printf("%v\n", &(p2.Name))

   // 修改 p2
   p2.Name = "PENNY"
   fmt.Println(p2.Name)
   fmt.Println(p1.Name)
}

执行上述代码,输出结果如下:

0xc000098180
0xc0000981b0
PENNY
Penny

根据输出的 Name 属性的内存地址不同就可以确定结构体是值类型,并且修改 p2 对 p1 没有影响。

因此结构体作为函数参数传递的时候也是,值传递,既复制一个给函数作为参数使用,与原结构体互不影响

结构体占用内存大小

结构体占用内存大小可以使用 unsafe.Sizeof 函数来获取,结构体占用内存大小是固定的,不会应为存的内容的大小而改变

func main() {

   // 结构体是值类型
   p1 := Female{"Penny", 23, "NYC"}
   p2 := Female{"PennyPennyPennyPennyPennyPenny", 23, "NYC"}

   fmt.Println(unsafe.Sizeof(p1), unsafe.Sizeof(p2)) // 40 40

}

string 和 slice 底层都是结构体。