Go筆記-結構體和接口

go沒有面向對象的概念,因此也沒有繼承概念,有的只是組合。java

類型系統

爲類型添加新方法

func main() {
	var v Integer = 1
	fmt.Println(v.Less(2)) // true
}

type Integer int

func (a Integer) Less(b Integer) bool {
	return a < b
}

Integer是一個新類型(也能夠當作int的別名),它和int沒有本質不一樣,只是它爲內置的int類型增長了個新方法Less()。這裏並非真正意義上的別名,由於使用這種方法定義以後的類型能夠擁有更多的特性,且在類型轉換時必須顯式轉換.golang


int 和 Integer 能夠相互轉換: Integer(23); int(Integer(23)); , 並且這種轉換並不會建立新的值數組


只有須要修改對象的時候,才必須用指針數據結構

func main() {
	var v Integer = 1
	v.Add(3)
	fmt.Println(v) // 4
}

type Integer int

func (a *Integer) Add(b Integer) {
	*a += b // 等同 *a = *a + b
}

若是沒有指針:併發

func main() {
	var v Integer = 1
	v.Add(3)
	fmt.Println(v) // 1
}

type Integer int

func (a Integer) Add(b Integer) {
	a += b
}

Go語言和C語言同樣,類型都是基於值傳遞的。要想修改變量的值,只能傳遞指針。app

使用指針的Append框架

type ByteSlice []byte

func (s *ByteSlice) Append(data byte) {
	slice := *s
	slice = append(slice, data)
	*s = slice
}

var s ByteSlice = []byte{1, 2, 3}
(&s).Append(4)
fmt.Println(s) // [1 2 3 4]

nil指針也能夠調用方法函數

bytes.Buffer中聲明瞭一個方法:測試

func (b *Buffer) String() string {
	if b == nil {
		// Special case, useful in debugging.
		return "<nil>"
	}
	return string(b.buf[b.off:])
}

當這樣調用時:優化

var buf *bytes.Buffer = nil
fmt.Println(buf.String())

並不會報錯,而是輸出 <nil>,也就是說buf爲空指針時也是能夠調用方法的。而若是buf的類型是bytes.Buffer而不是指針時就會直接報錯。

若是你有多個類型須要定義,可使用因式分解關鍵字的方式,例如:

type (
    IZ int
    FZ float
    STR string
)

每一個值都必須在通過編譯後屬於某個類型(編譯器必須可以推斷出全部值的類型),由於 Go 語言是一種靜態類型語言.

這種方式並不擁有原類型的方法,但擁有原類型的字段

func main() {
	s := &Sun{}
//	s.BaseFun() // 錯誤,沒有該方法
	fmt.Println(s.f) // 正確,有f字段
}

type Base struct {
	f string
}

func (this *Base) BaseFun() {
	fmt.Println("BaseFun")
}

type Sun Base

值語義和引用語義

Go語言中的大多數類型都基於值語義,包括:

基本類型,如byte、int、bool、float3二、float64和string等;

複合類型,如數組(array)、結構體(struct)和指針(pointer)等

數組傳遞也是值傳遞

a := [3]int{1, 2, 3}
b := a
b[0]++
fmt.Println(a) // [1 2 3]
fmt.Println(b) // [2 2 3]

若是b是a的指針類型的話:

a := [3]int{1, 2, 3}
b := &a // 或者var b = &a
b[0]++
fmt.Println(a) // [2 2 3]
fmt.Println(b) // &[2 2 3]
fmt.Println(*b) // [2 2 3]

此時b的類型不是[3]int,而是*[3]int

Go語言中有4個類型比較特別,看起來像引用類型

數組切片:指向數組(array)的一個區間。

map:極其常見的數據結構,提供鍵值查詢能力。

channel:執行體(goroutine)間的通訊設施。

接口(interface):對一組知足某個契約的類型的抽象

這些類型的定義中有指針類型的字段:

type slice struct { 
	first *T 
	len int 
	cap int 
}

自定義引用類型

type MyType struct {
	val *int
}

結構體

Go語言的結構體(struct)和其餘語言的類(class)有同等的地位,但Go語言放棄了包括繼承在內的大量面向對象特性,只保留了組合(composition)這個最基礎的特性

定義

type Rect struct {
	x, y, width, height float64
}

func (r *Rect) Area() float64 {
	return r.width * r.height
}

初始化

rect1 := new(Rect)
rect2 := &Rect{}
//rect3 := &Rect{0, 0} // 錯誤
rect4 := &Rect{0, 0, 5, 5}
rect5 := &Rect{width: 20, height: 20}

&rect{n}的類型是指針類型,若是不想得到指針,能夠去掉&:

