1. 程式人生 > 實用技巧 >一天搞懂Go語言(4)——介面

一天搞懂Go語言(4)——介面

  很多面向物件語言都有藉口的概念,Go語言的介面獨特之處在於它是隱式實現的。

介面定義和實現

/* 定義介面 */
type interface_name interface {
   method_name1 [return_type]
   method_name2 [return_type]
   method_name3 [return_type]
   ...
   method_namen [return_type]
}

/* 定義結構體 */
type struct_name struct {
   /* variables */
}

/* 實現介面方法 */
func (struct_name_variable struct_name) method_name1() [return_type] {
   /* 方法實現 */
}
...
func (struct_name_variable struct_name) method_namen() [return_type] {
   /* 方法實現*/
}

  介面賦值規則:僅當一個表示式實現了該介面,這個表示式才可以賦給介面型別

var w io.Writer
w = os.Stdout  //ok
w = new(bytes.Buffer) //ok
w = time.Second //錯誤


//當右側表示式也是一個介面時,該規則也有效
var rwc io.ReadWriteCloser
w = rwc //ok
rwc = w //錯誤

  介面也封裝了所對應的型別和資料,只有通過介面暴漏的方法才可以呼叫,型別的其他方法則無法通過介面來呼叫。

os.Stdout.Write([]byte("hello")) //ok
os.Stdout.Close() //ok

var w io.Writer
w = os.Stdout
w.Write([]byre("hello")) //ok
w.Close() //錯誤:io.Writer介面沒有Close方法,有點類似於java裡面父類不能呼叫子類新增的方法

空介面型別

  有點類似於java中的Object類,所有類都派生於Object類。我們可以把任何值賦給空介面型別interface{}。

var any interface{}
any = true
any = "hello"
any = 12.34

  當然即使我們建立了一個指向其他值的空介面,也無法使用其中的值,因為這個介面不包含任何方法,型別斷言可以從空介面中還原出實際值

介面值

  介面的零值是動態型別和值都是nil,此時介面與nil值判斷是相等的,但如果將一個帶有型別的 nil 賦值給介面時,只有 data 為 nil,而 type 不為 nil,此時,介面與 nil 判斷將不相等。

var w io.Writer //type=nil data=nil io.Writer的型別也是nil,暫時沒搞清楚為什麼,空的介面(interface{})型別也是nil
w = os.Stdout  //type=*os.File,data=os.Stdout
w = new(bytes.Buffer) //type=*bytes.Buffer,data=bytes.Buffer
w = nil //type=nil,data=nil

  在編譯時我們無法知道一個介面值的動態型別是什麼,所以通過介面來做呼叫必然需要使用動態分發。編譯器必須生成一段程式碼來從型別描述符拿到名為Write的方法地址,再間接呼叫該方法地址。呼叫的接受者就是介面的動態值,如果是上面第二行程式碼,即os.Stdout,所以實際效果w.Write等價於os.Stdout.Write。

  介面值可以用==和!=進行比較。如果兩個介面值都是nil或者二者的動態型別完全一致且二者動態值相等,那麼兩個介面值相等。介面值可比較,也就可以作為map的鍵,也可以作為switch語句的運算元。

注意:含有空指標的非空介面

  空的介面值與僅僅動態值為nil的介面值不一樣。(空介面和介面值為空不一樣)

介面的應用

使用sort.Interface來排序

  在很多語言中排序演算法跟序列的資料型別繫結,而在go語言的sort.sort函式對序列和其中的元素佈局無任何要求。

  一個原地排序演算法需要知道三個資訊:序列長度、比較兩個元素的含義以及如何交換兩個元素,所以sort.Interface介面就有三個方法:

package sort
type Interface interface{
    Len() int
    Less(i,j int) bool
    Swap(i,j int)
}

type StringSlice []string

func (p StringSlice) Len() int {return len(p)}
func (p StringSlice) Less(i,j int){return p[i]<p[j]}
func (p StringSlice) Swap(i,j int) {p[i],p[j]=p[j],p[i]}

sort.Sort(StringSlice(names)) //先把names轉換為定義排序規則的新型別StringSlice

  sort包提供了StringSlice型別,可以直接使用sort.Strings(names)進行排序。

  讓我們定義一個多層比較函式:

type Track struct{
  Title  string
  Artist  string
  Album  string
  Year  string
  Length  time.Duration
}

type customSort struct{
  t  []*Track
  less  func(x,y *Track) bool //排序規則,返回值為bool型別的函式變數
}
func (x customSort) Len() int  {return len(x.t)}
func (x customSort) Less(i,j int) bool  {return x.less(x.t[i],x.t[j])}
func (x customSort) Swap(i,j int)  {return x.t[i],x.t[j]=x.t[j],x.t[i]}

sort.Sort(customSort{tracks,func(x,y *Track)bool{
  if x.Title!=y.Tile{
    return x.Title<y.Title
  }
  if x.Year!=y.Year{
    return x.Year<y.Year
  }
  return false
}
})

error介面

type error interface{
    Error() string
}

//完整的error包
package errors

func New(text string) error  {return &errorString(text)}
type errorString struct {struct string} //不適用字串是為了避免將來無意間的佈局變更
func (e *errorString) Error() string {return e.text} //使用指標是為了讓每次New分配的error例項都互不相等

  構造error最簡單的方法是呼叫errors.New方法。直接呼叫比較罕見,因為有一個更易用的封裝函式fmt.Errorf,它還額外提供了字串格式化功能。

package fmt

import "errors"

func Errorf(fomat string,args ...interface{}) error{
  return errors.New(Sprintf(format,args...))
}

型別斷言

  型別斷言是一個作用在介面值上的操作,寫出來類似於x.(T),其中x是一個介面型別的表示式,T是一個型別。型別斷言會檢查作為運算元的動態型別是否滿足指定的斷言型別,如果檢查成功,會返回x的動態值,換句話說型別斷言就是用來從它的運算元中把具體型別的值提取出來的操作

var w io.Writer
w = os.Stdout
f:=w.(*os.File) //成功:f==os.Stdout
c:=w.(*bytes.Buffer) //崩潰

  如果斷言型別T是一個介面型別,那麼型別斷言檢查x的動態型別是否滿足T。如果滿足,動態值並沒有被提取出來,結構仍是一個介面值。