【Go語言踩坑系列(六)】面向對象

聲明

本系列文章並不會停留在Go語言的語法層面,更關注語言特性、學習和使用中出現的問題以及引發的一些思考。golang

問題思考

爲何有結構體?

首先,咱們須要明確面向對象的思想是包含各類獨立而又互相調用,這就須要一個承載的數據結構,那麼這個結構是什麼呢?很顯然,在GO語言中就是結構體。
其次,結構體做爲一種數據結構,不管是在C仍是C++仍是Go都發揮了極其重要的做用。另外,在Go語言中其實並無明確的面向對象的說法,實在要扯上的話,咱們能夠將struct比做其它語言中的class。至於爲何不用class,多是做者想要劃清和其餘語言不一樣的界限,畢竟Go在面向對象實現這方面是極其輕量的。咱們簡單看一下結構體的聲明:sql

type Poem struct {
    Title  string //聲明屬性,開頭大小寫表示屬性的訪問權限
    Author string
    intro  string
}

func (poem *Poem) publish() { //和其它語言不同,golang聲明方法和普通方法一致,只是在func後增長了poem Poem這樣的聲明
    fmt.Println("poem publish")
}

結構體比較

若是結構體的所有成員都是能夠比較的,那麼結構體也是能夠比較的,那樣的話兩個結構體將可使用==或!=運算符進行比較。相等比較運算符==將比較兩個結構體的每一個成員,所以下面兩個比較的表達式是等價的:編程