rect := Rect{}
var rect Rect = Rect{}

除了第一個外,其他都是花括號{}

要麼都不賦值(有默認值),要麼都賦值,只想給一部分賦值的話須要標明字段名

另外一種方式

type Person struct {
	name string
	age  int
}

var p Person // 這不僅是聲明瞭還初始化了???
fmt.Println(p) // { 0}
p.name, p.age = "chen", 23
fmt.Println(p) // {chen 23}

重複的鍵將致使報錯:

p := Persion{
	Name: "a",
	Name: "a",
}

報錯信息:

duplicate field name in struct literal: Name

匿名結構體和結構體數組

var v = struct {
		a, b string
	}{"aa", "bb"} // 聲明的同時初始化

	fmt.Println(v) // {aa bb}

	var vv = []struct {
		a, b string
	}{
		{"a1", "b1"},
		{"a2", "b2"},
	}
	fmt.Println(vv) // [{a1 b1} {a2 b2}]
type Point struct {
	x, y int
}

func main() {
	points := []Point{
		{1, 2},
		{3, 4},
	}
	fmt.Println(points)
}

先聲明,延後初始化

var inner struct {
		E interface{}
	}
	inner.E = 123

空結構體地址相同

type empty struct {
}
e1 := &empty{}
e2 := &empty{}
fmt.Printf("%p, %p\n", e1, e2)

輸出

0x5b5f60, 0x5b5f60

若是empty裏有字段,那地址就不一樣了。多是爲了優化,若是結構體裏沒有字段只有方法,那麼兩個結構體的執行結果確定相同,因此共用一個地址了。

結構體數組能夠不須要寫結構體名了

type Field struct {
	name string
}

fields := []Field{
	{"a"}, {"b"}, {"c"},
}

或者:

fields := []*Field{
	{"a"}, {"b"}, {"c"},
}

匿名組合

確切地說,Go語言也提供了繼承,可是採用了組合的文法,因此咱們將其稱爲匿名組合

type Base struct {
	Name string
}

func (b *Base) Foo() {
	fmt.Printf("Base.Foo():%s\n", b.Name)
}
func (b *Base) Bar() {
	fmt.Printf("Base.Bar():%s\n", b.Name)
}

type Sub struct {
	Base
	Name string
}

func (s *Sub) Foo() {
	s.Base.Foo() // 調用Base的方法
	fmt.Printf("Sub.Foo():%s\n", s.Name)
}

func main() {
	base := &Base{"i am base."}
	base.Foo()
	base.Bar()

	sub := &Sub{*base, "i am sub."} // 由於base是指針,這裏要加*
	sub.Foo()
	sub.Base.Bar()
	sub.Bar() // 其實調用的是Base的方法

	fmt.Println(sub.Name)

}

輸出:

Base.Foo():i am base.
Base.Bar():i am base.
Base.Foo():i am base.
Sub.Foo():i am sub.
Base.Bar():i am base.
Base.Bar():i am base.
i am sub.

這是組合,不是繼承,只是能夠當作是繼承,下面把Sub賦值給Base是錯誤的:

var v *Base = sub

提示:

cannot use sub (type *Sub) as type *Base in assignment

Sub會繼承Base的屬性和方法,能夠當成本身的使用,也能夠經過類型名來訪問,就像java中的super

type Person struct {
	string
	age int
}
p := Person{"person", 11}
fmt.Println(p.age, p.string) // 11 person,直接經過類型名訪問

Sub沒有Bar()方法,但仍是能夠調用Base的Bar(),調用形式看起來像是繼承


當咱們內嵌一個類型時,該類型的全部方法會變成外部類型的方法,可是當這些方法被調用時,其接收的參數仍然是內部類型,而非外部類型。


Sub中Base是值類型,這樣當在Sub中修改了Base.Name, base是不會有反應的

sub.Base.Name = "modify"
fmt.Println(base.Name) // 仍是i am base.

還能夠繼承Base指針

type Sub struct {
	*Base
	Name string
}

/* 重寫方法 */
func (s *Sub) Foo() {
	s.Base.Foo() // 調用Base的方法
	fmt.Printf("Sub.Foo():%s\n", s.Name)
}

func main() {
	base := &Base{"i am base."}
	base.Foo()
	base.Bar()

	sub := &Sub{base, "i am sub."}
	sub.Foo()
	sub.Base.Name = "modify"
	fmt.Println(base.Name) // modify
}

修改了sub中的Base,base也被同步了


在Go語言官方網站提供的Effective Go中曾提到匿名組合的一個小价值,值得在這裏再提一下。首先咱們能夠定義以下的類型,它匿名組合了一個log.Logger指針:

