13 Go語言——Struct 和Method

Go語言——Struct 和Method

struct特色:java

  • 使用 type <Name> struct{} 定義結構,名稱遵循可見性規則
  • struct是值類型
  • 可使用字面值對結構進行初始化
  • 支持匿名結構,和匿名字段
  • 容許直接經過指針來讀寫結構成員
  • 相同類型的成員可進行直接拷貝賦值,支持 == 與 !=比較運算符,但不支持 > 或 <
  • 嵌入結構做爲匿名字段看起來像繼承,但不是繼承
  • Go中的struct沒有構造函數,通常可使用工廠模式來解決這個問題

1、結構體struct

一、struct介紹

struct 首先是一種類型,值類型它是由一系列具備相同類型或不一樣類型的數據構成的數據集合。和c語言的struct很像,其實就至關於java的class,可是go是沒有class 概念的。定義類型的目的其實就是告訴編譯器要開闢多少內存,內存中放什麼類型,而結構體組合多個類型。express

二、struct 定義和初始化

type person struct{//定義一個person struct,
    Name string
    Age int
}
func main(){
    a:=person{}//初始化這個struct,這個時候會用類型零值進行初始化
    fmt.Println(a)
    a.Name="tom" //對結構體的屬性進行賦值
    a.Age=19
    fmt.Println(a)
}
//打印結果
{ 0}
{tom 19}

從上面的例子看到,咱們可使用type <Name> struct{} 來定義一個結構體,裏面能夠聲明結構體擁有的屬性。(結構體的名字遵循大小寫訪問權限)。而後,使用a:=person{} 初始化了一個空的結構體,這是時候打印出的是{空字符串 0},(初始化結構體時默認使用零值)。接下來能夠經過. 來操做初始化了的結構體,訪問它的屬性進行賦值操做。編程

接下來列出struct初始化的方式函數

2.1 使用var 關鍵字聲明,並初始化爲其零值學習

var Tom person   //關鍵字 var 建立了類型爲 person 且名爲 Tom 的變量
//注意,這裏會使用person各個屬性的零值進行初始化

2.2 使用字面量來聲明建立結構體變量,ui

第一種形式:指針

tom:=person{  //使用:= 建立了person結構體類型的變量tom,初始值是 tom,和18
    Name:"Tom",
    Age:18,
}
//注意:每一行必須以逗號結尾

第二種形式:code

tom:=person{"Tom",20} //必需要和結構聲明中字段的順序一致
//固然也能夠這樣---都是同樣的,注意順序就能夠
tom:=person{
    "Tom",
    20,
}

2.3 嵌套結構體對象

type student struct{
    per person        //person結構體做爲屬性
    grade string      //student的屬性,
}

其實使用這種組合的方式,能夠將不一樣的結構體組成一個集合,其實也是實現了面向對象的繼承。繼承

小結,struct是值類型,賦值和傳參會複製所有內容。可⽤ "_" 定義補位字段,⽀持指向⾃⾝類型的指針

三、結構體比較

struct ⽀持 "=="、"!=" 相等操做符,可⽤做 map 鍵類型。 相等比較運算符==將比較兩個結構體的每一個成員

type Point struct{ X, Y int }
p := Point{1, 2}
q := Point{2, 1}
fmt.Println(p.X == q.X && p.Y == q.Y) // "false"
fmt.Println(p == q) // "false"

既然能夠比較,那麼就能夠做爲map鍵類型

type User struct {
    id int
    name string
}
m := map[User]int{
    User{1, "Tom"}: 100,
}

四、匿名字段

Go語言有一個特性讓咱們只聲明一個成員對應的數據類型而不指名成員的名字;這類成員就叫匿名成員。匿名成員的數據類型必須是命名的類型或指向一個命名的類型的指針。

type person struct{ //匿名字段
     string
     int
}
func main(){
    tom:=person{"name",19} //順序必須和聲明一致
    fmt.Println(tom)
}

五、結構體嵌入

type human struct {  //定義 human的結構體
    Sex int
 }
 type teacher struct { //定義teacher結構體
    human          //組合的形式,不存在繼承直接放入human
    Name  string
    Age  int
 
 }
 
 type  student struct {
    human        //自動以human命名匿名結構
    Name string
    Age int
 }
 func main() {
     // 因此須要用human這個名稱進行操做這個匿名結構
    a := teacher{Name:"joe",Age:18,human:human{Sex:0}} 
    b:= student{Name:"joe",Age:18,human:human{Sex:0}}
    a.Name="aaaa"
    a.Age=11
    a.Sex=100     //能夠直接修改
    fmt.Println(a,b)
 }

從上面的例子能夠看出,teacher 和student 都是人,若是把human 結構體做爲他們的內部屬性,那就至關於teacher和student擁有了 human結構體的全部屬性和方法,變相實現了繼承。

2、Method

  • 只能爲當前包內命名類型定義方法。
  • 參數receiver可任意命名。如方法中不曾使用,可省略參數名。
  • 參數 receiver 類型能夠是 T 或 *T。基類型 T 不能是接口或指針。
  • 不支持方法重載, receiver 只是參數簽名的組成部分。
  • 可用實例 value 或 pointer 調用所有⽅法,編譯器自動轉換。
  • 類型別名不會擁有底層類型所附帶的方法

