[TOC]
# 結構體
## 結構體的定義
使用`type`和`struct`關鍵字來定義結構體,具體代碼格式如下:
~~~go
type 類型名 struct {
字段名 字段類型
字段名 字段類型
…
}
~~~
其中:
* 類型名:標識自定義結構體的名稱,在同一個包內不能重復。
* 字段名:表示結構體字段名。結構體中的字段名必須唯一。
* 字段類型:表示結構體字段的具體類型。
~~~go
type person struct {
name string
age int8
}
~~~
同樣類型的字段也可以寫在一行,
~~~go
type person1 struct {
name, province string
age int8
}
~~~
## 結構體實例化
只有當結構體實例化時,才會真正地分配內存。也就是必須實例化后才能使用結構體的字段。
~~~go
var 結構體實例 結構體類型
~~~
### 基本實例化
~~~go
type person struct {
name string
age int8
}
func main() {
var p1 person
p1.name = "小叮當"
p1.age = 18
fmt.Printf("p1=%v\n", p1) //p1={小叮當18}
fmt.Printf("p1=%#v\n", p1) //p1=main.person{name:"小叮當", age:18}
}
~~~
通過`.`來訪問結構體的字段(成員變量),例如`p1.name`和`p1.age`等。
### 匿名結構體
在定義一些臨時數據結構等場景下還可以使用匿名結構體。
~~~
package main
import (
"fmt"
)
func main() {
var user struct{Name string; Age int}
user.Name = "小叮當"
user.Age = 18
fmt.Printf("%#v\n", user)
}
~~~
### 創建指針類型結構體
可以通過使用`new`關鍵字對結構體進行實例化,得到的是結構體的地址
~~~go
var p2 = new(person)
fmt.Printf("%T\n", p2) //*main.person
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"",age:0}
~~~
需要注意的是在Go語言中支持對結構體指針直接使用`.`來訪問結構體的成員。
~~~go
var p2 = new(person)
p2.name = "小叮當"
p2.age = 18
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"小叮當", age:18}
~~~
### 取結構體的地址實例化
使用`&`對結構體進行取地址操作相當于對該結構體類型進行了一次`new`實例化操作。
~~~go
p3 := &person{}
fmt.Printf("%T\n", p3) //*main.person
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"",age:0}
p3.name = "小叮當"
p3.age = 18
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"小叮當",age:18}
~~~
`p3.name = "小叮當"`其實在底層是`(*p3).name = "小叮當"`,這是Go語言幫我們實現的語法糖。
## 結構體初始化
沒有初始化的結構體,其成員變量都是對應其類型的零值。
~~~go
type person struct {
name string
age int8
}
func main() {
var p4 person
fmt.Printf("p4=%#v\n", p4) //p4=main.person{name:"",age:0}
}
~~~
### 使用鍵值對初始化
使用鍵值對對結構體進行初始化時,鍵對應結構體的字段,值對應該字段的初始值。
~~~go
p5 := person{
name: "小叮當",
age: 18,
}
fmt.Printf("p5=%#v\n", p5) //p5=main.person{name:"小叮當", age:18}
~~~
也可以對結構體指針進行鍵值對初始化
~~~go
p6 := &person{
name: "小叮當",
city: "北京",
age: 18,
}
fmt.Printf("p6=%#v\n", p6)
~~~
當某些字段沒有初始值的時候,該字段可以不寫。此時,沒有指定初始值的字段的值就是該字段類型的零值。
~~~go
p7 := &person{
city: "北京",
}
fmt.Printf("p7=%#v\n", p7)
~~~
### 使用值的列表初始化
初始化結構體的時候可以簡寫,也就是初始化的時候不寫鍵,直接寫值:
~~~go
p8 := &person{
"小叮當",
18,
}
fmt.Printf("p8=%#v\n", p8) //p8=&main.person{name:"小叮當",age:18}
~~~
使用這種格式初始化時,需要注意:
1. 必須初始化結構體的所有字段。
2. 初始值的填充順序必須與字段在結構體中的聲明順序一致。
3. 該方式不能和鍵值初始化方式混用。
## 結構體內存布局
結構體占用一塊連續的內存。
~~~go
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)
~~~
輸出:
~~~bash
n.a 0xc0000a0060
n.b 0xc0000a0061
n.c 0xc0000a0062
n.d 0xc0000a0063
~~~
### 空結構體
空結構體是不占用空間的。
~~~go
var v struct{}
fmt.Println(unsafe.Sizeof(v)) // 0
~~~
### 面試題
~~~
type student struct?{
????Name?string
????Age??int
}
func pase_student()?map[string]*student?{
m?:=?make(map[string]*student)
stus?:=?[]student{
????????{Name:?"zhou",?Age:?24},
????????{Name:?"li",?Age:?23},
????????{Name:?"wang",?Age:?22},
????}
for _,?stu?:=?range?stus?{
????????m[stu.Name]?=?&stu
}
return?m
}
func main()?{
students?:=?pase_student()
fork,?v?:=?range?students?{
????????fmt.Printf("key=%s,value=%v?\n",?k,?v)
????}
}
~~~
output:&{wang 22}
**解析:**因為 for 遍歷時,變量?`stu`?指針不變,每次遍歷僅進行 struct 值拷貝,故?`m[stu.Name]=&stu`?實際上一致指向同一個指針,最終該指針的值為遍歷的最后一個 struct 的值拷貝。形同如下代碼:
~~~
varstu?student
for _,?stu?:=?range?stus?{
????m[stu.Name]?=?&stu
}
~~~
修正方案,取數組中原始值的指針:
~~~
for i,?_?:=?range?stus?{
stu:=stus[i]
????m[stu.Name]?=?&stu
}
~~~
## 構造函數
Go語言的結構體沒有構造函數,我們可以自己實現。 例如,下方的代碼就實現了一個`person`的構造函數。 因為`struct`是值類型,如果結構體比較復雜的話,值拷貝性能開銷會比較大,所以該構造函數返回的是結構體指針類型。
~~~go
func newPerson(name, city string, age int8) *person {
return &person{
name: name,
age: age,
}
}
~~~
調用構造函數
~~~go
p9 := newPerson("小叮當", 18)
fmt.Printf("%#v\n", p9) //&main.person{name:"小叮當", age:18}
~~~
## 方法和接收者
`方法(Method)`是一種作用于特定類型變量的函數。這種特定類型變量叫做`接收者(Receiver)`。接收者的概念就類似于其他語言中的`this`或者`self`。
格式如下:
~~~go
func (接收者變量 接收者類型) 方法名(參數列表) (返回參數) {
函數體
}
~~~
其中:
* 接收者變量:接收者中的參數變量名在命名時,官方建議使用接收者類型名稱首字母的小寫,而不是`self`、`this`之類的命名。例如,`Person`類型的接收者變量應該命名為`p`,`Connector`類型的接收者變量應該命名為`c`等。
* 接收者類型:接收者類型和參數類似,可以是指針類型和非指針類型。
* 方法名、參數列表、返回參數:具體格式與函數定義相同。
~~~go
//Person 結構體
type Person struct {
name string
age int8
}
//NewPerson 構造函數
func NewPerson(name string, age int8) *Person {
return &Person{
name: name,
age: age,
}
}
//Dream
func (p Person) Dream() {
fmt.Printf("%s的夢想是學好Go語言!\n", p.name)
}
func main() {
p1 := NewPerson("小叮當", 18)
p1.Dream()
}
~~~
`方法`與`函數`的區別是,函數不屬于任何類型,方法屬于特定的類型。
### 指針類型的接收者
指針類型的接收者由一個結構體的指針組成,由于指針的特性,調用方法時修改接收者指針的任意成員變量,在方法結束后,修改都是有效的。這種方式就十分接近于其他語言中面向對象中的`this`或者`self`
~~~go
// SetAge 設置p的年齡
// 使用指針接收者
func (p *Person) SetAge(newAge int8) {
p.age = newAge
}
~~~
調用該方法:
~~~go
func main() {
p1 := NewPerson("小叮當", 18)
fmt.Println(p1.age) // 18
p1.SetAge(99)
fmt.Println(p1.age) // 99
}
~~~
### 值類型的接收者
當方法作用于值類型接收者時,Go語言會在代碼運行時將接收者的值復制一份。在值類型接收者的方法中可以獲取接收者的成員值,但修改操作只是針對副本,無法修改接收者變量本身。
~~~go
// SetAge2 設置p的年齡
// 使用值接收者
func (p Person) SetAge2(newAge int8) {
p.age = newAge
}
func main() {
p1 := NewPerson("小叮當", 18)
p1.Dream()
fmt.Println(p1.age) // 18
p1.SetAge2(99) // (*p1).SetAge2(99)
fmt.Println(p1.age) // 18
}
~~~
### 什么時候應該使用指針類型接收者
1. 需要修改接收者中的值
2. 接收者是拷貝代價比較大的大對象
3. 保證一致性,如果有某個方法使用了指針接收者,那么其他的方法也應該使用指針接收者。
## 任意類型添加方法
在Go語言中,接收者的類型可以是任何類型,不僅僅是結構體,任何類型都可以擁有方法。 舉個例子,我們基于內置的`int`類型使用type關鍵字可以定義新的自定義類型,然后為我們的自定義類型添加方法。
~~~go
//MyInt 將int定義為自定義MyInt類型
type MyInt int
//SayHello 為MyInt添加一個SayHello的方法
func (m MyInt) SayHello() {
fmt.Println("Hello, 我是一個int。")
}
func main() {
var m1 MyInt
m1.SayHello() //Hello, 我是一個int。
m1 = 100
fmt.Printf("%#v %T\n", m1, m1) //100 main.MyInt
}
~~~
**注意事項:**非本地類型不能定義方法,也就是說我們不能給別的包的類型定義方法。
## 結構體的匿名字段
結構體允許其成員字段在聲明時沒有字段名而只有類型,這種沒有名字的字段就稱為匿名字段。
~~~go
//Person 結構體Person類型
type Person struct {
string
int
}
func main() {
p1 := Person{
"小叮當",
18,
}
fmt.Printf("%#v\n", p1) //main.Person{string:"小叮當", int:18}
fmt.Println(p1.string, p1.int) //北京 18
}
~~~
**注意:**這里匿名字段的說法并不代表沒有字段名,而是默認會采用類型名作為字段名,結構體要求字段名稱必須唯一,因此一個結構體中同種類型的匿名字段只能有一個。
## 嵌套結構體
一個結構體中可以嵌套包含另一個結構體或結構體指針