type Job struct{ 
	Command string
	*log.Logger 
}

在合適的賦值後,咱們在Job類型的全部成員方法中能夠很溫馨地借用全部log.Logger提供的方法。好比以下的寫法:

func(job *Job)Start() { 
	job.Log("starting now...") 
	... // 作一些事情
	job.Log("started.") 
}

對於Job的實現者來講,他甚至根本就不用意識到log.Logger類型的存在,這就是匿名組合的魅力所在。在實際工做中,只有合理利用才能最大發揮這個功能的價值。

須要注意的是,無論是非匿名的類型組合仍是匿名組合,被組合的類型所包含的方法雖然都升級成了外部這個組合類型的方法,但其實它們被組合方法調用時接收者並無改變。好比上面這個Job例子,即便組合後調用的方式變成了job.Log(...),但Log函數的接收者仍然是log.Logger指針,所以在Log中不可能訪問到job的其餘成員方法和變量。 這其實也很容易理解,畢竟被組合的類型並不知道本身會被什麼類型組合,固然就無法在實現方法時去使用那個未知的「組合者」的功能了


匿名組合類型至關於以其類型名稱(去掉包名部分)做爲成員變量的名字

type Logger struct{ 
	Level int
} 
type Y struct{ 
	*Logger 
	Name string
	*log.Logger 
}

Y類型中就至關於存在兩個名爲Logger的成員,雖然類型不一樣。所以,咱們預期會收到編譯錯誤。有意思的是,這個編譯錯誤並非必定會發生的。假如這兩個Logger在定義後再也沒有被用過,那麼編譯器將直接忽略掉這個衝突問題,直至開發者開始使用其中的某個Logger

隱藏結構體的方法

來源:io包下io_test.go的Buffer。

func main() {
	var a *A = new(A)
	a.f()
	var b *B = new(B)
	b.f()
}

type A struct {
}

func (this A) f() {
	fmt.Println("f() from A")
}

type I interface {
	f()
}

type B struct {
	A
	//I // 若是放開,調用b.f()會報錯
}

淺複製

type A struct {
	a string
	b int
	c map[string]string
}

func main() {
	a := &A{"a", 1, map[string]string{"a": "a"}}
	b := new(A)
	*b = *a // 淺複製
	fmt.Println(a, b)
	a.c["b"] = "b" // 兩個都改變了
	a.b = 2        // 只有a改變了
	fmt.Println(a, b)
}

輸出:

&{a 1 map[a:a]} &{a 1 map[a:a]}
&{a 2 map[b:b a:a]} &{a 1 map[a:a b:b]}

可見性

要使某個符號對其餘包(package)可見(便可以訪問),須要將該符號定義爲以大寫字母開頭,小寫字母開頭的僅對包內可見,包括包內其餘的類型。

接口

非侵入式接口

在Go語言中,一個類只須要實現了接口要求的全部函數,咱們就說這個類實現了該接口

type IFile interface {
	Read(buf []byte) (n int, err error)
	Write(buf []byte) (n int, err error)
	Close() error
}

type IRead interface {
	Read(buf []byte) (n int, err error)
}

type File struct {
	// ...
}

func (f *File) Read(buf []byte) (n int, err error) {
	fmt.Println("File.Read()")
	return 0, nil
}
func (f *File) Write(buf []byte) (n int, err error) {
	fmt.Println("File.Write()")
	return 0, nil
}
func (f *File) Close() error {
	fmt.Println("File.Read()")
	return nil
}

func main() {
	var file1 IFile = new(File)
	file1.Read([]byte{})
	file1.Write([]byte{})
	file1.Close()

	var file2 IRead = new(File)
	file2.Read([]byte{})
	//file2.Close() // 錯誤,未定義
}

接口賦值(對象實例賦值給接口)

定義實現類

type Integer int

func (a Integer) Less(b Integer) bool {
	return a < b
}
func (a *Integer) Add(b Integer) {
	*a += b
}

再定義接口(這樣接口就得事先知道實現類,否則不知道有Integer??)

type LessAdder interface {
	Less(b Integer) bool
	Add(b Integer)
}

賦值

var a Integer = 1 
var b LessAdder = &a ... (1) 
var b LessAdder = a ... (2) 錯誤

緣由在於,Go語言能夠根據下面的函數:

func(a Integer) Less(b Integer) bool

自動生成一個新的Less()方法:

func(a *Integer) Less(b Integer) bool{ 
	return(*a).Less(b) 
}

這樣,類型*Integer就既存在Less()方法,也存在Add()方法,知足LessAdder接口。而從另外一方面來講,根據

func(a *Integer) Add(b Integer)