func main() {
    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的key類型。segmentfault

func main() {
    type address struct {
        name string
        age     int
    }

    hits := make(map[address]int)
    hits[address{"nosay", 8}]++
    fmt.Println(hits[address{"nosay", 8}]) // 1

}

結構體在使用時的一個技巧

在結構體傳遞過程當中,若是考慮效率的話,較大的結構體一般會用指針的方式傳入和返回。並且若是要在函數內部修改結構體成員的話,用指針傳入是必須的;由於在Go語言中,全部的函數參數都是值拷貝傳入的(結構體較大的話會從新分配空間,浪費資源),函數參數將再也不是函數調用時的原始變量。數據結構

接口是什麼?

Go 語言中的接口就是一組方法的簽名,它是 Go 語言的重要組成部分。使用接口可以讓咱們更好地組織並寫出易於測試的代碼。但其實接口的本質就是引入一個新的中間層,調用方能夠經過接口與具體實現分離,解除上下游的耦合,上層的模塊再也不須要依賴下層的具體模塊,只須要依賴一個約定好的接口。咱們平常使用的sql又未嘗不是一個接口呢?例以下圖:
調用關係.png函數

GO語言接口是隱式的,一種鴨子模型很明確的體現,那麼鴨子模型是什麼?「當看到一隻鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那麼這隻鳥就能夠被稱爲鴨子。」在接口上體現就是當你實現了接口的全部方法的時候就會認爲你實現了接口,而不用像其餘語言同樣去顯示聲明我實現了這個接口。例以下邊這個例子Dog就實現了Pet接口:學習

type Pet interface {
    SetName(name string)
}

type Dog struct {
    Class string
}

func (dog *Dog) SetName(name string) {
    dog.Class = name
}

在Go語言中,只須要實現全部接口中定義的方法,咱們就默認這個類型實現了接口。測試

接口的數據結構

Go 語言根據接口類型『是否包含一組方法』對類型作了不一樣的處理,也就是分爲空接口和有方法的接口。咱們使用 iface 結構體表示包含方法的接口;使用 eface 結構體表示不包含任何方法的 interface{} 類型。接下來咱們來看看這兩種數據結構。ui

eface:

type eface struct { // 16 bytes
    _type *_type
    data  unsafe.Pointer
}

因爲 interface{} 類型不包含任何方法,因此它的結構也相對來講比較簡單,只包含指向底層數據和類型的兩個指針。從上述結構咱們也能推斷出 — Go 語言中的任意類型均可以轉換成 interface{} 類型。this

另外一個用於表示接口的結構體就是 iface,iface 內部維護兩個指針,tab 指向一個 itab 實體, 它表示接口的類型以及賦給這個接口的實體類型。data 則指向接口具體的值,通常而言是一個指向堆內存的指針。

iface:

type iface struct { // 16 bytes
    tab  *itab
    data unsafe.Pointer
}

接下來咱們分別來看看type和tab裏邊又是什麼內容:

type:

type _type struct {
    size       uintptr //字段存儲了類型佔用的內存空間,爲內存空間的分配提供信息
    ptrdata    uintptr
    hash       uint32 //字段可以幫助咱們快速肯定類型是否相等
    tflag      tflag //類型的 flag,和反射相關
    align      uint8 // 內存對齊相關
    fieldAlign uint8
    kind       uint8 //類型的編號,有bool, slice, struct 等等等等
    equal      func(unsafe.Pointer, unsafe.Pointer) bool //字段用於判斷當前類型的多個對象是否相等,該字段是爲了減小 Go 語言二進制包大小從 typeAlg 結構體中遷移過來的
    gcdata     *byte //gc相關
    str        nameOff
    ptrToThis  typeOff
}

tab:

type itab struct {
    inter  *interfacetype //接口的類型
    _type  *_type //實體的類型
    link   *itab
    hash   uint32 // type.hash的拷貝,用於比較目標類型和接口類型
    bad    bool   // type does not implement interface
    inhash bool   // has this itab been added to hash?
    unused [2]byte
    fun    [1]uintptr // 放置和接口方法對應的具體數據類型的方法地址,這裏存儲的是第一個方法的函數指針,若是有更多的方法,在它以後的內存空間裏繼續存儲
}

type interfacetype struct {
    typ     _type //實體類型
    pkgpath name //包名
    mhdr    []imethod //函數列表
}

由於eface和iface結構上有必定的共性,咱們這裏就只看一下iface數據結構的圖解,eface只是稍微變化一下就能夠:
iface.png
在這裏有個問題,一個iface結構體維護一個接口類型和實體類型的對應關係,咱們在代碼中經常會去屢次實現接口,那怎麼存呢?答案就是隻要在代碼中存在引用關係, go 就會在運行時爲這一對具體的 <Interface, Type> 生成 itab 信息。

值方法和指針方法的區別

咱們都知道,方法的接收者類型必須是某個自定義的數據類型,並且不能是接口類型或接口的指針類型。所謂的值方法,就是接收者類型是非指針的自定義數據類型的方法。那麼,值方法和指針方法體如今哪裏呢?咱們看下邊的代碼:

func (cat *Cat) SetName(name string) {
    cat.name = name
}

方法SetName的接收者類型是*Cat。Cat左邊再加個*表明的就是Cat類型的指針類型,這時,Cat能夠被叫作*Cat的基本類型。你能夠認爲這種指針類型的值表示的是指向某個基本類型值的指針。那麼,這個SetName就是指針方法。那麼什麼是值方法呢?通俗的講,把Cat前邊的*去掉就是值方法。指針方法和值方法究竟有什麼區別呢?請看下文。

  1. 值方法的接收者是該方法所屬的那個類型值的一個副本。咱們在該方法內對該副本的修改通常都不會體如今原值上,除非這個類型自己是某個引用類型(好比切片或字典)的別名類型。

而指針方法的接收者,是該方法所屬的那個基本類型值的指針值的一個副本。咱們在這樣的方法內對該副本指向的值進行修改,卻必定會體如今原值上。這塊可能有點繞,但若是以前函數傳切片那塊理解的話這塊也能夠想明白,總之就是一個拷貝的是整個數據結構,一個拷貝的是指向數據結構的地址。

  1. 一個自定義數據類型的方法集合中僅會包含它的全部值方法,而該類型的指針類型的方法集合卻囊括了前者的全部方法,包括全部值方法和全部指針方法。

嚴格來說,咱們在這樣的基本類型的值上只能調用到它的值方法。可是,Go 語言會適時地爲咱們進行自動地轉譯,使得咱們在這樣的值上也能調用到它的指針方法。

例以下邊這種也是能夠調用的:

type Pet interface {
    Name() string
}

type Dog struct {
    Class string
}

func (dog Dog) Name() string{
    return dog.Class
}

func (dog *Dog) SetName(name string) {
    dog.Class = name
}

func main() {
    a := Dog{"grape"}
    a.SetName("nosay") //a會先取地址而後去調用指針方法
    //Dog{"grape"}.SetName("nosay") //由於是值類型,調用失敗,cannot call pointer method       on Dog literal,cannot take the address of Dog literal
    (&Dog{"grape"}).SetName("nosay") //能夠
}

在後邊你會了解到,一個類型的方法集合中有哪些方法與它能實現哪些接口類型是息息相關的。若是一個基本類型和它的指針類型的方法集合是不一樣的,那麼它們具體實現的接口類型的數量就也會有差別,除非這兩個數量都是零。

好比,一個指針類型實現了某某接口類型,但它的基本類型卻不必定可以做爲該接口的實現類型。例如:

type Pet interface {
   SetName(name string)
   Name()string
   Category()string
}

type Dog struct {
   name string
}

func (dog *Dog) SetName(name string) {
   dog.name = name
}

func(dog Dog) Name()string{
   return dog.name
}

func (dog Dog)Category()string{
   return "dog"
}

func main() {
   dog:=Dog{"little pig"}

   _,ok:=interface{}(dog).(Pet)
   fmt.Printf("Dog implements interface Pet: %v\n", ok) //false
   _, ok = interface{}(&dog).(Pet)
   fmt.Printf("*Dog implements interface Pet: %v\n", ok)
   fmt.Println() //true
}

對於編譯器在什麼狀況下調用這些方法會調用失敗有如下幾種狀況:

值方法 指針方法
結構體初始化變量 經過 不經過
結構體指針初始化變量 經過 經過

說完基礎知識的疑惑,接下來咱們具體舉例看看GO如何實現面向對象的三把斧(繼承,封裝,多態的);

面向對象的三把斧

繼承

首先,咱們須要明確一個概念,Go語言中是沒有繼承的概念的,具體緣由在官網上是明確做出聲明的(參見爲何沒有繼承?,簡單的說,面向對象編程中的繼承,實際上是經過犧牲必定的代碼簡潔性來換取可擴展性,並且這種可擴展性是經過侵入的方式來實現的,而Go由於類型和接口之間沒有明確的關係,因此不須要管理或討論類型層次結構。
那麼,咱們經過下邊一個例子來看一下Go是怎麼經過嵌入組合來實現繼承的:

type Animal struct {
  name string
  subject string
}

// 動物的公共方法
func (a *Animal) Eat(food string) {
  fmt.Println("動物")
}
type Cat struct {
  // 繼承動物的屬性和方法
  Animal
  // 貓本身的屬性
  age int
}

// 貓類獨有的方法
func (c Cat) Sleep() {
  fmt.Println("睡覺")
}

func main() {
  // 建立一個動物類
  animal := Animal{name:"動物", subject:"動物科"}
  animal.Eat("肉")

  // 建立一個貓類
  cat := Cat{Animal: Animal{name:"貓", subject:"貓科"},age:1}
  cat.Eat("魚") //調用的Animal的Eat方法,「繼承」的體現
  cat.Sleep()
}

封裝

Go語言在包的級別進行封裝。 以小寫字母開頭的名稱只在該程序包中可見。 你能夠隱藏私有包中的任何內容,只暴露特定的類型,接口和工廠函數。

例如,在這裏要隱藏上面的Foo類型,只暴露接口,你能夠將其重命名爲小寫的foo,並提供一個NewFoo()函數,返回公共Fooer接口:

type foo struct {
}
 
func (f foo) Foo1() {
    fmt.Println("Foo1() here")
}
 
func (f foo) Foo2() {
    fmt.Println("Foo2() here")
}
 
func (f foo) Foo3() {
    fmt.Println("Foo3() here")
}
 
func NewFoo() Fooer {
    return &Foo{}
}

而後來自另外一個包的代碼可使用NewFoo()並訪問由內部foo類型實現的Footer接口,固然要記得引入包名:

f := NewFoo()
f.Foo1()
f.Foo2()
f.Foo3()

多態

多態性是面向對象編程的本質:只要對象堅持實現一樣的接口,Go語言就能處理不一樣類型的那些對象。 Go接口以很是直接和直觀的方式提供這種能力。
這裏有一個精心準備的例子,實現Ihello接口的多個實現被建立並存儲在一個slice中,而後輪詢調用Hello方法。 你會注意到不一樣實例化對象的風格。

type IHello interface {
    Hello(name string)
}

func Hello(hello IHello) {
    hello.Hello("hello")
}

type People struct {
    Name string
}

func (people *People) Hello(say string) {
    fmt.Printf("the people is %v, say %v\n", people.Name, say)
}

type Man struct {
    People
}

func (man *Man) Hello(say string) {
    fmt.Printf("the people is %v, say %v\n", man.Name, say)
}

type Women struct {
    People
}

func (women *Women) Hello(say string) {
    fmt.Printf("the people is %v, say %v\n", women.Name, say)
}

func Echo(hello []IHello) {
    for _,val := range hello {
        val.Hello("hello world")
    }
}

func main() {
    hello1 := &People{"people"}
    hello2 := &Man{People{Name: "xiaoming"}}
    hello3 := &Women{People{Name: "xiaohong"}}
    
    sli := []IHello{hello1, hello2, hello3}
    //the people is people, say hello world
    //the people is xiaoming, say hello world
    //the people is xiaohong, say hello world

    Echo(sli)
}

下期預告

【Go語言踩坑系列(七)】Goroutine

關注咱們

歡迎對本系列文章感興趣的讀者訂閱咱們的公衆號,關注博主下次不迷路~

Nosay

相關文章
相關標籤/搜索