### 嵌套匿名字段

字段查找順序:先在結構體中查找該字段,找不到再去嵌套的匿名字段中查找
### 嵌套結構體的字段名沖突
嵌套結構體內部可能存在相同的字段名,在這種情況下為了避免歧義需要通過指定具體的內嵌結構體字段名。

## 結構體的“繼承”

## 結構體字段的可見性
結構體中字段大寫開頭表示可公開訪問,小寫表示私有(僅在定義當前結構體的包中可訪問)。
## 結構體與JSON序列化
```
type School struct {
SchName string
? ? Class ? []*Class
}
type Class struct {
ClassNum int
}
func main() {
school := &School{
SchName: "小叮當",
Class: make([]*Class, 0, 2),
? ? }
for i := 1; i <= 2; i++ {
c := &Class{
? ? ? ? ? ? ClassNum: i,
}
school.Class = append(school.Class, c)
}
data, err := json.Marshal(school)
if err != nil {
? ? fmt.Println("json marshal failed")
return
?}
?fmt.Printf("json:%s\n", data)
//{"SchName":"小叮當","Class":[{"ClassNum":1},{"ClassNum":2}]}
jstr := `{"SchName":"小叮當","Class":[{"ClassNum":1},{"ClassNum":2}]}`
c := &School{}
err = json.Unmarshal([]byte(jstr), c)
if err != nil {
? ? ?fmt.Println("json marshal failed")
return
}
fmt.Printf("%#v", c)
//&main.School{SchName:"小叮當", Class:[]*main.Class{(*main.Class)(0xc000014218), (*main.Class)(0xc000014240)}}
}
```
## 結構體標簽(Tag)
`Tag`是結構體的元信息,可以在運行的時候通過反射的機制讀取出來。`Tag`在結構體字段的后方定義,由一對**反引號**包裹起來,具體的格式如下:
~~~
`key1:"value1" key2:"value2"`
~~~
結構體tag由一個或多個鍵值對組成。鍵與值使用冒號分隔,值用雙引號括起來。同一個結構體字段可以設置多個鍵值對tag,不同的鍵值對之間使用空格分隔。

