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会自动转换类型。
  • 涉及接口的状况下:值变量只拥有接收者是值类型的方法,指针都拥有。
相关文章
相关标签/搜索