Go初出茅庐(二)

接口(interface)

接口是一种类型。接口定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。

为什么要引入接口:

  • 比如三角形、四边形、圆形都能计算周长和面积,如何把它们当成“图形”来处理?
  • 比如学生、老师都会吃饭睡觉学习,如何把他们当成“人”来处理?

Go语言为了解决类似上面的问题,就设计了接口的概念。接口区别于之前学到所有的具体的类型,接口是一种抽象的类型。当看到一个接口类型的值时,唯一知道的是通过它的方法能做什么。

接口定义

Go语言提倡面向接口编程。每个接口由数个方法组成,接口的定义为:

type 接口类型名 interface{
    方法名1(参数列表1) 返回值列表1
    方法名2(参数列表2) 返回值列表2
}

其中:

  • 接口名:使用type关键字将接口定义为自定义的类型名,Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有字符串功能的接口叫Stringer等。接口名最好要能突出该接口的类型含义;
  • 方法名:当方法名首字母是大写且接口类型名首字母是大写时,该方法可以被接口所在包之外的代码访问;
  • 参数列表、返回值列表:列表中的变量名可以省略。

接口实例

一个对象只要实现全部实现了接口中的方法,即实现了这个接口。换句话说,接口就是一个需要实现的方法列表

接口类型变量能够存储所有实现了该接口中的方法的实例。像下面的例子中,persondogcat对象都实现了speaker接口中的方法speak(),因此speaker接口类型定义的变量能存储persondog, cat结构体对象。

这里和Java中的接口相比较:

  • 不同之处在于:Java中的接口定义的方法可以有自己的实现(即函数体),而Go接口中的方法不包含具体的实现;
  • 相同之处在于:可以通过接口类型的变量来存储引用实现了接口中方法的对象,这类似于Java中的多态。

栗子:

package main

import "fmt"

type speaker interface {
	speak()
}

type person struct{}

type dog struct{}

type cat struct{}

func (p person) speak() {
	fmt.Println("嘤嘤嘤~")
}

func (d dog) speak() {
	fmt.Println("汪汪汪~")
}

func (c cat) speak() {
	fmt.Println("喵喵喵~")
}

func _genericFunc(s speaker) {
	s.speak()
}

func main() {
	var (
		p person
		d dog
		c cat
	)
	_genericFunc(p)
	_genericFunc(d)
	_genericFunc(c)
}

代码输出:

image-20201006201559139

值接收者和指针接收者实现接口

值接收者实现接口

使用值接收者实现接口之后,不管是dog结构体还是结构体指针*dog类型的变量都可以赋值给该接口变量。Go语言中有对指针类型变量求值的语法糖,dog指针fugui内部会自动求值*fugui

package main

import "fmt"

// Mover 实现了包含移动动作的接口
type Mover interface {
	move()
}

type dog struct{}

// 使用值接收者实现接口
func (d dog) move() {
	fmt.Println("狗会动!")
}

func main() {
	var x Mover
	var wangcai = dog{} // wangcai是dog类型结构体变量
	x = wangcai         // x可以接受dog类型
	x.move()
	var fugui = &dog{} // fugui是*dog类型
	x = fugui          // x可以接受*dog类型
	x.move()
}

代码输出:

image-20201006211218887

指针接收者实现接口

相同的代码:

package main

import "fmt"

// Mover 实现了包含移动动作的接口
type Mover interface {
	move()
}

type dog struct{}

// 使用指针接收者实现接口
func (d *dog) move() {
	fmt.Println("狗会动!")
}

func main() {
	var x Mover
	// var wangcai = dog{} // wangcai是dog类型结构体变量
	// x = wangcai         // x不可以接受dog类型
	// x.move()
	var fugui = &dog{} // fugui是*dog类型
	x = fugui          // x可以接受*dog类型
	x.move()
}

此时实现Mover接口的是*dog类型,所以不能给x传入dog类型的变量,只能存储*dog类型的变量。

接口值

接口值由两部分组成,一个具体的类型和该类型的值,其称为接口的动态类型和动态值。

