结构体part1

2023年 10月 7日 25.9k 0

此系列文章主要用来记录自己学习 Go 语言的点点滴滴,在这个系列中,我将介绍和 Go 语言相关的基础知识,语言特性,性能优化以及最佳实践。

在这里,希望有在学习或者使用 Go 语言的小伙伴,也能够参与到我的学习旅程中来,希望每一位小伙伴不仅仅只是作为阅读者,还可以提问、分享你的见解,对我的内容进行反馈。因为我深信,真正的学习是相互的,让我们携手走进 Go 语言的世界,一起学习,一起成长,期待我们在 Go 语言的道路上共同前行。

Go 语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。在 Go 语言中,通过结构体的内嵌再配合接口,能够有比面向对象更高的扩展性和灵活性。

本文开始将会介绍 Go 语言里面的结构体相关内容,主要包含以下内容:

  • 类型别名和自定义类型

    • 类型别名
    • 自定义类型
    • 类型别名和自定义类型的区别
  • 结构体

    • 结构体基础语法
    • 结构体和指针
    • 结构体内存布局

类型别名和自定义类型

在正式介绍结构体之前,我们先介绍一下类型名别和自定义类型这两个概念。

类型别名

所谓类型别名,你可以看作是给一种已经存在的类型再取一个小名,就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字其实都指的是他同一个人。

Go 语言从 1.9 版本开始新增了类型别名的功能,语法如下:

type TypeAlias = Type

第一个 type 是关键字,表示这里要声明一个类型,TypeAlias 则是对应类型别名,等号后面的 Type 则是对应的具体类型。

在 Go 语言中,内置的数据类型 rune 和 byte 其实就是 uint8 和 int32 的类型别名,他们的定义如下:

type byte = uint8
type rune = int32

自定义类型

所谓自定义类型,则是我们自己定义了一个全新的类型,其语法如下:

// 将 MyInt 定义为 int 类型
type MyInt int

在上面的代码中,我们定义了一个名为 MyInt 的类型,这是一个全新的类型,它具有 int 的特性。

可能有的同学会觉得这里很奇怪,直接用 int 不就好了么?为啥要将 int 定义为一种新的类型 MyInt ?

这里其实是给后面的结构体做铺垫,下面是一个结构体的简单示例:

type person struct {
  name string
  city string
  age  int8
}

先不管后面的 struct 以及后面的大括号那一块,这里我们能清楚的看到,定义结构体时,其实就是定义了一个自定义的类型,例如这里就是定义了一个名为 person 的类型,只不过这个类型是一个结构体。

类型别名和自定义类型的区别

接下来我们来看一看类型别名和自定义类型之间的区别。

有的人觉得就是一个有等号,一个没有等号的区别,但是这种理解是比较肤浅的,那仅仅只是语法上面表现形式的区别。

我们通过下面的这段代码来解释两者之间的区别:

package main
​
import (
  "fmt"
)
​
// 自定义类型
type NewInt int
​
// 类型别名
type MyInt = int
​
func main() {
  var a NewInt
  var b MyInt
​
  fmt.Printf("type of a:%T\n", a) //type of a:main.NewInt
  fmt.Printf("type of b:%T\n", b) //type of b:int
}

可以看到,结果显示 a 的类型是 main.NewInt,表示 main 包下定义的 NewInt 类型。b 的类型是 int。

MyInt 这种类型别名只会在代码中存在,编译完成后实际上并不会有 MyInt 类型。而 NewInt 则是一种实实在在的新类型,所以编译之后也是会存在的。

结构体

接下来我们来看一下 Go 语言里面的结构体。

在 Go 语言中,基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了。Go 语言提供了结构体的方式,可以封装多个基本数据类型。

结构体基础语法

定义结构体的语法如下:

type 类型名 struct {
    字段名 字段类型
    字段名 字段类型
    ...
}