這個函數沒法自動生成如下這個成員方法:

func(a Integer) Add(b Integer) { 
	(&a).Add(b) 
}

由於(&a).Add()改變的只是函數參數a,對外部實際要操做的對象並沒有影響,這不符合用戶的預期。因此,Go語言不會自動爲其生成該函數。所以,類型Integer只存在Less()方法,缺乏Add()方法,不知足LessAdder接口,故此上面的語句(2)不能賦值

爲了進一步證實以上的推理,咱們不妨再定義一個Lesser接口,以下:

type Lesser interface{ 
	Less(b Integer) bool
}

而後定義一個Integer類型的對象實例,將其賦值給Lesser接口:

vara Integer = 1 
varb1 Lesser = &a ... (1) 
varb2 Lesser = a ... (2)

正如咱們所料的那樣,語句(1)和語句(2)都可以編譯經過

接口賦值(接口賦值給接口)

在Go語言中,只要兩個接口擁有相同的方法列表(次序不一樣沒關係),那麼它們就是等同的,能夠相互賦值 下面咱們來看一個示例,這是第一個接口:

package one 
type ReadWriter interface{ 
	Read(buf []byte) (n int, err error) 
	Write(buf []byte) (n int, err error) 
}

第二個接口位於另外一個包中:

package two 
type IStream interface{ 
	Write(buf []byte) (n int, err error) 
	Read(buf []byte) (n int, err error) 
}

如下這些代碼可編譯經過:

var file1 two.IStream = new(File) 
var file2 one.ReadWriter = file1 
var file3 two.IStream = file2

接口賦值並不要求兩個接口必須等價。若是接口A的方法列表是接口B的方法列表的子集,那麼接口B能夠賦值給接口A

例如,假設咱們有Writer接口:

type Writer interface{ 
	Write(buf []byte) (n int, err error) 
}

就能夠將上面的one.ReadWriter和two.IStream接口的實例賦值給Writer接口:

var file1 two.IStream = new(File) 
var file4 Writer = file1

但反過來並不成立

接口查詢

定義兩個接口和一個實現類

type Intf1 interface {I
	F()
}

type Intf2 interface {
	F()
}

type Impl1 struct{}

func (a Impl1) F() {
	fmt.Println("F()")
}

查詢Intf1指向的對象是否也實現了Intf2:

var intf1 Intf1 = Impl1{}
if what, ok := intf1.(Intf2); ok {
	fmt.Println("ok")
	fmt.Println(what == intf1)
} else {
	fmt.Println("no")
}

輸出:

ok   
true

若是intf1.(Intf2)中的intf1不是接口類型,會報錯:

var intf1 Impl1 = Impl1{} // 這樣不行

判斷是不是字符串

var s interface{} // s的類型必定要是接口
s = "hello"
if _, ok := s.(string); ok {
	fmt.Println("is string")
} else {
	fmt.Println("not string")
}

也能夠不寫ok : var.(type), 可是若是沒法轉換的話就直接panic了。

類型查詢

在Go語言中,還能夠更加直截了當地詢問接口指向的對象實例的類型

var intf1 interface{} = Impl1{}
switch intf1.(type) { // 類型查詢
case int:
	fmt.Println("int")
case string:
	fmt.Println("string")
case Intf1: 
	fmt.Println("intf1...")
default: // 配合接口查詢
	if v, ok := intf1.(Intf1); ok {
		fmt.Printf("Intf1,%T\n", v)
	}
	if v, ok := intf1.(Impl1); ok {
		fmt.Printf("Impl1,%T\n", v)
	}
}

輸出:

Intf1,intf.Impl1
Impl1,intf.Impl1

注意:

  1. ‘.’以前的必須是接口類型
  2. intf1.(Intf1)將Interface{}類型的轉換爲了Intf1,所以能夠寫成
if v, ok := intf1.(Intf1); ok {
  			fmt.Printf("Intf1,%T\n", v)
  			v.F()
  		}
  1. 類型switch中沒法使用fallthrough
case Intf1:
  		fmt.Println("intf1...")
  		fallthrough

報:cannot fallthrough in type switch

  1. switch 中直接拿到具體類型:

    var i interface{} = 1
    	switch v := i.(type) {
    	case int:
    		fmt.Printf("%T", v) // int
    	}

接口組合

// ReadWriter接口將基本的Read和Write方法組合起來
type ReadWriter interface{ 
	Reader 
	Writer 
}

這個接口組合了Reader和Writer兩個接口,它徹底等同於以下寫法:

type ReadWriter interface{ 
	Read(p []byte) (n int, err error) // Reader接口的方法
	Write(p []byte) (n int, err error) // Writer接口的方法
}

