結構體
結構體定義
結構體是由一些列屬性組成的複合資料類型,每個屬性都具有名稱、類型和值,結構體将屬
性組合在一起進行由程式進行處理。
自定義類型
在 go 語言中使用 type 聲明一種新的類型,文法格式為:
Format 可以時任意内置類型、函數簽名、結構體、接口。使用自定義類型的好處是見名知道其意思。其次自定義類型可以添加方法,但是對于原始的資料類型是沒有辦法添加方法的。
type User map[string]string
var c User
c = make(User)
c["one"] = "1"
fmt.Println(c)
也可以定義函數,對函數類型的重新定義
type Callback func()
callbacks := map[string]Callback{}
callbacks ["one"] = func() {
fmt.Println("one")
}
v,ok := callbacks["one"]
if ok{
v()
}
結構體是一種聚合類型,裡面可以包含任意類型的值,這些值就是我們定義的結構體的成員,也稱為字段。在 Go 語言中,要自定義一個結構體,需要使用 type+struct 關鍵字組合。
在下面的例子中,我自定義了一個結構體類型,名稱為 person,表示一個人。這個 person 結構體有兩個字段:name 代表這個人的名字,age 代表這個人的年齡。
type person struct {
name string
age uint
}
在定義結構體時,字段的聲明方法和平時聲明一個變量是一樣的,都是變量名在前,類型在後,隻不過在結構體中,變量名稱為成員名或字段名。
結構體的成員字段并不是必需的,也可以一個字段都沒有,這種結構體成為空結構體。
根據以上資訊,我們可以總結出結構體定義的表達式,如下面的代碼所示:
type structName struct{
fieldName typeName
....
....
}
其中:
- type 和 struct 是 Go 語言的關鍵字,二者組合就代表要定義一個新的結構體類型。
- structName 是結構體類型的名字。
- fieldName 是結構體的字段名,而 typeName 是對應的字段類型。
- 字段可以是零個、一個或者多個。
小提示:結構體也是一種類型,是以以後自定義的結構體,我會稱為某結構體或某類型,兩者是一個意思。比如 person 結構體和 person 類型其實是一個意思。
定義好結構體後就可以使用了,因為它是一個聚合類型,是以比普通的類型可以攜帶更多資料。
結構體聲明使用
結構體類型和普通的字元串、整型一樣,也可以使用同樣的方式聲明和初始化。
在下面的例子中,我聲明了一個 person 類型的變量 p,因為沒有對變量 p 初始化,是以預設會使用結構體裡字段的零值。
var p person
當然在聲明一個結構體變量的時候,也可以通過結構體字面量的方式初始化,如下面的代碼所示:
p=person{"飛雪無情",
30}
采用簡短聲明法,同時采用字面量初始化的方式,把結構體變量 p 的 name 初始化為“飛雪無情”,age 初始化為 30,以逗号分隔。
聲明了一個結構體變量後就可以使用了,下面我們運作以下代碼,驗證 name 和 age 的值是否和初始化的一樣。
fmt.Println(p.name,p.age)
在 Go 語言中,通路一個結構體的字段和調用一個類型的方法一樣,都是使用點操作符“.”。
采用字面量初始化結構體時,初始化值的順序很重要,必須和字段定義的順序一緻。
在 person 這個結構體中,第一個字段是 string 類型的 name,第二個字段是 uint 類型的 age,是以在初始化的時候,初始化值的類型順序必須一一對應,才能編譯通過。也就是說,在示例 {"飛雪無情",30} 中,表示 name 的字元串飛雪無情必須在前,表示年齡的數字 30 必須在後。
那麼是否可以不按照順序初始化呢?當然可以,隻不過需要指出字段名稱,如下所示:
p:=person{age:30,
name:"飛雪無情",}
其中,第一位我放了整型的 age,也可以編譯通過,因為采用了明确的 field:value 方式進行指定,這樣 Go 語言編譯器會清晰地知道你要初始化哪個字段的值。
有沒有發現,這種方式和 map 類型的初始化很像,都是采用冒号分隔。Go 語言盡可能地重用操作,不發明新的表達式,便于我們記憶和使用。
當然你也可以隻初始化字段 age,字段 name 使用預設的零值,如下面的代碼所示,仍然可以編譯通過。
p:=person{age:30}
字段結構體
結構體的字段可以是任意類型,也包括自定義的結構體類型,比如下面的代碼:
type person struct {
name string
age uint
addr address
}
type address struct {
province string
city string
}
在這個示例中,我定義了兩個結構體:person 表示人,address 表示位址。在結構體 person 中,有一個 address 類型的字段 addr,這就是自定義的結構體。
通過這種方式,用代碼描述現實中的實體會更比對,複用程度也更高。對于嵌套結構體字段的結構體,其初始化和正常的結構體大同小異,隻需要根據字段對應的類型初始化即可,如下面的代碼所示:
p:=person{
age:30,
name:"飛雪無情",
addr:address{
province: "北京",
city: "北京",
},
}
如果需要通路結構體最裡層的 province 字段的值,同樣也可以使用點操作符,隻不過需要使用兩個點,如下面的代碼所示:
fmt.Println(p.addr.province)
第一個點擷取 addr,第二個點擷取 addr 的 province。
結構體指針
new是初始化結構體的指針,使用 new 函數進行初始化結構體指針對象
type User struct {
id int
name string
birthday time.Time
}
b := new(User)
fmt.Printf("%T, %v\n",b,b)
*main.User, &{0 {0 0 <nil>}}
面向對象三大思想:
封裝📦:在go裡面實作封裝使用對是結構體,在其他語言裡面通過類來進行封裝,通過類實作封裝📦
結構體 > 類(類裡面一般都會有構造函數,用來建立類對應對執行個體的,建立結構體類型的變量可以使用New函數,看到new函數就知道是建構結構體的)
fun New(id int,name string,birthday Time) User{
return User{id,name,birthday}
}
如果傳回的是指針類型的結構體
fun New(id int,name string,birthday Time) *User{
return &User{id,name,birthday}
}
type User struct {
id int
name string
birthday time.Time
}
func New(id int,name string,birthday time.Time) *User{
return &User{
id: id,
name: name,
birthday: birthday,
}
}
d := New(1,"jerry",time.Now())
fmt.Println(d.id,d.name,d.birthday)
1 jerry 2022-03-08 08:42:34.551726 +0800 CST m=+0.001138918
組合 > 繼承:目前有了一個結構體,想要在之前的結構體上面擴充并且使用
type Addr struct {
province string
}
type User struct {
id int
name string
add Addr
}
組合可以了解為結構體裡面有屬性定義為了另外的結構體類型。
多态
匿名結構體
也有匿名結構體,一般使用在項目的配置,一個項目的配置隻有一份,一般會使用匿名結構體,
或者在做web前端開發,給模版上傳遞值。
e := struct {
id int
name string
}{id: 1,name: "cherry"}
fmt.Println(e)
或者
var user1 struct{
id int
name string
}
user1 = struct {
id int
name string
}{id: 1, name: "jerry"}
fmt.Println(user1)
或者直接
user1 := struct {
id int
name string
}{id: 1, name: "jerry"}
匿名結構體一般不使用指針類型。
接口
接口的定義
接口是和調用方的一種約定,它是一個高度抽象的類型,不用和具體的實作細節綁定在一起。接口要做的是定義好約定,告訴調用方自己可以做什麼,但不用知道它的内部實作,這和我們見到的具體的類型如 int、map、slice 等不一樣。
接口的定義和結構體稍微有些差别,雖然都以 type 關鍵字開始,但接口的關鍵字是 interface,表示自定義的類型是一個接口。也就是說 Stringer 是一個接口,它有一個方法 String() string,整體如下面的代碼所示:
type Stringer interface {
String() string
}
提示:Stringer 是 Go SDK 的一個接口,屬于 fmt 包。
針對 Stringer 接口來說,它會告訴調用者可以通過它的 String() 方法擷取一個字元串,這就是接口的約定。至于這個字元串怎麼獲得的,長什麼樣,接口不關心,調用者也不用關心,因為這些是由接口實作者來做的。
接口的實作
接口的實作者必須是一個具體的類型,繼續以 person 結構體為例,讓它來實作 Stringer 接口,如下代碼所示:
func (p person) String() string{
return fmt.Sprintf("the name is %s,age is %d",p.name,p.age)
}
給結構體類型 person 定義一個方法,這個方法和接口裡方法的簽名(名稱、參數和傳回值)一樣,這樣結構體 person 就實作了 Stringer 接口。
注意:如果一個接口有多個方法,那麼需要實作接口的每個方法才算是實作了這個接口。
實作了 Stringer 接口後就可以使用了。首先我先來定義一個可以列印 Stringer 接口的函數,如下所示:
func printString(s fmt.Stringer){
fmt.Println(s.String())
}
這個被定義的函數 printString,它接收一個 Stringer 接口類型的參數,然後列印出 Stringer 接口的 String 方法傳回的字元串。
printString 這個函數的優勢就在于它是面向接口程式設計的,隻要一個類型實作了 Stringer 接口,都可以列印出對應的字元串,而不用管具體的類型實作。
因為 person 實作了 Stringer 接口,是以變量 p 可以作為函數 printString 的參數,可以用如下方式列印:
printString(p)
結果為:
the name is 飛雪無情,age is 30
func (addr address) String() string{
return fmt.Sprintf("the addr is %s%s",addr.province,addr.city)
}
printString(p.addr)
//輸出:the addr is 北京北京