其中:

  • 类型名:标识自定义结构体的名称,在同一个包内不能重复。
  • 字段名:表示结构体字段名。结构体中的字段名必须唯一。
  • 字段类型:表示结构体字段的具体类型。

例如,我们想要定义一个 student 的结构体:

type student struct {
  name string
  age int
  gender string
  score int
}

类型相同的字段,也可以写在一行,不过可读性稍微差那么一些:

type student struct {
  name, gender string
  age, score int
}

通过这种方式,我们就不再局限于基础数据类型只能描述单一的值,而是通过定义一个复合数据类型来描述一组值。

需要注意,上面的语法仅仅是创建了一种新的复合的数据类型而已,如果声明了类型没有使用的话是没有任何意义的。要使用结构体也非常简单,因为是一种新的类型,直接像内置类型一样使用即可:

var 变量名 结构体类型

例如:

package main
​
import (
  "fmt"
)
​
type student struct {
  name   string
  age    int
  gender string
  score  int
}
​
func main() {
  var stu student
  stu.name = "张三"
  stu.age = 18
  stu.gender = "男"
  stu.score = 100
  fmt.Println(stu)          // {张三 18 男 100}
  fmt.Printf("%v \n", stu)  // {张三 18 男 100}
  fmt.Printf("%+v \n", stu) // {name:张三 age:18 gender:男 score:100}
  fmt.Printf("%#v \n", stu) // main.student{name:"张三", age:18, gender:"男", score:100}
}

在上面的代码中,我们声明了一个名为 student 的结构体,里面有 name、age、gender、score 等字段,之后在 main 中就像使用普通内置类型一样使用了 student 类型,通过 . 的方式来访问结构体内部的成员。

另外这里顺便显示了 %v、%+v、%#v 这几种格式化输出的方式:

  • %v 按默认格式输出
  • %+v 在%v的基础上额外输出字段名
  • %#v 在%+v的基础上额外输出类型名

在某些定义临时复合数据的场景下,可以定义匿名的结构体,例如:

package main
​
import (
  "fmt"
)
​
func main() {
  var stu struct {
    name   string
    age    int
    gender string
    score  int
  }
​
  stu.name = "张三"
  stu.age = 18
  stu.gender = "男"
  stu.score = 100
  fmt.Println(stu)          // {张三 18 男 100}
  fmt.Printf("%v \n", stu)  // {张三 18 男 100}
  fmt.Printf("%+v \n", stu) // {name:张三 age:18 gender:男 score:100}
  fmt.Printf("%#v \n", stu) // main.student{name:"张三", age:18, gender:"男", score:100}
}

在上面的代码中,我们定义了一个匿名的结构体类型,然后直接用在了 stu 变量上面,由于是匿名的方式,所以该结构体是一次性的,仅仅用于指定 stu 的类型是什么样的。

除了结构体能够匿名,结构体里面的字段也可以匿名,例如:

package main
​
import (
  "fmt"
)
​
type student struct {
  string
  int
}
​
func main() {
  var stu = &student{
    "张三",
    18,
  }
  fmt.Println(stu) // &{张三 18}
  fmt.Println(stu.string) // 张三
  fmt.Println(stu.int) // 18
}

在上面的代码中,我们声明了一个名为 student 的结构,但是结构体里面字段采用的是匿名字段,但是你仔细观察后面访问 stu.string 和 stu.int 就会发现,其实并非是字段没有了,而是因为字段名和字段类型相同,所以可以进行简写。

结构体和指针

前面我们有介绍过 Go 语言里面的 指针,在使用结构体时,经常会涉及到和指针相关的操作。

先看下面的代码:

package main
​
import (
  "fmt"
)
​
type student struct {
  name   string
  age    int
  gender string
  score  int
}
​
func main() {
  var stu1 student
  var stu2 = new(student)
​
  fmt.Println(stu1)          // { 0  0}
  fmt.Printf("%+v \n", stu1) // {name: age:0 gender: score:0}
​
  fmt.Println(stu2)          // &{ 0  0}
  fmt.Printf("%+v \n", stu2) // &{name: age:0 gender: score:0}
}