Any類型

因爲Go語言中任何對象實例都知足空接口interface{},因此interface{}看起來像是能夠指向任何對象的Any類型,以下:

varv1 interface{} = 1 // 將int類型賦值給interface{} 
varv2 interface{} = "abc" // 將string類型賦值給interface{} 
varv3 interface{} = &v2 // 將 *interface{}類型賦值給interface{} 
varv4 interface{} = struct{ X int}{1} // 聲明加初始化,匿名類?
varv5 interface{} = &struct{ X int}{1}

當函數能夠接受任意的對象實例時,咱們會將其聲明爲interface{},最典型的例子是標準庫fmt中PrintXXX系列的函數,例如:

func Printf(fmt string, args ...interface{}) 
func Println(args ...interface{}) 
...

驗證明現類是否實現了接口

type Intf interface {
	Name() string
}

type Imp struct{}

func (i *Imp) Name() string {
	return "implement"
}

var _ Intf = &Imp{}

使用 _ 將變量丟掉,這樣就能夠在編譯時驗證Imp是否實現了Intf。

var _ Intf = &Imp{} 能夠寫在正式代碼中,也能夠寫在測試代碼中。

還能夠這樣:var _ Intf = (*Imp)(nil)

Go 語言方法接受者類型的選擇

概述

不少人(特別是新手)在寫 Go 語言代碼時常常會問一個問題,那就是一個方法的接受者類型到底應該是值類型仍是指針類型呢,Go 的 wiki 上對這點作了很好的解釋,我來翻譯一下。

什麼時候使用值類型

  • 若是接受者是一個 map,func 或者 chan,使用值類型(由於它們自己就是引用類型)。
  • 若是接受者是一個 slice,而且方法不執行 reslice 操做,也不從新分配內存給 slice,使用值類型。
  • 若是接受者是一個小的數組或者原生的值類型結構體類型(好比 time.Time 類型),並且沒有可修改的字段和指針,又或者接受者是一個簡單地基本類型像是 int 和 string,使用值類型就行了。

一個值類型的接受者能夠減小必定數量的垃圾生成,若是一個值被傳入一個值類型接受者的方法,一個棧上的拷貝會替代在堆上分配內存(但不是保證必定成功),因此在沒搞明白代碼想幹什麼以前,別由於這個緣由而選擇值類型接受者。

什麼時候使用指針類型

  • 若是方法須要修改接受者,接受者必須是指針類型。
  • 若是接受者是一個包含了 sync.Mutex 或者相似同步字段的結構體,接受者必須是指針,這樣能夠避免拷貝。
  • 若是接受者是一個大的結構體或者數組,那麼指針類型接受者更有效率。(多大算大呢?假設把接受者的全部元素做爲參數傳給方法,若是你以爲參數有點多,那麼它就是大)。
  • 今後方法中併發的調用函數和方法時,接受者能夠被修改嗎?一個值類型的接受者當方法調用時會建立一份拷貝,因此外部的修改不能做用到這個接受者上。若是修改必須被原始的接受者可見,那麼接受者必須是指針類型。
  • 若是接受者是一個結構體,數組或者 slice,它們中任意一個元素是指針類型並且可能被修改,建議使用指針類型接受者,這樣會增長程序的可讀性

當你看完這個仍是有疑慮,仍是不知道該使用哪一種接受者,那麼記住使用指針接受者。

關於接受者的命名

社區約定的接受者命名是類型的一個或兩個字母的縮寫(像 c 或者 cl 對於 Client)。不要使用泛指的名字像是 me,this 或者 self,也不要使用過分描述的名字,最後,若是你在一個地方使用了 c,那麼就不要在別的地方使用 cl。

Go 語言中的方法,接口和嵌入類型

來自 http://studygolang.com/articles/1113

概述

在 Go 語言中,若是一個結構體和一個嵌入字段同時實現了相同的接口會發生什麼呢?咱們猜一下,可能有兩個問題:

  • 編譯器會由於咱們同時有兩個接口實現而報錯嗎?
  • 若是編譯器接受這樣的定義,那麼當接口調用時編譯器要怎麼肯定該使用哪一個實現?

在寫了一些測試代碼並認真深刻的讀了一下標準以後,我發現了一些有意思的東西,並且以爲頗有必要分享出來,那麼讓咱們先從 Go 語言中的方法開始提及。

方法

Go 語言中同時有函數和方法。一個方法就是一個包含了接受者的函數,接受者能夠是命名類型或者結構體類型的一個值或者是一個指針。全部給定類型的方法屬於該類型的方法集。

下面定義一個結構體類型和該類型的一個方法:

