Go中的結構體(就至關於其它語言裏的class):golang
struct 聲明:json
type (標識符) struct { field1 type field2 type }
例子:markdown
type Student struct { Name string Age int Score int }
結構體中字段的訪問,和其餘語言同樣,使用點:數據結構
package main import "fmt" type Student struct { Name string Age int Score int } func main() { var stu Student stu.Name = "Adam" stu.Age = 18 stu.Score = 90 fmt.Println(stu) fmt.Println(stu.Name) fmt.Println(stu.Age) fmt.Println(stu.Score) }
struct 定義的3種形式:ide
var stu Student var stu *Student = new (Student) var stu *Student = &Student{}
後兩種返回的都是指向結構體的指針,因此須要再跟個等號分配內存空間。而且,有些場景應該是須要用指針的結構體會更加方便。
強調一下, struct 是值類型。這裏要用new來建立值類型。不是make,make是用來建立 map 、slice、channel 的。
結構體的訪問形式以下:函數
stu.Name (*stu).Name
用上面兩種形式訪問都是能夠的,可是定義的時候返回指針的話,標準作法仍是應該用指針來訪問的,不過Go作了處理,能夠簡化,直接用第一種就夠了,可是要知道調用的本質。佈局
結構體是值類型,裏面全部的字段在內存裏是連續的:優化
package main import "fmt" type Student struct { Name string Age int Score int } func main() { var stu Student stu.Name = "Adam" stu.Age = 18 stu.Score = 90 fmt.Println(stu) fmt.Printf("%p\n", &stu) fmt.Printf("%p\n", &stu.Name) fmt.Printf("%p\n", &stu.Age) fmt.Printf("%p\n", &stu.Score) } /* 執行結果 PS H:\Go\src\go_dev\day5\struct\attribute> go run main.go {Adam 18 90} 0xc04204a3a0 0xc04204a3a0 0xc04204a3b0 0xc04204a3b8 PS H:\Go\src\go_dev\day5\struct\attribute> */
package main import "fmt" type Student struct { Name string Age int Score int } func main(){ var stu1 Student stu1.Name = "Adam" stu1.Age = 16 stu1.Score = 90 var stu2 Student = Student{ Name: "Bob", Age: 15, Score: 85, } var stu3 *Student = &Student{ Name: "Cara", Age: 18, Score: 80, } fmt.Println(stu1) fmt.Println(stu2) fmt.Println(&stu2) fmt.Println(stu3) fmt.Println(*stu3) } /* 執行結果 PS H:\Go\src\go_dev\day5\struct\init> go run .\main.go {Adam 16 90} {Bob 15 85} &{Bob 15 85} &{Cara 18 80} {Cara 18 80} PS H:\Go\src\go_dev\day5\struct\init> */
也能夠在大括號裏按位置傳參數進行初始化和定義:this
type Student struct { Name string Age int Score int } var s1 Student s1 = Student {"stu1", 18, 90} var s2 Student = Student{"stu2", 20, 80}
用結構體定義數據類型指針
每一個節點包含下一個節點的地址,這樣就把全部的節點串起來了。一般把鏈表中的第一個節點叫作鏈表頭。
type Link struct { Name string Next *Link }
下面有頭插法和尾插法建立鏈表,還有遍歷鏈表的方法:
package main import "fmt" type Student struct { Name string next *Student } // 遍歷鏈表的方法 func trans(p *Student) { for p != nil { fmt.Println(*p) p = p.next } } // 頭插法,從左邊插入 // 每一個新加入的元素都插入到頭部元素的後面,這樣的好處是頭部元素的地址不變 func CreateLinkListLeft(p *Student) { var head = p for i := 0; i < 10; i++ { p := Student{ Name: fmt.Sprintf("stuL%d", i), } p.next = head.next head.next = &p } } // 尾插法,從右邊加入 func CreateLinkListRight(p *Student) { var tail = p for i := 0; i < 10; i++ { p := Student{ Name: fmt.Sprintf("stuR%d", i), } tail.next = &p tail = &p } } func main() { var headL Student fmt.Println("頭插法") CreateLinkListLeft(&headL) // 結構體是值類型,要改變裏面的值,就是傳指針 trans(&headL) var headR Student fmt.Println("尾插法") CreateLinkListRight(&headR) trans(&headR) }
還有雙鏈表,詳細就不展開了:
type Link struct { Name string Next *Link Prev *Link }
每一個節點都有2個指針,分別用來指向左子樹和右子樹:
type binaryTree struct { Name string left *binaryTree right *binaryTree }
這裏只給一個深度優先的遍歷方法:
// 遍歷二叉樹,深度優先 func trans(root *Student) { if root == nil { return } // 前序遍歷 fmt.Println(root) trans(root.left) trans(root.right) }
最後3句的相對位置,主要是打印的方法的位置不一樣又有3種不一樣的叫法。上面這個是前序遍歷。若是打印放中間就是中序遍歷。若是打印放最後,就是後序遍歷。
廣度優先的遍歷方法,暫時能力還不夠。另外若是要驗證上面的遍歷方法,也只能用笨辦法來建立二叉樹。
看下結構體裏的一些高級用法
能夠給結構體取別名:
type Student struct { Name string } type Stu Student // 取個別名
下面的代碼,給原生的int類型取了個別名,也是能夠像int同樣使用的:
package main import "fmt" type integer int func main() { var i = integer = 100 fmt.Println(i) }
可是定義了別名的類型和原來的類型被系統認爲不是同一個類型,不能直接賦值。可是是能夠強轉類型的:
type integer int func main() { var i integer = 100 var j int // j = i // 不一樣的類型不能賦值 j = int(i) // 賦值須要強轉類型 }
上面都是用原生的 int 類型演示的,自定義的結構體也是同樣的。
golang 中的 struct 不像其餘語言裏的 class 有構造函數。struct 沒有構造函數,通常可使用工廠模式來解決這個問題:
// go_dev/day5/struct/new/model/model.go package model // 名稱是小寫,就是不讓你訪問的 type student struct { Name string Age int } // 外部要調用的是這個工廠函數,返回上面的通過構造函數處理的完成了初始化的結構體,即實例 func NewStudent(name string, age int) *student { // 這裏能夠補充其餘構造函數裏的代碼 return &student{ Name: name, Age: age, } } // go_dev/day5/struct/new/main/main.go package main import ( "../model" "fmt" ) func main() { s := model.NewStudent("Adam", 20) fmt.Println(*s) }
能夠爲 struct 中的每一個字段,寫上一個tag。這個 tag 能夠經過反射的機制獲取到。
爲字段加說明
type student struct { Name string "This is name field" Age int "This is age field" }
json序列化
最經常使用的場景就是 json 序列化和反序列化。先看一下序列化的用法:
package main import ( "fmt" "encoding/json" ) type Stu1 struct{ name string age int score int } type Stu2 struct { Name string Age int score int // 這個仍是小寫,因此仍是會有問題 } func main() { var s1 Stu1 = Stu1 {"Adam", 16, 80} var s2 Stu2 = Stu2 {"Bob", 17, 90} var data []byte var err error data, err = json.Marshal(s1) if err != nil { fmt.Println("JSON err:", err) } else { fmt.Println(string(data)) // 類型是 []byte 轉成 string 輸出 } data, err = json.Marshal(s2) if err != nil { fmt.Println("JSON err:", err) } else { fmt.Println(string(data)) } } /* 執行結果 PS H:\Go\src\go_dev\day5\struct\json> go run main.go {} {"Name":"Bob","Age":17} PS H:\Go\src\go_dev\day5\struct\json> */
結構體中,小寫的字段外部是訪問不了的,因此第一個輸出是空的。而第二個結構體中只有首字母大寫的字段才作了序列化。
因此通常結構體裏的字段名都是首字母大寫的,這樣外部才能訪問到。不過這樣的話,序列化以後的變量名也是首字母大寫的。而json是能夠實現跨語言傳遞數據的,可是在其餘語言裏,都是習慣變量小寫的。這樣go裏json序列化出來的數據在別的語言裏看就很奇怪。
在go的json包裏,經過tag幫咱們作了優化。會去讀取字段的tag,去裏面找到json這個key,把對應的值,做爲字段的別名。具體作法以下:
package main import ( "fmt" "encoding/json" ) type Student struct{ Name string `json:"name"` Age int `json:"age"` Score int `json:"score"` } func main() { var stu Student = Student{"Cara", 16, 95} data, err := json.Marshal(stu) if err != nil { fmt.Println("JSON err:", err) return } fmt.Println(string(data)) // 類型是 []byte 轉成 string 輸出 } /* 執行結果 PS H:\Go\src\go_dev\day5\struct\json_tag> go run main.go {"name":"Cara","age":16,"score":95} PS H:\Go\src\go_dev\day5\struct\json_tag> */
反引號,做用和雙引號同樣,不過內部不作轉義。
結構體力的字段能夠沒有名字,即匿名字段。
type Car struct { Name string Age int } type Train struct { Car // 這個Car也是類型,上面定義的。這裏沒有名字 Start time.TIme int // 這個字段也沒有名字,即匿名字段 }
訪問匿名字段
能夠直接經過匿名字段的類型來訪問,因此匿名字段的類型不能重複:
var t Train t.Car.Name = "beemer" t.Car.Age = 3 t.int = 100
對於結構體類型,還能夠在簡化,結構體的名字能夠不寫,下面的賦值和上面的效果同樣:
var t Train t.Name = "beemer" t.Age = 3
匿名字段衝突處理
type Car struct { Name string Age int } type Train struct { Car Start time.TIme Age int // 這個字段也叫 Age } var t Train t.Age // 這個Age是Train裏的Age t.Car.Age // Car裏的Age如今只能把類型名加上了
經過匿名字段實現繼承
匿名字段在須要有繼承的的場景下很好用:
type Animal struct { Color string Age int Weight int Type string } type Dog Struct { Animal Name string Weight float32 }
定義了一個 Animal 動物類,裏面有不少屬性。再定義一個 Dog 狗的類,也屬於動物,須要繼承動物的屬性。這裏用匿名字段就方便的繼承過來了。而且有些字段還能夠再從新定義覆蓋原先的,好比例子裏的 Weight 。這樣 Dog 就有 Animal 的全部的字段,而且 Dog 還能添加本身的字段,也能夠利用衝突覆蓋父類裏的字段。
Golang 中的方法是做用在特定類型的變量上的。所以自定義類型也能夠有方法,而不只僅是 struct 。
func (變量名 方法所屬的類型) 方法名 (參數列表) (返回值列表) {}
方法和函數的區別就是在func關鍵字後面多了 (變量名 方法所屬的類型) 。這個也別稱爲方法的接收器(receiver)。這個是聲明這個方法是屬於哪一個類型,這裏的類型也包括 struct。
Golang裏的接收器沒有 this 或者 self 這樣的特殊關鍵字,因此名字能夠任意取的。通常而言,出於對一致性和簡短的須要,咱們使用類型的首字母。類比 self ,也就知道這個接收器的變量在方法定義的代碼塊裏就是代指當前類型的實例。
package main import "fmt" type Student struct { Name string Age int } func (s *Student) growup () { s.Age++ } func (s *Student) rename (newName string) { s.Name = newName } func main() { var stu Student = Student{"Adam", 17} fmt.Println(stu) stu.growup() fmt.Println(stu) stu.rename("Bob") fmt.Println(stu) } /* 執行結果 PS H:\Go\src\go_dev\day5\method\beginning> go run main.go {Adam 17} {Adam 18} {Bob 18} PS H:\Go\src\go_dev\day5\method\beginning> */
上面的2個方法裏的接收器類型加了星號。若是不用指針的話,傳入的是對象的副本,方法改變是副本的值,不會改變原來的對象。
另外上面調用方法的用法也已經簡寫了,實際是經過結構體的地址調用的 (&stu).growup()
。
匿名字段就是繼承的用法。不但能夠繼承字段,方法也是繼承的:
package main import "fmt" type Animal struct { Type string } func (a Animal) hello() { fmt.Println(a.Type, "Woo~~") } type Dog struct { Animal Name string } func main() { var a1 Animal = Animal{"Tiger"} var d1 Dog d1.Type = "Labrador" d1.Name = "Seven" a1.hello() d1.hello() // Dog 也能調用 Animal 的方法 }
多繼承
一個 struct 裏用了多個匿名結構體,那麼這個結構體就能夠直接訪問多個匿名結構體的方法,從而實現了多繼承。
若是一個 struct 嵌套了另外一個匿名 struct,這個結果能夠直接訪問匿名結構的方法,從而實現了繼承。
若是一個 struct 嵌套了另外一個有名 struct,這個模式就叫組合:
package main import "fmt" type School struct { Name string City string } type Class struct { s School Name string } func main() { var s1 School = School{"SHHS", "DC"} var c1 Class c1.s = s1 c1.Name = "Class One" fmt.Println(c1) fmt.Println(c1.s.Name) }
繼承與組合的區別:
若是一個變量實現了 String() 方法,那麼 fmt.Println 默認會調用這個變量的 String() 進行輸出。
package main import "fmt" type Animal struct { Type string Weight int } type Dog struct { Animal Name string } func (d Dog) String () string{ return d.Name + ": " + d.Type } func main(){ var a1 Animal = Animal{"Tiger", 230} var d1 Dog = Dog{Animal{"Labrador", 100}, "Seven"} fmt.Println(a1) fmt.Println(d1) } /* 執行結果 PS H:\Go\src\go_dev\day5\method\string_method> go run main.go {Tiger 230} Seven: Labrador PS H:\Go\src\go_dev\day5\method\string_method> */
注意傳值仍是傳指針,例子裏定義的時候沒有星號,是傳值的,打印的時候也是傳值就有想過。打印的使用用 fmt.Println(&d1)
也是同樣的。可是若是定義的時候用了星號,就是傳指針,打印的時候就必須加上&把地址傳進去纔有效果。不然就是按照原生的方法打印出來。
這是go語言多態的實現方式
Interface 類型能夠定義一組方法,可是這些不須要實現,而且 interface 不能包含任何變量。
這裏講接口只是起個頭,下一篇繼續講接口
定義接口使用 interface 關鍵字。而後只須要再裏面定義一個或者多個方法就好,不須要實現:
type 接口名 interface { 方法名1(參數列表) 方法名2(參數列表) [返回值] 方法名3(參數列表) [返回值] }
interface 類型默認是一個指針,默認值是空 nil :
type example interface{ Method() } var a example // 這裏定義了一個接口,a就是一個指針 // 目前a沒有賦值,a裏沒有任何實現,a是一個空指針 a.Method() // a仍是一個空指針,裏面沒有任何實現,這句會報錯
上面定義了a以後,還缺乏一步,給指針a指向一個具體的實現。
Golang 中的接口,不須要顯式的實現,只要一個變量,含有接口類型中的全部方法,那麼這個變量就實現了這個接口。所以,golang 中沒有 implement 類型的關鍵字
若是一個變量含有了多個 interface 類型的方法,那麼這個變量就實現了多個接口。
下面是一個接口實現的示例:
package main import "fmt" // 定義一個接口 type AnimalInterface interface { Sleep() // 定義一個方法 GetAge() int // 再定義一個有返回值的方法 } // 定義一個類 type Animal struct { Type string Age int } // 接下來要實現接口裏的方法 // 實現接口的一個方法 func (a Animal) Sleep() { fmt.Printf("%s need sleep\n", a.Type) } // 實現了接口的另外一個方法 func (a Animal) GetAge() int { return a.Age } // 又定義了一個類,是上面的子類 type Pet struct { Animal Name string } // 重構了一個方法 func (p Pet) sleep() { fmt.Printf("%s need sleed\n", p.Name) } // 有繼承,因此Age方法會繼承父類的 func main() { var a1 Animal = Animal{"Dog", 5} // 建立一個實例 var aif AnimalInterface // 建立一個接口 aif = a1 // 由於類裏實現了接口的方法,因此能夠賦值給接口 aif.Sleep() // 能夠用接口調用 a1.Sleep() // 使用結構體調用也是同樣的效果,這就是多態 var p1 Pet = Pet{Animal{"Labrador", 4}, "Seven"} aif = p1 aif.Sleep() fmt.Println(aif.GetAge()) }
一種事務的多種形態,均可以按照統一的接口進行操做。
多態,簡單點說就是:"一個接口,多種實現"。好比 len(),你給len傳字符串就返回字符串的長度,傳切片就返回切片長度。
package main import "fmt" func main() { var s1 string = "abcdefg" fmt.Println(len(s1)) var l1 []int = []int{1, 2, 3, 4} fmt.Println(len(l1)) }
參照上面的,本身寫的方法也能夠接收接口做爲參數,對不一樣的類型對應多種實現:
package main import "fmt" type Msg interface { Print() } type Message struct { msg string } func (m Message) Print() { fmt.Println("Message:", m.msg) } type Information struct { msg string level int } func (i Information) Print() { fmt.Println("Information:", i.level, i.msg) } func interfaceUse(m Msg) { m.Print() } func main() { message := Message{"Hello"} // 定義一個結構體 information := Information{"Hi", 2} // 定義另一個類型的結構體 // 這裏並不須要 var 接口,以及賦值 interfaceUse(message) // 參數不看類型了,而看你是否知足接口 interfaceUse(information) // 雖然這裏的參數和上面不是同一個類型,可是這裏對參數的要求是接口 }
這裏並不須要顯式的用 var 聲明接口以及賦值。
golang 中並無明確的面向對象的說法,能夠將 struct 類比做其它語言中的 class。
constructor 構造函數
經過結構體的工廠模式返回實例來實現
Encapsulation 封裝
經過自動的大小寫控制可見
Inheritance 繼承
結構體嵌套匿名結構體
Composition 組合
結構體嵌套有名結構體
Polymorphism 多態
經過接口實現
實現一個圖書管理系統,具備如下功能: