1. 程式人生 > 其它 >Golang通脈之結構體

Golang通脈之結構體

Go語言中的基礎資料型別可以表示一些事物的基本屬性,但是要表達一個事物的全部或部分屬性時,這時候再用單一的基本資料型別明顯就無法滿足需求了,Go語言提供了一種自定義資料型別,可以封裝多個基本資料型別,這種資料型別叫結構體,英文名稱struct。 也就是可以通過struct來定義自己的型別了。

Go語言中通過struct來實現面向物件。

結構體的定義

Go 語言中陣列可以儲存同一型別的資料,但在結構體中我們可以為不同項定義不同的資料型別。 結構體是由一系列具有相同型別或不同型別的資料構成的資料集合。

使用typestruct關鍵字來定義結構體,具體程式碼格式如下:

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的自定義型別,它有namecityage三個欄位。使用這個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}

使用這種格式初始化時,需要注意:

  1. 必須初始化結構體的所有欄位。
  2. 初始值的填充順序必須與欄位在結構體中的宣告順序一致。
  3. 該方式不能和鍵值初始化方式混用。

結構體記憶體佈局

結構體佔用一塊連續的記憶體。

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")
    }
}