type User struct {
  Name  string
  Email string
}

func (u User) Notify() error

首先咱們定義了一個叫作 User 的結構體類型,而後定義了一個該類型的方法叫作 Notify,該方法的接受者是一個 User 類型的值。要調用 Notify 方法咱們須要一個 User 類型的值或者指針:

// User 類型的值能夠調用接受者是值的方法
damon := User{"AriesDevil", "ariesdevil@xxoo.com"}
damon.Notify()

// User 類型的指針一樣能夠調用接受者是值的方法
alimon := &User{"A-limon", "alimon@ooxx.com"}
alimon.Notify()

在這個例子中當咱們使用指針時,Go 調整和解引用指針使得調用能夠被執行。注意,當接受者不是一個指針時,該方法操做對應接受者的值的副本(意思就是即便你使用了指針調用函數,可是函數的接受者是值類型,因此函數內部操做仍是對副本的操做,而不是指針操做 --意思是說若是在Notify()中修改了Name的值,damon和alimon的Name值是不會變的)。

咱們能夠修改 Notify 方法,讓它的接受者使用指針類型:

func (u *User) Notify() error

再來一次以前的調用(注意:當接受者是指針時,即便用值類型調用那麼函數內部也是對指針的操做):

// User 類型的值能夠調用接受者是指針的方法
damon := User{"AriesDevil", "ariesdevil@xxoo.com"}
damon.Notify()

// User 類型的指針一樣能夠調用接受者是指針的方法
alimon := &User{"A-limon", "alimon@ooxx.com"}
alimon.Notify()

若是Notify()中修改了Name的值,那麼damon和alimon的Name值都是會變的。

若是你不清楚到底何時該使用值,何時該使用指針做爲接受者,你能夠去看一下這篇介紹。這篇文章同時還包含了社區約定的接受者該如何命名。

接口

Go 語言中的接口很特別,並且提供了難以置信的一系列靈活性和抽象性。它們指定一個特定類型的值和指針表現爲特定的方式。從語言角度看,接口是一種類型,它指定一個方法集,全部方法爲接口類型就被認爲是該接口。

下面定義一個接口:

type Notifier interface {
  Notify() error
}

咱們定義了一個叫作 Notifier 的接口幷包含一個 Notify 方法。當一個接口只包含一個方法時,按照 Go 語言的約定命名該接口時添加 -er 後綴。這個約定頗有用,特別是接口和方法具備相同名字和意義的時候。 咱們能夠在接口中定義儘量多的方法,不過在 Go 語言標準庫中,你很難找到一個接口包含兩個以上的方法。

實現接口

當涉及到咱們該怎麼讓咱們的類型實現接口時,Go 語言是特別的一個。Go 語言不須要咱們顯式的實現類型的接口。若是一個接口裏的全部方法都被咱們的類型實現了,那麼咱們就說該類型實現了該接口。

讓咱們繼續以前的例子,定義一個函數來接受任意一個實現了接口 Notifier 的類型的值或者指針:

func SendNotification(notify Notifier) error {
  return notify.Notify()
}

SendNotification 函數調用 Notify 方法,這個方法被傳入函數的一個值或者指針實現。這樣一來一個函數就能夠被用來執行任意一個實現了該接口的值或者指針的指定的行爲。

用咱們的 User 類型來實現該接口而且傳入一個 User 類型的值來調用 SendNotification 方法:

func (u *User) Notify() error {
  log.Printf("User: Sending User Email To %s<%s>\n",
      u.Name,
      u.Email)
  return nil
}

func main() {
  user := User{
    Name:  "AriesDevil",
    Email: "ariesdevil@xxoo.com",
  }

  SendNotification(user)
}

// Output:
cannot use user (type User) as type Notifier in function argument:
User does not implement Notifier (Notify method has pointer receiver)

爲何編譯器不考慮咱們的值是實現該接口的類型?接口的調用規則是創建在這些方法的接受者和接口如何被調用的基礎上。下面的是語言規範裏定義的規則,這些規則用來講明是否咱們一個類型的值或者指針實現了該接口:

類型 *T 的可調用方法集包含接受者爲 *T 或 T 的全部方法集

這條規則說的是若是咱們用來調用特定接口方法的接口變量是一個指針類型,那麼方法的接受者能夠是值類型也能夠是指針類型。顯然咱們的例子不符合該規則,由於咱們傳入 SendNotification 函數的接口變量是一個值類型。

類型 T 的可調用方法集包含接受者爲 T 的全部方法

這條規則說的是若是咱們用來調用特定接口方法的接口變量是一個值類型,那麼方法的接受者必須也是值類型該方法才能夠被調用。顯然咱們的例子也不符合這條規則,由於咱們 Notify 方法的接受者是一個指針類型。

