前言
結構體是一種聚合的數據類型,是由零個或多個任意類型的值聚合成的實體。每個值稱為結構體的成員。
用結構體的經典案例:學校的學生信息,每個學生信息包含一個唯一的學生學號、學生的名字、學生的性別、家庭住址等等。所有的這些信息都需要綁定到一個實體中,可以作為一個整體單元被復制,作為函數的參數或返回值,或者是被存儲到數組中,等等。
結構體也是值類型,因此可以通過 new 函數來創建。
組成結構體類型的那些數據稱為字段(fields)。字段有以下特性:
- 字段擁有自己的類型和值。
- 字段名必須唯一。
- 字段的類型也可以是結構體,甚至是字段所在結構體的類型。
關於 Go 語言的類(class)
Go 語言中沒有“類”的概念,也不支持“類”的繼承等面向對象的概念。Go 語言的結構體與“類”都是復合結構體,但 Go 語言中結構體的內嵌配合接口比面向對象具有更高的擴展性和靈活性。
Go 語言不僅認為結構體能擁有方法,且每種自定義類型也可以擁有自己的方法。
結構體的定義
使用關鍵字 type 可以將各種基本類型定義為自定義類型,基本類型包括整型、字符串、布爾等。結構體是一種復合的基本類型,通過 type 定義為自定義類型后,使結構體更便於使用。
結構體的定義格式如下:
type 結構體類型名 struct {
字段1 字段1類型
字段2 字段2類型
…
}
其中:
1、結構體類型名:標識自定義結構體的名稱,在同一個包內不能重復。
2、字段1:表示結構體字段名。結構體中的字段名必須唯一。
3、字段1類型:表示結構體字段的具體類型。
舉個例子,我們定義一個 Student(學生)結構體,代碼如下:
type Student struct{
id int
name string
age int
gender int // 0 表示女生,1 表示男生
addr string
}
在這里,Student 的地位等價於 int、byte、bool、string 等類型
通常一行對應一個結構體成員,成員的名字在前,類型在后
不過如果相鄰的成員類型如果相同的話可以被合並到一行:
type Student struct{
id int
name string
age, gender int
addr string
}
這樣我們就擁有了一個 Student 的自定義類型,它有 id、name、age等字段。
這樣我們使用這個 Student 結構體就能夠很方便的在程序中表示和存儲學生信息了。
遞歸結構體
結構體類型可以通過引用自身來定義。這在定義鏈表或二叉樹的元素(通常叫節點)時特別有用,此時節點包含指向臨近節點的鏈接(地址)。
如下所示,鏈表中的 su
,樹中的 ri
和 le
分別是指向別的節點的指針。
鏈表
type Node struct {
data float64
su *Node
}
雙向鏈表
type Node struct {
pr *Node
data float64
su *Node
}
二叉樹
type Tree struct {
le *Tree
data float64
ri *Tree
}
結構體的實例化
結構體的定義只是一種內存布局的描述,只有當結構體實例化時,才會真正地分配內存,因此必須在定義結構體並實例化后才能使用結構體的字段。
實例化就是根據結構體定義的格式創建一份與格式一致的內存區域,結構體實例與實例間的內存是完全獨立的。
Go語言可以通過多種方式實例化結構體,根據實際需要可以選用不同的寫法。
基本的實例化形式
結構體本身也是一種類型,我們可以像聲明內置類型一樣使用 var 關鍵字聲明結構體類型。
基本實例化格式如下:
var 結構體實例 結構體類型
對 Student 進行實例化,代碼如下:
type Student struct{
id int
name string
age int
gender int // 0 表示女生,1 表示男生
addr string
}
func main() {
var stu1 Student
stu1.id = 120100
stu1.name = "Conan"
stu1.age = 18
stu1.gender = 1
fmt.Println("stu1 = ", stu1) // stu1 = {120100 Conan 18 1 }
}
注意:沒有賦值的字段默認為該字段類型的零值,此時 addr = ""
我們可以通過 點 "."
的方式來訪問結構體的成員變量,如 stu1.name
,結構體成員變量的賦值方法與普通變量一致。
創建指針類型的結構體
Go 語言中,還可以使用 new 關鍵字對類型(包括結構體、整型、浮點數、字符串等)進行實例化,結構體在實例化后會形成指針類型的結構體。
使用 new 的格式如下:
變量名 := new(類型)
Go 語言讓我們可以像訪問普通結構體一樣使用 點"."
來訪問結構體指針的成員,例如:
type Student struct{
id int
name string
age int
gender int // 0 表示女生,1 表示男生
addr string
}
func main() {
stu2 := new(Student)
stu2.id = 120101
stu2.name = "Kidd"
stu2.age = 23
stu2.gender = 1
fmt.Println("stu2 = ", stu2) // stu2 = &{120101 Kidd 23 1 }
}
經過 new 實例化的結構體實例在成員賦值上與基本實例化的寫法一致。
注意:在 Go 語言中,訪問結構體指針的成員變量時可以繼續使用 點"."
,這是因為 Go 語言為了方便開發者訪問結構體指針的成員變量,使用了語法糖(Syntactic sugar)技術,將 stu2.name 形式轉換為 (*stu2).name。
取結構體的地址實例化
在 Go 語言中,對結構體進行 &
取地址操作時,視為對該類型進行一次 new 的實例化操作,取地址格式如下:
變量名 := &結構體類型{}
取地址實例化是最廣泛的一種結構體實例化方式,具體代碼如下:
type Student struct{
id int
name string
age int
gender int // 0 表示女生,1 表示男生
addr string
}
func main() {
stu3 := &Student{}
stu3.id = 120102
stu3.name = "Lan"
stu3.age = 18
stu3.gender = 0
fmt.Println("stu3 = ", stu3) // stu3 = &{120102 Lan 18 0 }
}
結構體的初始化
結構體在實例化時可以直接對成員變量進行初始化,初始化有兩種形式分別是以字段“鍵值對”形式和多個值的列表形式。
鍵值對形式的初始化適合選擇性填充字段較多的結構體,多個值的列表形式適合填充字段較少的結構體。
特別地,還有一種初始化匿名結構體。
使用“鍵值對”初始化結構體
結構體可以使用“鍵值對”(Key value pair)初始化字段,每個“鍵”(Key)對應結構體中的一個字段,鍵的“值”(Value)對應字段需要初始化的值。
鍵值對的填充是可選的,不需要初始化的字段可以不填入初始化列表中。
結構體實例化后字段的默認值是字段類型的零值,例如 ,數值為 0、字符串為 ""(空字符串)、布爾為 false、指針為 nil 等。
鍵值對初始化的格式如下:
變量名 := 結構體類型名{
字段1: 字段1的值,
字段2: 字段2的值,
...
}
注意:
1、字段名只能出現一次。
2、鍵值之間以 : 分隔,鍵值對之間以 , 分隔。
使用鍵值對形式初始化結構體的代碼如下:
stu4 := Student{
id: 120103,
name: "Gin",
age: 25,
gender: 1,
addr: "unknown",
}
fmt.Println("stu4 = ", stu4) // stu4 = {120103 Gin 25 1 unknown}
使用多個值的列表初始化結構體
Go語言可以在“鍵值對”初始化的基礎上忽略“鍵”,也就是說,可以使用多個值的列表初始化結構體的字段。
多個值使用逗號分隔初始化結構體,例如:
變量名 := 結構體類型名{
字段1的值,
字段2的值,
...
}
注意:
1、必須初始化結構體的所有字段。
2、每一個初始值的填充順序必須與字段在結構體中的聲明順序一致。
3、鍵值對與值列表的初始化形式不能混用。
使用多個值列表初始化結構體的代碼如下:
stu5 := Student{
120104,
"Kogorou",
38,
1,
"毛利偵探事務所",
}
fmt.Println("stu5 = ", stu5) // stu5 = {120104 Kogorou 38 1 毛利偵探事務所}
初始化匿名結構體
匿名結構體沒有類型名稱,無須通過 type 關鍵字定義就可以直接使用。
例如:
package main
import (
"fmt"
)
func main() {
var user struct{name string; age int}
user.name = "Conan"
user.age = 18
fmt.Println("user = ", user) // user = {Conan 18}
}
結構體的賦值與比較
結構體的賦值
當使用 = 對結構體賦值時,更改其中一個結構體的值不會影響另外的值:
package main
import (
"fmt"
)
type Student struct {
id int
name string
age int
gender int // 0 表示女生,1 表示男生
addr string
}
func main() {
var stu1 Student
stu1.id = 120100
stu1.name = "Conan"
stu1.age = 18
stu1.gender = 1
stu6 := stu1
fmt.Println("stu1 = ", stu1) // stu1 = {120100 Conan 18 1 }
fmt.Println("stu6 = ", stu6) // stu6 = {120100 Conan 18 1 }
stu6.name = "柯南"
fmt.Println("stu1 = ", stu1) // stu1 = {120100 Conan 18 1 }
fmt.Println("stu6 = ", stu6) // stu6 = {120100 柯南 18 1 }
}
結構體的比較
如果結構體的全部成員都是可以比較的,那么結構體也是可以比較的;如果結構體中存在不可比較的成員變量,比如說切片、map等,那么結構體就不能比較。這時如果強行用 ==、!= 來進行判斷的話,程序會直接報錯,我們可以用 DeepEqual 來進行深度比較。
如果結構體的全部成員都是可以比較的,那么兩個結構體將可以使用 == 或 != 運算符進行比較。
相等比較運算符 == 將比較兩個結構體的每個成員,因此下面兩個比較的表達式是等價的:
type Student struct {
id int
name string
}
func main() {
var stu1 Student
stu1.id = 120100
stu1.name = "Conan"
stu6 := stu1
stu6.name = "柯南"
fmt.Println(stu1.id == stu6.id && stu1.name == stu6.name) // "false"
fmt.Println(stu1 == stu6) // "false"
}
可比較的結構體類型和其他可比較的類型一樣,可以用於 map 的 key 類型。
結構體數組和切片
現在我們有一個需求:用結構體存儲多個學生的信息。
我們就可以定義結構體數組來存儲,然后通過循環的方式,將結構體數組中的每一項進行輸出:
package main
import "fmt"
type student struct {
id int
name string
score int
}
func main() {
// 結構體數組
students := [3]student{
{101, "conan", 88},
{102, "kidd", 78},
{103, "lan", 98},
}
// 打印結構體數組的每一項
for index, stu := range students {
fmt.Println(index, stu.name)
}
}
用結構體切片存儲同理。
練習1:計算以上學生成績的總分。
package main
import "fmt"
type student struct {
id int
name string
score int
}
func main() {
// 結構體數組
students := [3]student{
{101, "conan", 88},
{102, "kidd", 78},
{103, "lan", 98},
}
// 計算以上學生成績的總分
sum := students[0].score
for i, stuLen := 1, len(students); i < stuLen; i++ {
sum += students[i].score
}
fmt.Println("總分是:", sum)
}
練習2:輸出以上學生成績中最高分。
package main
import "fmt"
type student struct {
id int
name string
score int
}
func main() {
// 結構體數組
students := [3]student{
{101, "conan", 88},
{102, "kidd", 78},
{103, "lan", 98},
}
// 輸出以上學生成績中最高分
maxScore := students[0].score
for i, stuLen := 1, len(students); i < stuLen; i++ {
if maxScore < students[i].score {
maxScore = students[i].score
}
}
fmt.Println("最高分是:", maxScore)
}
結構體作為 map 的 value
結構體作為 map 的 value 示例如下:
package main
import "fmt"
type student struct {
id int
name string
score int
}
func main0801() {
// 結構體數組
students := [3]student{
{101, "conan", 88},
{102, "kidd", 78},
{103, "lan", 98},
}
// 打印結構體數組的每一項
for index, stu := range students {
fmt.Println(index, stu.name)
}
fmt.Println(students)
// 計算以上學生成績的總分
sum := students[0].score
for i, stuLen := 1, len(students); i < stuLen; i++ {
sum += students[i].score
}
fmt.Println("總分是:", sum)
// 輸出以上學生成績中最高分
maxScore := students[0].score
for i, stuLen := 1, len(students); i < stuLen; i++ {
if maxScore < students[i].score {
maxScore = students[i].score
}
}
fmt.Println("最高分是:", maxScore)
}
func main() {
// 定義 map
m := make(map[int]student)
m[101] = student{101, "conan", 88}
m[102] = student{102, "kidd", 78}
m[103] = student{103, "lan", 98}
fmt.Println(m) // map[101:{101 conan 88} 102:{102 kidd 78} 103:{103 lan 98}]
for k, v := range m {
fmt.Println(k, v)
}
}
結構體切片作為 map 的 value
結構體切片(本質上是切片)作為 map 的 value 示例如下:
package main
import "fmt"
type student struct {
id int
name string
score int
}
func main() {
m := make(map[int][]student)
m[101] = append(m[101], student{1, "conan", 88}, student{2, "kidd", 78})
m[102] = append(m[101], student{1, "lan", 98}, student{2, "blame", 66})
// 101 [{1 conan 88} {2 kidd 78}]
// 102 [{1 conan 88} {2 kidd 78} {1 lan 98} {2 blame 66}]
for k, v := range m {
fmt.Println(k, v)
}
for k, v := range m {
for i, data := range v {
fmt.Println(k, i, data)
}
}
}
結構體作為函數參數
你可以像其它數據類型一樣將結構體類型作為參數傳遞給函數:
結構體傳遞為 值傳遞(形參單元和實參單元是不同的存儲區域,修改不會影響其它的值)
package main
import "fmt"
type student struct {
id int
name string
score int
}
func foo(stu student) {
stu.name = "lan"
}
func main() {
stu := student{101, "conan", 88}
fmt.Println(stu) // {101 conan 88}
foo(stu)
fmt.Println(stu) // {101 conan 88}
}
通過以上程序,我們知道:Go 函數給參數傳遞值的時候是以復制的方式進行的。復制傳值時,如果函數的參數是一個 struct 對象,將直接復制整個數據結構的副本傳遞給函數。
這有兩個問題:
函數內部無法修改傳遞給函數的原始數據結構,它修改的只是原始數據結構拷貝后的副本;
如果傳遞的原始數據結構很大,完整地復制出一個副本開銷並不小。
所以,如果條件允許,應當給需要 struct 實例作為參數的函數傳 struct 的指針。
PS:
- 結構體切片作為函數參數是地址傳遞
- 結構體數組作為函數參數是值傳遞
練習
定義結構體,存儲5名學生,三門成績,求出每名學生的總成績和平均成績。
結構體定義示例:
type student struct {
id int
name string
score []int
}
package main
import "fmt"
type student struct {
id int
name string
score []int
}
func main() {
stus := []student{
{101, "小明", []int{100, 99, 94}},
{102, "小紅", []int{60, 123, 98}},
{103, "小剛", []int{90, 109, 81}},
{104, "小強", []int{55, 66, 99}},
{105, "小花", []int{123, 65, 89}},
}
for _, stu := range stus {
// 三門總成績
sum := 0
for _, value := range stu.score {
sum += value
}
fmt.Printf("%s 的總成績為: %d, 平均成績為: %d\n", stu.name, sum, sum/len(stu.score))
}
}
李培冠博客
歡迎訪問我的個人網站:
李培冠博客:lpgit.com