在上面的代码中,我们声明了一个 student 的结构体类型,然后声明了两个变量 stu1 和 stu2,不同于 stu1,stu2 在声明的时候,使用到了 new 关键字,最终通过 new 关键字所声明的 stu2,背后存储的结构体的地址,也就是说,stu2 是一个指针变量。

因为在 Go 语言中虽然支持指针,但是仅仅支持指针的访问,并不支持指针的操作,因此我们可以直接对结构体指针使用 . 的形式来访问结构体里面的成员,例如:

package main
​
import (
  "fmt"
)
​
type student struct {
  name   string
  age    int
  gender string
  score  int
}
​
func main() {
  var stu = new(student)
  stu.name = "张三"
  stu.age = 18
  stu.gender = "男"
  stu.age = 100
​
  fmt.Println(stu) // &{张三 100 男 0}
}

另外可以使用 & 对结构体做取地址操作,这种操作等价于上面所介绍的 new 操作:

package main
​
import (
  "fmt"
)
​
type student struct {
  name   string
  age    int
  gender string
  score  int
}
​
func main() {
  var stu1 = new(student)
  var stu2 = &student{}
​
  fmt.Println(stu1) // &{ 0  0}
  fmt.Println(stu2) // &{ 0  0}
}

可以看到,上面我们分别使用 new 和 & 符号创建了两个变量,这两个变量都是指针变量,由于没有赋值,所以里面都是对应类型的零值。

相比 new 的方式,使用 & 符号时我们可以在声明变量的同时指定初始值,这样的操作会更方便一些:

package main
​
import (
  "fmt"
)
​
type student struct {
  name   string
  age    int
  gender string
  score  int
}
​
func main() {
  var stu = &student{
    name:   "Alice",
    age:    20,
    gender: "Female",
    score:  90,
  }
  fmt.Println(stu) // &{Alice 20 Female 90}
}

当某些字段没有初始值的时候,该字段可以不写。此时,没有指定初始值的字段的值就是该字段类型的零值。例如:

package main
​
import (
  "fmt"
)
​
type student struct {
  name   string
  age    int
  gender string
  score  int
}
​
func main() {
  var stu = &student{
    name:   "Alice",
    gender: "Female",
  }
  fmt.Println(stu) // &{Alice 0 Female 0}
}

指定初始值的时候还可以不写键,直接写值:

package main
​
import (
  "fmt"
)
​
type student struct {
  name   string
  age    int
  gender string
  score  int
}
​
func main() {
  var stu = &student{
    "Alice",
    20,
    "Female",
    90,
  }
  fmt.Println(stu) // &{Alice 20 Female 90}
}

使用这种格式初始化时,需要注意:

  • 必须初始化结构体的所有字段
  • 初始值的填充顺序必须与字段在结构体中的声明顺序一致
  • 该方式不能和键值初始化方式混用

仔细观察下面的初始化代码:

package main
​
import (
  "fmt"
)
​
type student struct {
  name   string
  age    int
  gender string
  score  int
}
​
func main() {
  stu1 := student{
    name:   "Bill",
    age:    20,
    gender: "Male",
    score:  100,
  }
  stu2 := &student{
    name:   "Alice",
    age:    20,
    gender: "Female",
    score:  90,
  }
  fmt.Println(stu1) // {Bill 20 Male 100}
  fmt.Println(stu2) // &{Alice 20 Female 90}
}

在上面的代码中,我们在声明 stu1 和 stu2 时,都在声明的同时对变量进行了初始化值的操作,但是区别在于一个用了 & 符号,另一个没有,两者之间是有区别的:

  • stu1 是 student 类型的一个实例,也就是说它的值是直接存储在变量 stu1 中的。
  • stu2 是一个指向 student 类型实例的指针,它的值是存储在内存的某个位置,而 stu2 存储的是这个位置的地址。