var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil

在Go语言中,变量总是被一个明确的值初始化,即使接口类型也不例外,对于一个接口的零值就是它的类型和值部分都是nil。第一条语句定义了变量w

image-20201007095502160

可通过使用w==nil或者w!=nil来判断接口值是否为空,调用一个空接口值上的任意方法都会产生panic

w.Write([]byte("hello")) // panic: nil pointer dereference

第二个语句将一个*os.File类型的值赋给变量w,这个赋值过程调用了一个具体类型到接口类型的隐式转换,这和显式的使用io.Writer(os.Stdout)是等价的。这类转换不管是显式的还是隐式的,都会刻画出操作到的类型和值。这个接口值的动态类型被设为*os.File指针的类型描述符,它的动态值持有os.Stdout的拷贝;这是一个代表处理标准输出的os.File类型变量的指针。

image-20201007100512922

调用一个包含*os.File类型指针的接口值的Write方法,使得(*os.File).Write方法被调用。这个调用输出“hello”。

w.Write([]byte("hello")) // "hello"

后面的语句效果类似,可参考Go语言圣经

接口值可以使用 == 和!=来进行比较。两个接口值相等仅当它们都是nil值,或者它们的动态类型相同并且动态值也根据这个动态类型的==操作相等。因为接口值是可比较的,所以它们可以用在map的键或者作为switch语句的操作数。

然而,如果两个接口值的动态类型相同,但是这个动态类型是不可比较的(比如切片),将它们进行比较就会失败并且panic。

接口与类型的关系

一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。例如,狗可以叫,也可以动。我们就分别定义Sayer接口和Mover接口。

// Sayer 接口
type Sayer interface {
	say()
}

// Mover 接口
type Mover interface {
	move()
}

dog既可以实现Sayer接口,也可以实现Mover接口。

type dog struct {
	name string
}

// 实现Sayer接口
func (d dog) say() {
	fmt.Printf("%s会叫汪汪汪\n", d.name)
}

// 实现Mover接口
func (d dog) move() {
	fmt.Printf("%s会动\n", d.name)
}

func main() {
	var x Sayer
	var y Mover

	var a = dog{name: "旺财"}
	x = a
	y = a
	x.say()
	y.move()
}

多个类型可以实现同一个接口。

接口嵌套

接口还可以嵌套,从而构成一个新的接口类型,效果如下面的例子所示:

package main

import "fmt"

// Sayer 接口
type Sayer interface {
	say(string)
}

// Mover 接口
type Mover interface {
	move(string)
}

// 接口嵌套
type animal interface {
	Sayer
	Mover
}

type cat struct {
	name string
}

func (c *cat) say(name string) {
	fmt.Printf("%s:喵喵喵~\n", name)
}

func (c *cat) move(name string) {
	fmt.Printf("%s:我可是跑的比你快多了哟~\n", name)
}

func main() {
	c1 := &cat{
		name: "招财猫",
	}
	var x Sayer = c1
	x.say(c1.name)

	c2 := &cat{
		name: "汤姆猫",
	}
	var y Mover = c2
	y.move(c2.name)

	c3 := &cat{
		name: "加菲猫",
	}
	var z animal = c3
	z.move(c3.name)
	z.say(c3.name)
}

运行结果:

image-20201007112111788

空接口

空接口是指没有定义任何方法的接口,因此任何类型都实现了空接口。因此,空接口类型的变量可以存储任意类型的变量。

package main

import "fmt"

func main() {
	// 定义空接口类型
	// 可直接将空接口类型简写成interface{}
	// type x interface {
	// }
	// 定义空接口类型变量
	var x interface{}
	s := "Hello World"
	x = s
	fmt.Printf("type:%T, value:%v\n", x, x)
	i := 100
	x = i
	fmt.Printf("type:%T, value:%v\n", x, x)
	b := true
	x = b
	fmt.Printf("type:%T, value:%v\n", x, x)
}

运行结果为:

image-20201007162308663

空接口主要可以用来:作为函数的参数;作为map的值。