語言規範裏只有這兩條規則,我經過這兩條規則得出了符合咱們例子的規則:

類型 T 的可調用方法集不包含接受者爲 *T 的方法

咱們碰巧遇上了我推斷出的這條規則,因此編譯器會報錯。Notify 方法使用指針類型做爲接受者而咱們卻經過值類型來調用該方法。解決辦法也很簡單,咱們只須要傳入 User 值的地址到 SendNotification 函數就行了:

func main() {
  user := &User{
    Name:  "AriesDevil",
    Email: "ariesdevil@xxoo.com",
  }

  SendNotification(user)
}

// Output:
User: Sending User Email To AriesDevil<ariesdevil@xxoo.com>

嵌入類型

結構體類型能夠包含匿名或者嵌入字段。也叫作嵌入一個類型。當咱們嵌入一個類型到結構體中時,該類型的名字充當了嵌入字段的字段名。

下面定義一個新的類型而後把咱們的 User 類型嵌入進去:

type Admin struct {
  User
  Level  string
}

咱們定義了一個新類型 Admin 而後把 User 類型嵌入進去,注意這個不叫繼承而叫組合。 User 類型跟 Admin 類型沒有關係。

咱們來改變一下 main 函數,建立一個 Admin 類型的變量並把變量的地址傳入 SendNotification 函數中:

func main() {
  admin := &Admin{
    User: User{
      Name:  "AriesDevil",
      Email: "ariesdevil@xxoo.com",
    },
    Level: "master",
  }

  SendNotification(admin)
}

// Output
User: Sending User Email To AriesDevil<ariesdevil@xxoo.com>

事實證實,咱們能夠 Admin 類型的一個指針來調用 SendNotification 函數。如今 Admin 類型也經過來自嵌入的 User 類型的方法提高實現了該接口。

若是 Admin 類型包含了 User 類型的字段和方法,那麼它們在結構體中的關係是怎麼樣的呢?

當咱們嵌入一個類型,這個類型的方法就變成了外部類型的方法,可是當它被調用時,方法的接受者是內部類型(嵌入類型),而非外部類型。– Effective Go

所以嵌入類型的名字充當着字段名,同時嵌入類型做爲內部類型存在,咱們可使用下面的調用方法:

admin.User.Notify()

// Output
User: Sending User Email To AriesDevil<ariesdevil@xxoo.com>

這兒咱們經過類型名稱來訪問內部類型的字段和方法。然而,這些字段和方法也一樣被提高到了外部類型:

admin.Notify()

// Output
User: Sending User Email To AriesDevil<ariesdevil@xxoo.com>

因此經過外部類型來調用 Notify 方法,本質上是內部類型的方法。

下面是 Go 語言中內部類型方法集提高的規則:


給定一個結構體類型 S 和一個命名爲 T 的類型,方法提高像下面規定的這樣被包含在結構體方法集中:

若是 S 包含一個匿名字段 T,S 和 *S 的方法集都包含接受者爲 T 的方法提高。

這條規則說的是當咱們嵌入一個類型,嵌入類型的接受者爲值類型的方法將被提高,能夠被外部類型的值和指針調用。


對於 *S 類型的方法集包含接受者爲 *T 的方法提高

這條規則說的是當咱們嵌入一個類型,能夠被外部類型的指針調用的方法集只有嵌入類型的接受者爲指針類型的方法集,也就是說,當外部類型使用指針調用內部類型的方法時,只有接受者爲指針類型的內部類型方法集將被提高。


若是 S 包含一個匿名字段 *T,S 和 *S 的方法集都包含接受者爲 T 或者 *T 的方法提高

這條規則說的是當咱們嵌入一個類型的指針,嵌入類型的接受者爲值類型或指針類型的方法將被提高,能夠被外部類型的值或者指針調用。


這就是語言規範裏方法提高中僅有的三條規則,我根據這個推導出一條規則:

若是 S 包含一個匿名字段 T,S 的方法集不包含接受者爲 *T 的方法提高。

這條規則說的是當咱們嵌入一個類型,嵌入類型的接受者爲指針的方法將不能被外部類型的值訪問。這也是跟咱們上面陳述的接口規則一致。

回答開頭的問題

如今咱們能夠寫程序來回答開頭提出的兩個問題了,首先咱們讓 Admin 類型實現 Notifier 接口:

func (a *Admin) Notify() error {
  log.Printf("Admin: Sending Admin Email To %s<%s>\n",
      a.Name,
      a.Email)

  return nil
}