**注意事項:**為結構體編寫`Tag`時,必須嚴格遵守鍵值對的規則。結構體標簽的解析代碼的容錯能力很差,一旦格式寫錯,編譯和運行時都不會提示任何錯誤,通過反射也無法正確取值。例如不要在key和value之間添加空格。
## 結構體和方法補充點
因為slice和map這兩種數據類型都包含了指向底層數據的指針
```
type Person struct {
name string
age int8
dreams []string
}
func (p *Person) SetDreams(dreams []string) {
p.dreams = dreams
}
func main() {
p1 := Person{name: "小叮當", age: 18}
data := []string{"吃飯", "睡覺", "變魔術"}
p1.SetDreams(data)
// 下面的這個操作會改變 p1的dreams
data[1] = "做夢"
fmt.Println(p1.dreams)
}
//如果不改變,那就用copy去復制一份數據,保存在新的內存當中
func (p *Person) SetDreams(dreams []string) {
p.dreams = make([]string, len(dreams))
copy(p.dreams, dreams)
}
```
- Go準備工作
- 依賴管理
- Go基礎
- 1、變量和常量
- 2、基本數據類型
- 3、運算符
- 4、流程控制
- 5、數組
- 數組聲明和初始化
- 遍歷
- 數組是值類型
- 6、切片
- 定義
- slice其他內容
- 7、map
- 8、函數
- 函數基礎
- 函數進階
- 9、指針
- 10、結構體
- 類型別名和自定義類型
- 結構體
- 11、接口
- 12、反射
- 13、并發
- 14、網絡編程
- 15、單元測試
- Go常用庫/包
- Context
- time
- strings/strconv
- file
- http
- Go常用第三方包
- Go優化
- Go問題排查
- Go框架
- 基礎知識點的思考
- 面試題
- 八股文
- 操作系統
- 整理一份資料
- interface
- array
- slice
- map
- MUTEX
- RWMUTEX
- Channel
- waitGroup
- context
- reflect
- gc
- GMP和CSP
- Select
- Docker
- 基本命令
- dockerfile
- docker-compose
- rpc和grpc
- consul和etcd
- ETCD
- consul
- gin
- 一些小點
- 樹
- K8s
- ES
- pprof
- mycat
- nginx
- 整理后的面試題
- 基礎
- Map
- Chan
- GC
- GMP
- 并發
- 內存
- 算法
- docker