空接口作为函数的参数

使用空接口实现可以接受任意类型的函数参数。

func show(a interface{}){
    fmt.Printf("Type:%T Value:%v\n", a, a)
}
空接口作为map的值

使用空接口可以保存任意值的字典。

var studentInfo = make(map[string]interface{})
studentInfo["name"] = "Tom"
studentInfo["age"] = 23
studentInfo["married"] = false
fmt.Println(studentInfo)

类型断言

要想知道接口所存储的变量的类型,可以使用类型断言,其语法格式为x.(T),这里的x是一个空接口类型的变量,T表示一个类型。一个类型断言检查它操作对象的动态类型是否和断言的类型相匹配。

在下面的例子中,该语法返回两个参数,第一个参数是x转化为T类型后的变量,第二个值是一个布尔值,若为true则表示断言成功,为false则表示断言失败。在这种情况下,不使用ok的话,断言失败的话会触发panic,而这里只是将false写入到ok,且vT类型的零值。

func main() {
	var x interface{}
	x = "Hello World"
	v, ok := x.(string)
	if ok {
		fmt.Println(v)
	} else {
		fmt.Println("类型断言失败")
	}
}

上面的示例中如果要断言多次就需要写多个if判断,这个时候我们可以使用switch语句来实现。

func justifyType(x interface{}) {
	switch v := x.(type) {
	case string:
		fmt.Printf("x is a string,value is %v\n", v)
	case int:
		fmt.Printf("x is a int is %v\n", v)
	case bool:
		fmt.Printf("x is a bool is %v\n", v)
	default:
		fmt.Println("unsupport type!")
	}
}

可参考这篇文章

并发

在Go语言中,每一个并发的执行单元叫做goroutine。目前可以简单地把goroutine类比成一个线程,这样就可以先写出一些正确的程序了,goroutine和线程的本质区别之后研究。

当一个程序启动时,其主函数即在一个单独的goroutine中运行,称为main goroutine。新的goroutine会用go语句创建,go语句是一个普通的函数或方法调用前加上关键字gogo语句会使其语句中的函数在一个新创建的goroutine中运行,而go语句本身会迅速地完成。

f() // call f(); wait for it to return
go f() // create a new goroutine that calls f(); don't wait

小tips: golang ide里面的几个快捷键,

  • alt+鼠标左键可同时选中多行进行编辑;
  • ctrl+D可以复制当前行;ctrl+x可以删除当前行;
  • shift+enter可以快速到下一行进行编辑,即使光标在当前行正中间;
  • alt+shift+up/down可以将光标所在行的代码上下移动

第一个goroutine的例子如下代码所示:

package main

import (
	"fmt"
	"time"
)

func main() {
	go spinner(100 * time.Millisecond)
	const n = 45
	fibN := fib(45)
	fmt.Printf("\rFibonacci(%d) = %d\n", n, fibN)
}

// 递归的方式计算斐波那契数列
func fib(x int) int {
	if x < 2 {
		return x
	}
	return fib(x-1) + fib(x-2)
}

// time包中对Duration的定义
//type Duration int64
//const (
//	Nanosecond  Duration = 1
//	Microsecond          = 1000 * Nanosecond
//	Millisecond          = 1000 * Microsecond
//	Second               = 1000 * Millisecond
//	Minute               = 60 * Second
//	Hour                 = 60 * Minute
//)

func spinner(delay time.Duration) {
	for {
		for _, r := range `-\|/` {
			fmt.Printf("\r%c", r)
			time.Sleep(delay)
		}
	}
}

在这个例子中,动画显示了几秒之后,fib(45)的调用成功返回,并打印结果。然后主函数返回,主函数返回之后,所有的goroutine都会被直接打断,程序退出。除了从主函数退出或者直接终止程序之外,没有其他的编程方法能够让一个goroutine来打断另一个的执行,但是之后可以看到一种方式实现这个目的,通过goroutine之间的通信来让一个goroutine请求其他的goroutine,并让被请求的goroutine自动结束执行。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值