Admin 類型實現的接口顯示一條 admin 方面的信息。當咱們使用 Admin 類型的指針去調用函數 SendNotification 時,這將幫助咱們肯定究竟是哪一個接口實現被調用了。

如今建立一個 Admin 類型的值並把它的地址傳入 SendNotification 函數,來看看發生了什麼:

func main() {
  admin := &Admin{
    User: User{
      Name:  "AriesDevil",
      Email: "ariesdevil@xxoo.com",
    },
    Level: "master",
  }

  SendNotification(admin)
}

// Output
Admin: Sending Admin Email To AriesDevil<ariesdevil@xxoo.com>

預料之中,Admin 類型的接口實現被 SendNotification 函數調用。如今咱們用外部類型來調用 Notify 方法會發生什麼呢:

admin.Notify()

// Output
Admin: Sending Admin Email To AriesDevil<ariesdevil@xxoo.com>

咱們獲得了 Admin 類型的接口實現的輸出。User 類型的接口實現不被提高到外部類型了。

如今咱們有了足夠的依據來回答問題了:

  • 編譯器會由於咱們同時有兩個接口實現而報錯嗎?

不會,由於當咱們使用嵌入類型時,類型名充當了字段名。嵌入類型做爲結構體的內部類型包含了本身的字段和方法,且具備惟一的名字。因此咱們能夠有同一接口的內部實現和外部實現。

  • 若是編譯器接受這樣的定義,那麼當接口調用時編譯器要怎麼肯定該使用哪一個實現?

若是外部類型包含了符合要求的接口實現,它將會被使用。不然,經過方法提高,任何內部類型的接口實現能夠直接被外部類型使用。

總結

在 Go 語言中,方法,接口和嵌入類型一塊兒工做方式是獨一無二的。這些特性能夠幫助咱們像面向對象那樣組織結構而後達到一樣的目的,而且沒有其它複雜的東西。用本文中談到的語言特點,咱們能夠以極少的代碼來構建抽象和可伸縮性的框架。

接口的類型和值

接口被賦值時有類型和值的區別,若是一個接口被賦值爲nil,則類型和值都是nil,若是先聲明一個結構體變量,該變量的值是nil,把這個變量賦值給接口時,接口的類型不是nil,但值是nil。

下面先聲明一個接口和類型:

type intf interface {
	String() string
}

type A struct {
	Name string
}

func (a *A) String() string {
	// 每次都判斷一下a是否是nil比較好
	//	if a == nil {
	//		return ""
	//	}
	return a.Name
}

有一個打印方法:

func printIntf(i intf) {
	if i == nil {
		fmt.Println("nil")
		return
	}
	fmt.Println(i.String())
}

main()方法:

func main() {
	var a3 *A = nil
	printIntf(a3)
}

運行時報錯:

invalid memory address or nil pointer dereference

能夠看到 if i == nil 的判斷沒有起做用。這是由於此時的i的類型是有值的,但實際的值倒是nil,這是 var a3 *A = nil 形成的。

一個比較好的解決方案是在String()中判斷。

可是若是想在printIntf()中攔截i是nil的狀況怎麼辦?

如今能想到的辦法是利用反射:

func printIntf(i intf) {
	// 直接判斷i是否是nil適用於printIntf(nil)這種狀況
	if i == nil {
		fmt.Println("nil")
		return
	}

	// 能夠判斷main()中的狀況
	v := reflect.ValueOf(i)
	if v.IsNil() {
		fmt.Println("nil - 2")
		return
	}

	fmt.Println(i.String())
}

反射有可能影響效率,因此仍是儘可能少用。遇到這種狀況仍是從新審視一下本身的設計。

方法接收者是值/引用的一個小例子

package main

import "fmt"


func main() {
	a1 := A{"1"}
	a1.F1("11")
	fmt.Println(a1) // {1}
	a1.F2("12")
	fmt.Println(a1) // {12}, (&a1).F2("12")

	a2 := &A{"2"}
	a2.F1("21")
	fmt.Println(a2) // &{2}, (*a2).F1("21"), 修改的是值
	a2.F2("22")
	fmt.Println(a2) // &{22}

	//var _ I = A{} // error,沒有實現F2
	var _ I = (*A)(nil)
}

type A struct {
	name string
}

func (a A) F1(s string) {
	a.name = s
}

func (a *A) F2(s string) {
	a.name = s
}

type I interface {
	F1(string)
	F2(string)
}

能夠這樣理解:

  • 不涉及接口的狀況下:值只有接收者是值類型的方法,同理指針只有接收者是指針的方法,可是Go會自動轉換類型。
  • 涉及接口的狀況下:值變量只擁有接收者是值類型的方法,指針都擁有。
相關文章
相關標籤/搜索