一、方法聲名

type A struct{   //定義結構體 A 
    Name string
}
type B struct{    //定義結構體B
    Name string
}

func (a A) Print(){  //結構體A的方法
    fmt.Println("A的方法")
}
func (b B) Print(){ //結構體B的方法
    fmt.Println("B的方法")
}
func main(){
    a:=A{}
    a.Print()
    b:=B{}
    b.Print()
}

從上面的代碼看出,方法實際上也是函數,只是在聲明時,在關鍵字func 和方法名之間增長了一個參數,

func (接收者變量名  接收者) 方法名(){

}

receiver 聲名的是誰,這個就是哪一個結構體的方法。注意:只能爲同一個包中的類型定義方法

沒有構造和析構方法,一般用簡單工廠模式返回對象實例。

type Person struct {
    Name string
}
func setName() *Person { // 建立對象實例。
    return &Person{}
}
func main(){
    a:=setName()
    a.Name="zhangsan"
    fmt.Println(a.Name)
}

二、值接收者和指針接收者

type Data struct {
    x int
}

func (self Data) ValueTest() { // 值接受者方法
    fmt.Printf("值接收者: %p\n", &self)
}
func (self *Data) PointerTest() { // 指針接受者方法
    fmt.Printf("指針接受者: %p\n", self)
}
func main() {
    d := Data{}
    p := &d
    fmt.Printf("原始地址: %p\n", p)
    d.ValueTest()   // ValueTest(d)
    d.PointerTest() // PointerTest(&d)
    p.ValueTest()   // ValueTest(*p)
    p.PointerTest() // PointerTest(p)
}
//輸出結果
原始地址: 0xc00005a058
值接收者: 0xc00005a090
指針接受者: 0xc00005a058
值接收者: 0xc00005a098
指針接受者: 0xc00005a058

這個實例能夠看到,定義結構體的方法能夠是值接收者,也能夠是指針接收者。區別在於方法名前面的receiver一個是指針,一個是實際的值。

而且調用方法的時候,struct 變量的值能夠調用指針接收者的方法,指針也能夠調用值接受者的方法。分開來講這幾種狀況:

2.1 使用值調用值接收者方法

func (self Data) ValueTest() { // 值接受者方法
    fmt.Printf("值接收者: %p\n", &self)
}

使用值來調用此方法,receiver是 Data類型。當發起調用的時候,此方法操做的是值調用者的一個值的拷貝。如最上面的代碼中,連續值調用最終打印出的地址是不一樣的,每次都不相同,由於每一次的調用,都是使用的值的拷貝,而且對這個值作任何操做,都不會影響初始值。

2.2 使用指針調用指針接收者方法

func (self *Data) PointerTest() { // 指針接受者方法
    fmt.Printf("指針接受者: %p\n", self)
}

使用指針來調用此方法,receiver是一個指向Data類型的指針。當調用使用指針接收者聲明的方法時,這個方法會共享調用方法時接收者所指向的值。其實也就是,此方法直接能夠操做接收者指針指向的底層數據的值。這樣方法對值進行任何改變都會對原始值產生影響。 相似引用傳遞。(指針的拷貝)

2.3 使用指針調用值接收者方法

使用指針來調用值接收者方法就有意思了。值接收者前面說了是對值的拷貝進行操做,那麼指針怎麼樣?其實這是go語言編譯器作了處理,好比p.ValueTest() 這段代碼在編譯器會修改成(*p).ValueTest() ,也就是編譯器會自動先取出指針指向的值,而後調用值接收者方法。注意:這裏的調用也是一個值的拷貝,只不過拷貝的是指針指向的值。再次聲明:此方法操做的是指針指向的值的副本。 也就是,此方法怎麼操做也不會影響原始值。

2.4 使用值調用指針接收者方法

使用值來調用指針接收者方法。這個一看很矛盾,指針怎麼接受值。因而乎go的編譯器再一次作了調整,如d.PointerTest(),通過編譯器處理變爲(&d).PointerTest() 。這就很明顯了,其實就是語法糖,爲了方便。編譯器會先取出值的地址,而後使用此地址指向的值進行處理。操做的也是原始值。

總結下:值接收者使用值的副原本調用方法,而指針接受者使用實際值來調用方法

2.五、廢除指針的多級調用

從1.4開始再也不支持指針的多級調用方法(瞭解)

type X struct{}
func (*X) test() {
    println("X.test")
}
func main() {
    p := &X{}
    p.test()
    // Error: calling method with receiver &p (type **X) requires explicit dereference
    // (&p).test()
}

不能將一個指針的指針去調用指針接受者的方法,只能用這個接受者的指針去調用。

三、匿名字段

能夠像字段成員那樣訪問匿名字段⽅法,編譯器負責查找

type User struct { //User 結構體
    id   int
    name string
}
type Manager struct {
    User        //匿名字段
}

