Go語言中的基礎數據類型可以表示一些事物的基本屬性,但是要表達一個事物的全部或部分屬性時,這時候再用單一的基本數據類型明顯就無法滿足需求了,Go語言提供了一種自定義數據類型,可以封裝多個基本數據類型,這種數據類型叫結構體,英文名稱struct
。 也就是可以通過struct
來定義自己的類型了。
Go語言中通過struct
來實現面向對象。
結構體的定義
Go 語言中數組可以存儲同一類型的數據,但在結構體中我們可以為不同項定義不同的數據類型。 結構體是由一系列具有相同類型或不同類型的數據構成的數據集合。
使用type
和struct
關鍵字來定義結構體,具體代碼格式如下:
type struct_variable_type struct {
member definition;
member definition;
...
member definition;
}
其中:
struct_variable_type
:標識自定義結構體的名稱,在同一個包內不能重復。member
:表示結構體字段名。結構體中的字段名必須唯一。definition
:表示結構體字段的具體類型。
定義一個Person
結構體:
type person struct {
name string
city string
age int8
}
同樣類型的字段可以寫在一行,
type person1 struct {
name, city string
age int8
}
這樣就擁有了一個person
的自定義類型,它有name
、city
、age
三個字段。使用這個person
結構體就能夠很方便的在程序中表示和存儲人的信息了。
語言內置的基礎數據類型是用來描述一個值的,而結構體是用來描述一組值的。比如一個人有名字、年齡和居住城市等,本質上是一種聚合型的數據類型
結構體實例化
只有當結構體實例化時,即結構體聲明后,才會真正地分配內存。也就是必須實例化后才能使用結構體的字段。
結構體本身也是一種類型,可以像聲明內置類型一樣使用var
關鍵字聲明結構體類型。
var 結構體實例 結構體類型
基本實例化
type Books struct {
title string
author string
subject string
book_id int
}
func main() {
var book1 Books /* 聲明 book1 為 Books 類型 */
/* book 1 描述 */
Book1.title = "Go 語言"
Book1.author = "Go大佬"
Book1.subject = "Go"
Book1.book_id = 6495407
}
通過.
來訪問結構體的字段(成員變量)。
匿名結構體
在定義一些臨時數據結構等場景下還可以使用匿名結構體。
func main() {
var user struct{Name string; Age int}
user.Name = "張三"
user.Age = 20
fmt.Printf("%#v\n", user)
}
創建指針類型結構體
還可以通過使用new
關鍵字對結構體進行實例化,得到的是結構體的地址:
var p = new(person)
fmt.Printf("%T\n", p) //*main.person
fmt.Printf("p=%#v\n", p) //p=&main.person{name:"", city:"", age:0}
可以看出p
是一個結構體指針。
需要注意的是在Go語言中支持對結構體指針直接使用.
來訪問結構體的成員。
var p = new(person)
p.name = "張三"
p.age = 18
p.city = "深圳"
fmt.Printf("p=%#v\n", p) //p=&main.person{name:"張三", city:"深圳", age:18}
取結構體的地址實例化
使用&
對結構體進行取地址操作相當於對該結構體類型進行了一次new
實例化操作。
book := &Books{}
book.title = "Java"
book.author = "Java大佬"
book.subject = "Java 語言"
book.book_id = 6495700
book.title= "Java"
其實在底層是(*book).title= "Java"
,這是Go語言實現的語法糖。
結構體初始化
沒有初始化的結構體,其成員變量都是對應其類型的零值。
type person struct {
name string
city string
age int8
}
func main() {
var p person
fmt.Printf("p=%#v\n", p) //p=main.person{name:"", city:"", age:0}
}
使用鍵值對初始化
使用鍵值對對結構體進行初始化時,鍵對應結構體的字段,值對應該字段的初始值。
p := person{
name: "張三",
city: "",
age: 18,
}
fmt.Printf("p=%#v\n", p) //p=main.person{name:"張三", city:"深圳", age:18}
也可以對結構體指針進行鍵值對初始化,例如:
p := &person{
name: "張三",
city: "深圳",
age: 18,
}
fmt.Printf("p=%#v\n", p) //p=&main.person{name:"張三", city:"深圳", age:18}
當某些字段沒有初始值的時候,該字段可以不寫。此時,沒有指定初始值的字段的值就是該字段類型的零值。
p := &person{
city: "深圳",
}
fmt.Printf("p=%#v\n", p) //p=&main.person{name:"", city:"深圳", age:0}
使用值的列表初始化
初始化結構體的時候可以簡寫,也就是初始化的時候不寫鍵,直接寫值:
p := &person{
"張三",
"深圳",
18,
}
fmt.Printf("p=%#v\n", p) //p=&main.person{name:"張三", city:"深圳", age:18}
使用這種格式初始化時,需要注意:
- 必須初始化結構體的所有字段。
- 初始值的填充順序必須與字段在結構體中的聲明順序一致。
- 該方式不能和鍵值初始化方式混用。
結構體內存布局
結構體占用一塊連續的內存。
type test struct {
a int8
b int8
c int8
d int8
}
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 0xc0000a0060
n.b 0xc0000a0061
n.c 0xc0000a0062
n.d 0xc0000a0063
關於Go語言中的內存對齊:在 Go 中恰到好處的內存對齊
空結構體
空結構體是不占用空間的。
var v struct{}
fmt.Println(unsafe.Sizeof(v)) // 0
空結構體的作用
因為空結構體不占據內存空間,因此被廣泛作為各種場景下的占位符使用。一是節省資源,二是空結構體本身就具備很強的語義,即這里不需要任何值,僅作為占位符。
實現集合(Set)
Go 語言標准庫沒有提供 Set 的實現,通常使用 map 來代替。事實上,對於集合來說,只需要 map 的鍵,而不需要值。即使是將值設置為 bool 類型,也會多占據 1 個字節,那假設 map 中有一百萬條數據,就會浪費 1MB 的空間。
因此,將 map 作為集合(Set)使用時,可以將值類型定義為空結構體,僅作為占位符使用即可。
type Set map[string]struct{}
func (s Set) Has(key string) bool {
_, ok := s[key]
return ok
}
func (s Set) Add(key string) {
s[key] = struct{}{}
}
func (s Set) Delete(key string) {
delete(s, key)
}
func main() {
s := make(Set)
s.Add("Tom")
s.Add("Sam")
fmt.Println(s.Has("Tom"))
fmt.Println(s.Has("Jack"))
}
不發送數據的信道(channel)
func worker(ch chan struct{}) {
<-ch
fmt.Println("do something")
close(ch)
}
func main() {
ch := make(chan struct{})
go worker(ch)
ch <- struct{}{}
}
有時候使用 channel 不需要發送任何的數據,只用來通知子協程(goroutine)執行任務,或只用來控制協程並發度。這種情況下,使用空結構體作為占位符就非常合適了。
僅包含方法的結構體
type Door struct{}
func (d Door) Open() {
fmt.Println("Open the door")
}
func (d Door) Close() {
fmt.Println("Close the door")
}
在部分場景下,結構體只包含方法,不包含任何的字段。例如上面的 Door
,在這種情況下,Door
事實上可以用任何的數據結構替代:
type Door int
type Door bool
無論是 int
還是 bool
都會浪費額外的內存,因此這種情況下,聲明為空結構體是最合適的。
面試題
type student struct {
name string
age int
}
func main() {
m := make(map[string]*student)
stus := []student{
{name: "張三", age: 18},
{name: "李四", age: 23},
{name: "王五", age: 25},
}
for _, stu := range stus {
m[stu.name] = &stu
}
for k, v := range m {
fmt.Println(k, "=>", v.name)
}
}
//與Java的foreach一樣,for range使用的是副本的方式。
//for range在循環時,go會創建一個額外的變量去存儲循環的元素,所以在每一次迭代中,該變量都會被重新賦值,
//所以m[stu.Name]=&stu實際上一致指向同一個指針,
//最終該指針的值為遍歷的最后一個struct的值拷貝。 就像想修改切片元素的屬性:
//for _, stu := range stus {
// stu.age = stu.age+10
//}
//也是不可行的。
構造函數
Go語言的結構體沒有構造函數,但可以自己實現。 因為struct
是值類型,如果結構體比較復雜的話,值拷貝性能開銷會比較大,所以構造函數返回的是結構體指針類型:
func NewPerson(name, city string, age int8) *person {
return &person{
name: name,
city: city,
age: age,
}
}
調用構造函數
p := NewPerson("張三", "深圳", 18)
fmt.Printf("%#v\n", p) //&main.person{name:"張三", city:"深圳", age:18}
結構體的匿名字段
可以用字段來創建結構,這些字段只包含一個沒有字段名的類型。這些字段被稱為匿名字段。
在類型中,使用不寫字段名的方式,使用另一個類型
type Human struct {
name string
age int
weight int
}
type Student struct {
Human // 匿名字段,那么默認Student就包含了Human的所有字段
speciality string
}
func main() {
// 初始化一個學生
mark := Student{Human{"Mark", 25, 120}, "Computer Science"}
// 訪問相應的字段
fmt.Println("His name is ", mark.name)
fmt.Println("His age is ", mark.age)
fmt.Println("His weight is ", mark.weight)
fmt.Println("His speciality is ", mark.speciality)
// 修改對應的備注信息
mark.speciality = "AI"
fmt.Println("Mark changed his speciality")
fmt.Println("His speciality is ", mark.speciality)
// 修改年齡信息
fmt.Println("Mark become old")
mark.age = 46
fmt.Println("His age is", mark.age)
// 修改體重信息
fmt.Println("Mark is not an athlet anymore")
mark.weight += 60
fmt.Println("His weight is", mark.weight)
}
可以使用"."的方式進行調用匿名字段中的屬性值
實際就是字段的繼承
其中可以將匿名字段理解為字段名和字段類型都是同一個
基於上面的理解,所以可以mark.Human = Human{"Marcus", 55, 220}
和mark.Human.age -= 1
若存在匿名字段中的字段與非匿名字段名字相同,則最外層的優先訪問,就近原則
通過匿名訪問和修改字段相當的有用,但是不僅僅是struct字段,所有的內置類型和自定義類型都是可以作為匿名字段的。
注意:這里匿名字段的說法並不代表沒有字段名,而是默認會采用類型名作為字段名,結構體要求字段名稱必須唯一,因此一個結構體中同種類型的匿名字段只能有一個。
嵌套結構體
一個結構體中可以嵌套包含另一個結構體或結構體指針。
type Address struct {
city, state string
}
type Person struct {
name string
age int
address Address
}
func main() {
var p Person
p.name = "Naveen"
p.age = 50
p.address = Address {
city: "Chicago",
state: "Illinois",
}
fmt.Println("Name:", p.name)
fmt.Println("Age:",p.age)
fmt.Println("City:",p.address.city)
fmt.Println("State:",p.address.state)
}
提升字段
在結構體中屬於匿名結構體的字段稱為提升字段,因為它們可以被訪問,就好像它們屬於擁有匿名結構字段的結構一樣。理解這個定義是相當復雜的。
type Address struct {
city, state string
}
type Person struct {
name string
age int
Address
}
func main() {
var p Person
p.name = "Naveen"
p.age = 50
p.Address = Address{
city: "Chicago",
state: "Illinois",
}
fmt.Println("Name:", p.name)
fmt.Println("Age:", p.age)
fmt.Println("City:", p.city) //city is promoted field
fmt.Println("State:", p.state) //state is promoted field
}
運行結果
Name: Naveen
Age: 50
City: Chicago
State: Illinois
若存在匿名字段中的字段與非匿名字段名字相同,則最外層的優先訪問,就近原則
嵌套結構體的字段名沖突
嵌套結構體內部可能存在相同的字段名。在這種情況下為了避免歧義需要通過指定具體的內嵌結構體字段名。
//Address 地址結構體
type Address struct {
Province string
City string
CreateTime string
}
//Email 郵箱結構體
type Email struct {
Account string
CreateTime string
}
//User 用戶結構體
type User struct {
Name string
Gender string
Address
Email
}
func main() {
var user User
user.Name = "張三"
user.Gender = "男"
// user.CreateTime = "2021" //ambiguous selector user.CreateTime
user.Address.CreateTime = "2000" //指定Address結構體中的CreateTime
user.Email.CreateTime = "2000" //指定Email結構體中的CreateTime
}
結構體的“繼承”
Go語言中使用結構體也可以實現其他編程語言中面向對象的繼承。
//Animal 動物
type Animal struct {
name string
}
func (a *Animal) move() {
fmt.Printf("%s會動!\n", a.name)
}
//Dog 狗
type Dog struct {
Feet int8
*Animal //通過嵌套匿名結構體實現繼承
}
func (d *Dog) wang() {
fmt.Printf("%s會汪汪汪~\n", d.name)
}
func main() {
d1 := &Dog{
Feet: 4,
Animal: &Animal{ //注意嵌套的是結構體指針
name: "樂樂",
},
}
d1.wang() //樂樂會汪汪汪~
d1.move() //樂樂會動!
}
結構體字段的可見性
結構體中字段大寫開頭表示可公開訪問(可以從其他包訪問它),小寫表示私有(僅在定義當前結構體的包中可訪問)。
結構體與JSON序列化
JSON(JavaScript Object Notation) 是一種輕量級的數據交換格式。易於人閱讀和編寫。同時也易於機器解析和生成。JSON鍵值對是用來保存JS對象的一種方式,鍵/值對組合中的鍵名寫在前面並用雙引號""
包裹,使用冒號:
分隔,然后緊接着值;多個鍵值之間使用英文,
分隔。
//Student 學生
type Student struct {
ID int
Gender string
Name string
}
//Class 班級
type Class struct {
Title string
Students []*Student
}
func main() {
c := &Class{
Title: "101",
Students: make([]*Student, 0, 200),
}
for i := 0; i < 10; i++ {
stu := &Student{
Name: fmt.Sprintf("stu%02d", i),
Gender: "男",
ID: i,
}
c.Students = append(c.Students, stu)
}
//JSON序列化:結構體-->JSON格式的字符串
data, err := json.Marshal(c)
if err != nil {
fmt.Println("json marshal failed")
return
}
fmt.Printf("json:%s\n", data)
//JSON反序列化:JSON格式的字符串-->結構體
str := `{"Title":"101","Students":[{"ID":0,"Gender":"男","Name":"stu00"},{"ID":1,"Gender":"男","Name":"stu01"},{"ID":2,"Gender":"男","Name":"stu02"},{"ID":3,"Gender":"男","Name":"stu03"},{"ID":4,"Gender":"男","Name":"stu04"},{"ID":5,"Gender":"男","Name":"stu05"},{"ID":6,"Gender":"男","Name":"stu06"},{"ID":7,"Gender":"男","Name":"stu07"},{"ID":8,"Gender":"男","Name":"stu08"},{"ID":9,"Gender":"男","Name":"stu09"}]}`
c1 := &Class{}
err = json.Unmarshal([]byte(str), c1)
if err != nil {
fmt.Println("json unmarshal failed!")
return
}
fmt.Printf("%#v\n", c1)
}
結構體標簽(Tag)
Tag
是結構體的元信息,可以在運行的時候通過反射的機制讀取出來。 Tag
在結構體字段的后方定義,由一對反引號包裹起來,具體的格式如下:
`key1:"value1" key2:"value2"`
結構體tag由一個或多個鍵值對組成。鍵與值使用冒號分隔,值用雙引號括起來。同一個結構體字段可以設置多個鍵值對tag,不同的鍵值對之間使用空格分隔。
注意事項: 為結構體編寫Tag
時,必須嚴格遵守鍵值對的規則。結構體標簽的解析代碼的容錯能力很差,一旦格式寫錯,編譯和運行時都不會提示任何錯誤,通過反射也無法正確取值。例如不要在key和value之間添加空格。
例如我們為Student
結構體的每個字段定義json序列化時使用的Tag:
//Student 學生
type Student struct {
ID int `json:"id"` //通過指定tag實現json序列化該字段時的key
Gender string //json序列化是默認使用字段名作為key
name string //私有不能被json包訪問
}
func main() {
s1 := Student{
ID: 1,
Gender: "男",
name: "張三",
}
data, err := json.Marshal(s1)
if err != nil {
fmt.Println("json marshal failed!")
return
}
fmt.Printf("json str:%s\n", data) //json str:{"id":1,"Gender":"男"}
}
結構體比較
結構體是值類型,如果每個字段具有可比性,則是可比較的。如果它們對應的字段相等,則認為兩個結構體變量是相等的。
type name struct {
firstName string
lastName string
}
func main() {
name1 := name{"Steve", "Jobs"}
name2 := name{"Steve", "Jobs"}
if name1 == name2 {
fmt.Println("name1 and name2 are equal")
} else {
fmt.Println("name1 and name2 are not equal")
}
name3 := name{firstName:"Steve", lastName:"Jobs"}
name4 := name{}
name4.firstName = "Steve"
if name3 == name4 {
fmt.Println("name3 and name4 are equal")
} else {
fmt.Println("name3 and name4 are not equal")
}
}
運行結果
name1 and name2 are equal
name3 and name4 are not equal
如果結構變量包含的字段是不可比較的,那么結構變量是不可比較的:
type image struct {
data map[int]int
}
func main() {
image1 := image{data: map[int]int{
0: 155,
}}
image2 := image{data: map[int]int{
0: 155,
}}
if image1 == image2 {
fmt.Println("image1 and image2 are equal")
}
}