1.結構體
Go 語言中沒有“類”的概念,也不支援像繼承這種面向對象的概念。但是Go 語言的結構體與“類”都是複合結構體,而且Go 語言中結構體的組合方式比面向對象具有更高的擴充性和靈活性。
1.1 結構體定義
結構體一般定義如下:
type identifier struct {
field1 type1
field2 type2
...
}
例如我們想聲明一個學生的結構體類型:
type Student struct {
Name string
Age int
}
結構體中字段的類型可以是任何類型,包括函數類型,接口類型,甚至結構體類型本身。例如我們聲明一個連結清單中的節點的結構體類型。
type ListNode struct {
Val int
Next *ListNode
}
在聲明結構體時我們也可以不給字段指定名字,例如下面這樣
type Person struct {
ID string
int
}
我們可以看到其中有一個int字段沒有名字,這種我們稱其為匿名字段。
Go語言的結構體類型(Struct)比函數類型更加靈活。它可以封裝屬性和操作。前者即是結構體類型中的字段,而後者則是結構體類型所擁有的方法。
結構體類型的字面量由關鍵字type、類型名稱、關鍵字struct,以及由花括号包裹的若幹字段聲明組成。其中,每個字段聲明獨占一行并由字段名稱(可選)和字段類型組成。示例如下:
type Person struct {
Name string
Gender string
Age uint8
}
結構體類型Person中有三個字段,分别是Name、Gender和Age。我們可以用字面量建立出一個該類型的值,像這樣:
1.2 操作結構體
聲明完結構體之後我們需要建立結構體的執行個體,可以使用如下幾種方法建立,仍然以上面的Student結構體為例。
s1 := new(Student) //第一種方式
s2 := Student{"james", 35} //第二種方式
s3 := &Student { //第三種方式
Name: "LeBron",
Age: 36,
}
使用new函數會建立一個指向結構體類型的指針,建立過程中會自動為結構體配置設定記憶體,結構體中每個變量被賦予對應的零值。
也可以使用第二種方式生命結構類型,需要注意的是此時給結構體指派的順序需要與結構體字段聲明的順序一緻。
第三種方式更為常用,我們建立結構體的同時顯示的為結構體中每個字段進行指派。
聲明完結構體之後可以直接按如下方式操作結構體。
s1.Name = "james"
s1.Age = 35
需要注意的是,結構體也仍然遵循可見性規則,要是定義結構體的字段時首字母為小寫在其他包是不能直接通路該字段的。
如果我們将定義的結構體首字母也變為小寫那麼在其他包内就不能直接建立該結構體,你知道這種情況應該怎麼處理麼?
上面我們提到的匿名字段,可以使用如下方法對其進行操作。
p := new(Person)
p.ID = "123"
p.int = 10
我們直接通過p.int的方式來通路結構體中的匿名字段對其指派,通過這個例子也可以發現,對于一個結構體來說,每一種資料類型隻能有一個匿名字段。
結構體執行個體化
隻有當結構體執行個體化時,才會真正地配置設定記憶體。也就是必須執行個體化後才能使用結構體的字段。
結構體本身也是一種類型,我們可以像聲明内置類型一樣使用var關鍵字聲明結構體類型。
var 結構體執行個體 結構體類型
(1)基本執行個體化
type person struct {
name string
city string
age int8
}
func main() {
var p1 person
p1.name = "沙河娜紮"
p1.city = "北京"
p1.age = 18
fmt.Printf("p1=%v\n", p1) //p1={沙河娜紮 北京 18}
fmt.Printf("p1=%#v\n", p1) //p1=main.person{name:"沙河娜紮", city:"北京", age:18}
}
我們通過.來通路結構體的字段(成員變量),例如p1.name和p1.age等。
(2)匿名結構體
package main
import (
"fmt"
)
func main() {
var user struct{Name string; Age int}
user.Name = "小六"
user.Age = 18
fmt.Printf("%#v\n", user)
}
1.3 标簽
在go語言中結構體除了字段的名稱和類型外還有一個可選的标簽tag,标記的tag隻有reflect包可以通路到,一般用于orm或者json的資料傳遞,下面這段代碼示範了如何為結構體打标簽。
type Student struct {
Name string `json:"name"`
Age int `json:"age"`
}
我們可以使用go自帶的json包将聲明的結構體變量轉變為json字元串。
func ToJson(s *Student) (string, error) {
bytes, err := json.Marshal(s)
if err != nil {
return "", nil
}
return string(bytes), nil
}
如果我們沒有給結構體打标簽輸出的json字元串如下所示
{"Name":"james","Age":35}
如果我們給結構體打過标簽之後輸出的json字元串如下所示
{"name":"james","age":35}
1.4 内嵌結構體
之前我們介紹到了匿名字段,結構體作為一種資料類型也可以将其生命為匿名字段,此時我們稱其為内嵌結構體,下面這段代碼中我們将結構體A嵌入到結構體B中。
type A struct {
X, Y int
}
type B struct {
A
Name string
}
通過内嵌結構體的方式我們可以在結構體B的變量下很友善的操作A中定義的字段。
b := new(B)
b.X = 10
b.Y = 20
b.Name = "james"
可以看到在b中我們操作結構體A中定義的字段就像結構體B本身定義的字段一樣自然。
但是如果存在字段的名稱沖突我們該怎麼辦?例如我們聲明如下一個結構體C。
type C struct {
A
B
X int
}
此時結構體C中也有字段X,但是内嵌的結構體A中也有字段X,如果我們使用如下這種指派方式會将X的值賦給誰呢?你可以嘗試一下
c := new(C)
c.X = 10
c.Y = 11
如果上面結構體B也有字段X,那麼程式還能成功運作麼?
需要注意的是,内嵌結構體和聲明一個結構體類型的字段是不同的,例如下面的結構體B的定義方式與上面是完全不同的。
type B struct {
a A
Name string
}
你可以嘗試一下在結構體中定義一些複雜類型例如切片,字典等是如何操作的。
2 方法
2.1 方法定義
方法與函數類似,隻不過在方法定義時會在func和方法名之間增加一個參數,如下所示:
func (r Receiver)func_name(){
// body
}
其中r被稱為方法的接收者,例如我們下面這個例子:
type Person struct {
name string
}
func (p Person) GetName() string {
return p.name
}
其中GetName方法的接收者為p是Person結構體類型,也就是說我們為結構體Person綁定了一個GetName方法,我們可以使用如下的方式進行調用。
func main() {
p := Person{
name:"james",
}
fmt.Println(p.GetName())
}
2.2 方法接收者
對于一個方法來說接收者分為兩種類型:值接收者和指針接收者。上面的GetName的接收者就是值接收者。我們再為Person結構體定義一個指針接收者。
func (p *Person)SetName(name string){
p.name = name
}
使用值接收者定義的方法,在調用的時使用的其實是值接收者的一個拷貝,是以對該值的任何操作,都不會影響原來的類型變量。
但是如果使用指針接收者的話,在方法體内的修改就會影響原來的變量,因為指針傳遞的也是位址,但是是指針本身的位址,此時拷貝得到的指針還是指向原值的,是以對指針接收者操作的同時也會影響原來類型變量的值。
而且在go語言中還有一點比較特殊,我們使用值接收者定義的方法使用指針來調用也是可以的,反過來也是如此,如下所示:
func main() {
p := &Person{
name: "james",
}
fmt.Println(p.GetName())
p1 := Person{
name: "james",
}
p1.SetName("kobe")
fmt.Println(p1.GetName())
}
3 接口
3.1 接口定義
接口相當于一種規範,它需要做的是誰想要實作我這個接口要做哪些内容,而不是怎麼做。在go語言中接口的定義如下所示:
type Namer interface {
Method1(param_list) return_type
Method2(param_list) return_type
...
}
3.2 實作接口
在go語言中不需要顯示的去實作接口,隻要一個類型實作了該接口中定義的所有方法就是預設實作了該接口,而且允許多個類型都實作該接口,也允許一個類型實作多個接口。
案例如下:
type Animal interface {
Eat()
}
type Bird struct {
Name string
}
func (b Bird) Eat() {
fmt.Println(b.Name + "吃蟲")
}
type Dog struct {
Name string
}
func (d Dog) Eat() {
fmt.Println(d.Name + "吃肉")
}
func EatWhat(a Animal) {
a.Eat()
}
func main() {
b := Bird{"Bird"}
d := Dog{"Dog"}
EatWhat(b)
EatWhat(d)
}
在EatWaht函數中是傳遞一個Animal接口類型,上面的Bird和Dog結構體都實作了Animal接口,是以都可以傳遞到函數中去來實作多态特性。
但是還有幾點需要大家去探索一下:
通過值接收者和指針接收者定義的方法,對于接口的實作有什麼影響嗎?
還記得我們之前說過的内嵌結構體麼,如果嵌入的結構體實作了某個接口那麼對于外部的結構體有什麼影響嗎?
7.3.3 類型斷言
有些時候方法傳遞進來的參數可能是一個接口類型,但是我們要繼續判斷是哪個具體的類型才能進行下一步操作,這時就用到了類型斷言,下面我們通過一個例子來進行講解:
func IsDog(a Animal) bool {
if v, ok := a.(Dog); ok {
fmt.Println(v)
return true
}
return false
}
上面的方法對傳遞進來的參數進行判斷,判斷其是否為Dog類型,如果是Dog類型的話就會将其進行轉換為v,ok用來表示是否斷言成功。
但是如果我們對于一個類型有好多種子類型要進行判斷,這樣寫的話顯然是有些複雜,可以使用如下這種方式:
func WhatType(a Animal) {
switch a.(type) {
case Dog:
fmt.Println("Dog")
case Bird:
fmt.Println("Bird")
default:
fmt.Println("error")
}
}
3.4 空接口
空接口是一個比較特殊的類型,因為其内部沒有定義任何方法是以空接口可以表示任何一個類型,比如可以進行下面的操作:
var any interface{}
any = 1
fmt.Println(any)
any = "hello"
fmt.Println(any)
any = false
fmt.Println(any)