func (self *User) ToString() string { //User的方法
    return fmt.Sprintf("User: %p, %v", self, self)
}
func main() {
    m := Manager{User{1, "張大仙"}} 
    fmt.Printf("ManagerL: %p\n", &m)
    fmt.Println(m.ToString())
}
//輸出·
ManagerL: 0xc000054400
User: 0xc000054400, &{1 張大仙}

從上面代碼看到,Manager 內部聲明瞭User類型的匿名字段,那麼Manager就具有了User的全部方法。

若是匿名字段的方法和外部結構的方法重名怎麼辦? 其實仍是就近原則,若是外部結構和嵌入結構存在同名方法,則優先調用外部結構的方法

因而可知,咱們能夠理解爲:使用匿名字段,就至關於擴展了此struct的功能,也就是變相實現了繼承,就能夠直接調用匿名字段的方法。若是咱們要實現方法重寫,那麼就能夠在外層定義一個重名方法,修改方法內容,那不就是實現了方法重寫嗎!!!

type User struct { //User 結構體
    id   int
    name string
}
type Manager struct {
    User              //匿名字段
    title string    //Manager特有的字段
}

func (self *User) ToString() string { //user的方法
    return fmt.Sprintf("User: %p, %v", self, self)
}
func (self *Manager) ToString() string {//manager 的方法
    return fmt.Sprintf("Manager: %p, %v", self, self)
}
func main() {
    m := Manager{User{1, "張大仙"}, "標題"}
    fmt.Println(m.ToString()) 
    fmt.Println(m.User.ToString())
}
//輸出結果
Manager: 0xc00006e240, &{{1 張大仙} 標題}
User: 0xc00006e240, &{1 張大仙}

四、Method Value和Method Expression

方法值和方法表達式,從某種意義上來講,方法是函數的語法糖,由於receiver其實就是方法所接收的第1個參數(Method Value vs. Method Expression)

根據調⽤者不一樣,⽅法分爲兩種表現形式:

instance.method(args...) ---> <type>.func(instance, args...)

二者均可像普通函數那樣賦值和傳參,區別在於 method value 綁定實例,⽽ method expression 則須顯式傳參。

type TZ int   //自定義一個int類型的類型TZ
func (a *TZ)Print(){
    fmt.Println("TZ")
}
func main(){
    var a TZ  //聲明變量a  TZ 類型
    a.Print()    //Method Value 方式
    (*TZ).Print(&a)// Method Expression 方式
}

這裏借用下go學習筆記的總結,慢慢體會

五、方法集:

go語言,每種類型都有與之關聯的方法集,方法集定義了一組關聯到給定類型的值或者指針的方法,定義方法時使用的接受者的類型決定了這個方法是關聯到值,仍是關聯到指針,仍是兩個都關聯。這對於接口的實現規則很是重要。對於方法集規則,在講接口的時候會詳細寫明,此處瞭解便可。這裏只需記住:直接用實例或者實例的指針調用方法,不受方法集約束,接口的實現規則纔會受方法集約束。

/*
每一個類型都有與之關聯的方法集,這會影響到接口實現規則
- 類型T方法集包含所有receiver T方法
- 類型*T 方法集包含所有receiver T+ *T方法
- 如類型S包含匿名字段T,則S方法集包含T方法
- 如類型S包含匿名字段*T,則S方法集包含T+*T方法
- 無論嵌入T或者* T,*S方法集老是包含T+*T方法
*/

3、面向對象

go 沒有class關鍵字,struct替代了class的做用,go對於面向對象的支持是不同的。拿struct來講,struct沒有繼承的概念,go語言是經過 組合的概念來進行面向對象編程。面向對象的目的其實就是代碼複用,go經過組合不一樣的結構體,使這個struct具備更多的功能,複用其餘結構體的屬性和方法,這個就是struct的繼承。

因此,對go語言來講,封裝採用首字母大小寫的可見性支持,繼承採用不一樣struct的組合來實現。多態會在講接口的時候說。

一、 封裝

go是直接支持strut的封裝的,go語言的可見性是根據首字母大小寫來實現的。 首字母大寫表示對外部可見,等同Java中的public,首字母小寫,對外部不可見,等同於Java中的private。

type Student struct{ //聲名一個對包外部可見的結構體,
    name string
    age int
}
func (s *Student)setName(name string){
    s.name=name
}
func (s *Student)setAge(age int){
    s.age=age
}
func (s *Student)getName() string{
    return s.name
}
func (s *Student)getAge()int{
    return s.age
}
func main(){
    s:=Student{}
    fmt.Println(s)
    s.setName("張三")
    s.setAge(18)
    fmt.Println(s)
}
//輸出
{ 0}
{張三 18}

這裏的可見不可見是相對於包外部說得,對於同一個包內都是可見的。

二、繼承

使用匿名字段或者叫結構體嵌入,能夠變向的實現繼承,而且能夠實現多繼承

go語言的struct使用組合的方式實現繼承,比傳統的面向對象更加靈活,能夠有效的避免相似Java的超多層級的繼承。而go另闢蹊徑,根據須要本身組合,也就是組合大於繼承。代碼見上面方法的匿名字段。

相關文章
相關標籤/搜索