在 Go 语言中,结构体类型的变量默认是值类型,也就是说当你把 stu1 赋值给另一个变量时,会创建一个新的、与 stu1 有着相同值的 student 实例。任何对新变量的改变都不会影响到原变量 stu1。

例如:

stu3 := stu1
stu3.score = 80
fmt.Println(stu1.score) // 输出 100
fmt.Println(stu3.score) // 输出 80

然而,stu2 是指针类型,当你把 stu2 赋值给另一个变量时,会创建一个新的指针,这个新指针和 stu2 指向同一个 student 实例。因此,任何对新变量的改变都会影响到原变量 stu2。例如:

stu4 := stu2
stu4.score = 80
fmt.Println(stu2.score) // 输出 80
fmt.Println(stu4.score) // 输出 80

结构体内存布局

当我们创建了一个结构体,然后某个变量使用该结构体进行初始化之后,该变量所占用的内存是一块连续的内存。

下面的代码可以证明:

package main
​
import (
  "fmt"
)
​
type test struct {
  a int8
  b int8
  c int8
  d int8
}
​
func main() {
  n := test{
    1, 2, 3, 4,
  }
  fmt.Printf("n.a %p\n", &n.a)
  fmt.Printf("n.b %p\n", &n.b)
  fmt.Printf("n.c %p\n", &n.c)
  fmt.Printf("n.d %p\n", &n.d)
  // n.a 0x140000a8014
  // n.b 0x140000a8015
  // n.c 0x140000a8016
  // n.d 0x140000a8017
}

在上面的代码中,我们创建了一个名为 test 类型的结构,然后声明了一个 n 变量为 test 类型,然后分别打印了它的四个字段的内存地址。由于 int8 类型占用一个字节的内存,所以每个字段的地址都比前一个字段的地址大 1,这证明了结构体中的字段是在连续的内存地址中存储的。

然而,需要注意的是,虽然在这个特定的例子中,结构体的字段在内存中是连续的,但这并不总是成立。

Go 的编译器可能会对结构体进行内存对齐优化,这可能导致结构体的字段之间存在内存空隙。具体的内存布局取决于各个字段的类型和它们在结构体中的顺序。例如,在以下的代码中,test 结构体中的字段并不是在连续的内存地址中存储的:

package main
​
import (
  "fmt"
)
​
type test struct {
  a int8
  b int32
  c int8
  d int32
}
​
func main() {
  n := test{
    1, 2, 3, 4,
  }
  fmt.Printf("n.a %p\n", &n.a)
  fmt.Printf("n.b %p\n", &n.b)
  fmt.Printf("n.c %p\n", &n.c)
  fmt.Printf("n.d %p\n", &n.d)
  // n.a 0x140000140c0
  // n.b 0x140000140c4
  // n.c 0x140000140c8
  // n.d 0x140000140cc
}

关于 Go 语言中内存对齐的相关内容,可以参阅 这里。

另外,空结构体是不占用空间的,例如:

package main
​
import (
  "fmt"
  "unsafe"
)
​
type test struct {
  a int8
  b int8
  c int8
  d int8
}
​
func main() {
  var a = test{
    1, 2, 3, 4,
  }
  fmt.Println(unsafe.Sizeof(a)) // 4
​
  var b test
  fmt.Println(unsafe.Sizeof(b)) // 4
​
  var c struct{}
  fmt.Println(unsafe.Sizeof(c)) // 0
}

在上面的代码中,a 就不用说了,肯定是会占用内存空间的,而 b 之所以也会占用内存空间是因为当 b 声明为 test 类型后,会初始化为相应的零值。c 的类型为 struct{ },Go 语言中的空结构体 struct{ } 是特殊的一种结构体,它不包含任何字段,因此不占用任何内存。

-EOF-

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论