go生僻语法学习二(接口与断言)

前言

在应用go语言开发中,有一些概念其实比较少用到,这里单独提出来方便温习学习,这里强烈推荐以下书书籍

资料来源:Go语言圣经(中文版)

go 接口

在Go语言中还存在着另外一种类型:接口类型。接口类型是一种抽象的类型。它不会暴露出它所代表的对象的内部值的结构和这个对象支持的基础操作的集合(集合含义:相同的方法实现,不同的输入类型);它们只会表现出它们自己的方法

fmt.Printf,它会把结果写到标准输出,和fmt.Sprintf,它会把结果以字符串的形式返回。得益于使用接口,我们不必复制一份。fmt.Fprintf这个函数对它的计算结果会被怎么使用是完全不知道的。

package fmt

func Fprintf(w io.Writer, format string, args ...interface{}) (int, error)
func Printf(format string, args ...interface{}) (int, error) {
    return Fprintf(os.Stdout, format, args...)
}
func Sprintf(format string, args ...interface{}) string {
    var buf bytes.Buffer
    Fprintf(&buf, format, args...)
    return buf.String()
}

o.Writer类型是用得最广泛的接口之一,因为它提供了所有类型的写入bytes的抽象,包括文件类型,内存缓冲区,网络链接,HTTP客户端,压缩工具,哈希等等。io包中定义了很多其它有用的接口类型。Reader可以代表任意可以读取bytes的类型,Closer可以是任意可以关闭的值,例如一个文件或是网络链接。(到现在你可能注意到了很多Go语言中单方法接口的命名习惯)

囊扩方法

package io
type Reader interface {
    Read(p []byte) (n int, err error)
}
type Closer interface {
    Close() error
}

接口可嵌套接口,如下:

囊括变量

注意,这意味着总接口需要实现这里囊括变量中的所有子方法

type ReadWriter interface {
    Reader
    Writer
}
type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

上面用到的语法和结构内嵌相似,我们可以用这种方式以一个简写命名一个接口,而不用声明它所有的方法。这种方式称为接口内嵌。尽管略失简洁,我们可以像下面这样,不使用内嵌来声明io.ReadWriter接口

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

或者甚至使用一种混合的风格:

type ReadWriter interface {
    Read(p []byte) (n int, err error)
    Writer
}

go 接口实现

一个类型如果拥有一个接口需要的所有方法,那么这个类型就实现了这个接口。例如,*os.File类型实现了io.Reader,Writer,Closer,和ReadWriter接口。*bytes.Buffer实现了Reader,Writer,和ReadWriter这些接口,但是它没有实现Closer接口因为它不具有Close方法。Go的程序员经常会简要的把一个具体的类型描述成一个特定的接口类型。举个例子,*bytes.Buffer是io.Writer;*os.Files是io.ReadWriter。

接口指定的规则非常简单:表达一个类型属于某个接口只要这个类型实现这个接口。所以都可以直接赋值到该类型上,并作为该类型使用

var w io.Writer
w = os.Stdout           // OK: *os.File has Write method
w = new(bytes.Buffer)   // OK: *bytes.Buffer has Write method
w = time.Second         // compile error: time.Duration lacks Write method

var rwc io.ReadWriteCloser
rwc = os.Stdout         // OK: *os.File has Read, Write, Close methods
rwc = new(bytes.Buffer) // compile error: *bytes.Buffer lacks Close method

这个规则甚至适用于等式右边本身也是一个接口类型,特定已实现相同方法的接口类型也能互相赋值在方法都满足时

w = rwc                 // OK: io.ReadWriteCloser has Write method
rwc = w                 // compile error: io.Writer lacks Close method

那么关于interface{}类型,它没有任何方法,为空接口

这看上去好像没有用,但实际上interface{}被称为空接口类型是不可或缺的。因为空接口类型对实现它的类型没有要求,所以我们可以将任意一个值赋给空接口类型

var any interface{}
any = true
any = 12.34
any = "hello"
any = map[string]int{"one": 1}
any = new(bytes.Buffer)

总结小例子

package main

import "fmt"

type Packet interface {
	// 如果要表示具体内容,可以写get set方法,方便后续实现,更加规范
	getMoney() int64
	setMoney(int64)
}

type RealPacket struct {
	money int64
}

type Savings interface {
	// float64 realPacket 都会报错,不能放具体类型,只能放interface 或者函数
	// 方法可以正常构建
	saveMoney(int64)
	takeMoney(int64) int64
	// interface 虚类型可正常放置
	Packet
}

// 接口实现
type Bank struct {
	packet Packet // 结构体类型中可以放置接口
}
// 必须两个方法都进行实现
func (a Bank)saveMoney(money int64) {
	a.packet.setMoney(a.packet.getMoney() + money) // 实体结构中可以直接使用接口
	fmt.Printf("save money to %d",a.packet.getMoney())
}
func (a Bank)takeMoney(money int64) int64{
	if a.packet.getMoney() >= money {
		a.packet.setMoney(a.packet.getMoney() - money) // 实体结构中可以直接使用接口
		fmt.Printf("take money to %d",a.packet.getMoney())
		return money
	}
	cash := a.packet.getMoney()
	a.packet.setMoney(0)
	fmt.Printf("take money to %d",0)
	return cash
}
func (a *RealPacket)getMoney() int64{ // 想要复写,必须方法对应上
	return a.money
}
func (a *RealPacket)setMoney(money int64) { // 必须使用指针,否则是直接拷贝,未操作至同一个结构
	a.money = money
}

// 接口实现2
type Fund struct {
	money int64
}
// 必须两个方法都进行实现
func (a Fund)saveMoney(money int64) {
	a.money += money
	fmt.Printf("fund save money to %d",a.money)
}
func (a Fund)takeMoney(money int64) int64{
	if a.money >= money {
		a.money -= money // 实体结构中可以直接使用接口
		fmt.Printf("fund take money to %d",a.money)
		return money
	}
	cash := a.money
	a.money = 0
	fmt.Printf("fund take money to %d",0)
	return cash
}
func (a Fund)getMoney() int64{
	return a.money
}
func (a Fund)setMoney(money int64) {
	a.money = money
}

func main() {

	var bank Bank
	//bank.saveMoney(100) // 会直接panic,因为此时没实现
	var realPacket RealPacket
	bank.packet = &realPacket
	bank.saveMoney(200) // 此时可用

	var agency Savings
	//agency = bank // 会循环判断里面是否所有子接口方法是否已经实现,这里会失败,自己包含的接口,是不会外显的
	var fund Fund
	agency = fund
	agency.saveMoney(100) // 已实现该接口全部内容,此时可用
}

接口值

概念上讲一个接口的值**,接口值,由两个部分组成,一个具体的类型和那个类型的值**。它们被称为接口的动态类型和动态值对于像Go语言这种静态类型的语言,类型是编译期的概念;因此一个类型不是一个值。在我们的概念模型中,一些提供每个类型信息的值被称为类型描述符,比如类型的名称和方法**。在一个接口值中,类型部分代表与之相关类型的描述符。**

下面4个语句中,变量w得到了3个不同的值。(开始和最后的值是相同的)

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

让我们进一步观察在每一个语句后的w变量的值和动态行为。第一个语句定义了变量w:

var w io.Writer

在Go语言中,变量总是被一个定义明确的值初始化,即使接口类型也不例外。**对于一个接口的零值就是它的类型和值的部分都是nil(**图7.1)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xBGevOMC-1644326872485)(/upload/2022/02/image-2901f596d4af4fe297f94cf342cb25e8.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6CwnR8PB-1644326872487)(_image/ch7-01.png)]

一个接口值基于它的动态类型被描述为空或非空,所以这是一个空的接口值可以通过使用w==nil或者w!=nil来判断接口值是否为空。调用一个空接口值上的任意方法都会产生panic:

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

第二个语句将一个*os.File类型的值赋给变量w:

w = os.Stdout

这个赋值过程调用了一个具体类型到接口类型的隐式转换,这和显式的使用io.Writer(os.Stdout)是等价的。这类转换不管是显式的还是隐式的,**都会刻画出操作到的类型和值。这个接口值的动态类型被设为*os.File指针的类型描述符,**它的动态值持有os.Stdout的拷贝;这是一个代表处理标准输出的os.File类型变量的指针(图7.2)。也就是在赋值的时候,动态类型和动态值发生了改变
在这里插入图片描述

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

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

**通常在编译期,我们不知道接口值的动态类型是什么,所以一个接口上的调用必须使用动态分配。**因为不是直接进行调用,所以编译器必须把代码生成在类型描述符的方法Write上,然后间接调用那个地址。这个调用的接收者是一个接口动态值的拷贝,os.Stdout。效果和下面这个直接调用一样:

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

第三个语句给接口值赋了一个*bytes.Buffer类型的值

w = new(bytes.Buffer)

现在动态类型是*bytes.Buffer并且动态值是一个指向新分配的缓冲区的指针。

Write方法的调用也使用了和之前一样的机制:

w.Write([]byte("hello")) // writes "hello" to the bytes.Buffers

这次类型描述符是*bytes.Buffer,所以调用了(*bytes.Buffer).Write方法,并且接收者是该缓冲区的地址。这个调用把字符串“hello”添加到缓冲区中。

最后,第四个语句将nil赋给了接口值:

w = nil

个重置将它所有的部分都设为nil值,把变量w恢复到和它之前定义时相同的状态,也就是动态类型和动态值都变为了nil,在图7.1中可以看到。

一个接口值可以持有任意大的动态值。例如,表示时间实例的time.Time类型,这个类型有几个对外不公开的字段。我们从它上面创建一个接口值:

var x interface{} = time.Now()

结果可能和图7.4相似**。从概念上讲,不论接口值多大,动态值总是可以容下它**。(这只是一个概念上的模型;具体的实现可能会非常不同)
在这里插入图片描述

接口值可以使用和!=来进行比较。两个接口值相等仅当它们都是nil值,即动态类型和动态值都为nil,或者它们的**动态类型相同并且动态值也根据这个动态类型的操作相等**,这时候才能确认相等。因为接口值是可比较的,所以它们可以用在map的键或者作为switch语句的操作数,这时候就看类型是否可以比较(比如切片),不能比较就panic

var x interface{} = []int{1, 2, 3}
fmt.Println(x == x) // panic: comparing uncomparable type []int

考虑到这点,接口类型是能否比较时变化的,因此要加条件判断后才能比较其它类型要么是安全的可比较类型(如基本类型和指针)要么是完全不可比较的类型(如切片,映射类型,和函数),但是在比较接口值或者包含了接口值的聚合类型时,我们必须要意识到潜在的panic。同样的风险也存在于使用接口作为map的键或者switch的操作数。只能比较你非常确定它们的动态值是可比较类型的接口值。

当我们处理错误或者调试的过程中,得知接口值的动态类型是非常有帮助的。所以我们使用fmt包的%T动作:

var w io.Writer
fmt.Printf("%T\n", w) // "<nil>"
w = os.Stdout
fmt.Printf("%T\n", w) // "*os.File"
w = new(bytes.Buffer)
fmt.Printf("%T\n", w) // "*bytes.Buffer"

在fmt包内部,使用反射来获取接口动态类型的名称。

警告:一个包含nil指针的接口不是nil接口

一个不包含任何值的nil接口值和一个刚好包含nil指针的接口值是不同的。这个细微区别产生了一个容易绊倒每个Go程序员的陷阱。

思考下面的程序。当debug变量设置为true时,main函数会将f函数的输出收集到一个bytes.Buffer类型中。

const debug = true

func main() {
    var buf *bytes.Buffer
    if debug {
        buf = new(bytes.Buffer) // enable collection of output
    }
    f(buf) // NOTE: subtly incorrect!
    if debug {
        // ...use buf...
    }
}

// If out is non-nil, output will be written to it.
func f(out io.Writer) {
    // ...do something...
    if out != nil {
        out.Write([]byte("done!\n"))
    }
}

我们可能会预计当把变量debug设置为false时可以禁止对输出的收集,但是实际上在out.Write方法调用时程序发生了panic:

if out != nil {
    out.Write([]byte("done!\n")) // panic: nil pointer dereference
}

**发生了空指针调用,所以panic,**当main函数调用函数f时,它给f函数的out参数赋了一个bytes.Buffer的空指针,所以out的动态值是nil。然而,它的动态类型是bytes.Buffer,意思就是out变量是一个包含空指针值的非空接口,所以防御性检查out!=nil的结果依然是true。

也就是说,这个判断接口是否非空,而不是值是否非空,导致错误

动态分配机制依然决定(bytes.Buffer).Write的方法会被调用,但是这次的接收者的值是nil。对于一些如os.File的类型,nil是一个有效的接收者(§6.2.1),但是*bytes.Buffer类型不在这些种类中。这个方法会被调用,但是当它尝试去获取缓冲区时会发生panic。

问题在于尽管一个nil的*bytes.Buffer指针有实现这个接口的方法,它也不满足这个接口具体的行为上的要求。特别是这个调用违反了(*bytes.Buffer).Write方法的接收者非空的隐含先觉条件,所以将nil指针赋给这个接口是错误的。解决方案就是将main函数中的变量buf的类型改为io.Writer,因此可以避免一开始就将一个不完整的值赋值给这个接口:

var buf io.Writer
if debug {
    buf = new(bytes.Buffer) // enable collection of output
}
f(buf) // OK

类型断言

类型断言是一个使用在接口值上的操作。语法上它看起来像x.(T)被称为断言类型,这里x表示一个接口的类型和T表示一个类型。一个类型断言检查它操作对象的动态类型是否和断言的类型匹配

这里有两种可能。

第一种,T是一个具体类型情况下,然后类型断言检查x的动态类型是否和T相同。**如果这个检查成功了,类型断言的结果是x的动态值,当然它的类型是T。**换句话说,具体类型的类型断言从它的操作对象中获得具体的值。**如果检查失败,接下来这个操作会抛出panic。**例如:

var w io.Writer
w = os.Stdout
f := w.(*os.File)      // success: f == os.Stdout
c := w.(*bytes.Buffer) // panic: interface holds *os.File, not *bytes.Buffer

第二种,断言的类型T是一个接口类型,然后类型断言检查是否x的动态类型满足T。如果这个检查成功了,动态值不影响;这个结果仍然是一个有相同动态类型和值部分的接口值,但是结果为类型T。换句话说,对一个接口类型的类型断言改变了类型的表述方式,改变了可以获取的方法集合(通常更大),但是它保留了接口值内部的动态类型和值的部分。

在下面的第一个类型断言后,w和rw都持有os.Stdout,因此它们都有一个动态类型*os.File,但是变量w是一个io.Writer类型,只对外公开了文件的Write方法,而rw变量还公开了它的Read方法。

var w io.Writer
w = os.Stdout
rw := w.(io.ReadWriter) // success: *os.File has both Read and Write
w = new(ByteCounter)
rw = w.(io.ReadWriter) // panic: *ByteCounter has no Read method

如果断言操作的对象是一个nil接口值,那么不论被断言的类型是什么这个类型断言都会失败。

w = rw             // io.ReadWriter is assignable to io.Writer
w = rw.(io.Writer) // fails only if rw == nil

如果直接panic,这样对于编程来说是不友好的

经常地,对一个接口值的动态类型我们是不确定的,并且我们更愿意去检验它是否是一些特定的类型。如果类型断言出现在一个预期有两个结果的赋值操作中,例如如下的定义,这个操作不会在失败的时候发生panic,但是替代地返回一个额外的第二个结果这个结果是一个标识成功与否的布尔值

var w io.Writer = os.Stdout
f, ok := w.(*os.File)      // success:  ok, f == os.Stdout
b, ok := w.(*bytes.Buffer) // failure: !ok, b == nil

第二个结果通常赋值给一个命名为ok的变量。如果这个操作失败了,那么ok就是false值,第一个结果等于被断言类型的零值,在这个例子中就是一个nil的*bytes.Buffer类型。也就是说,断言失败会回归原先类型的零值,不为被断言类型

这个ok结果经常立即用于决定程序下面做什么。if语句的扩展格式让这个变的很简洁:

if f, ok := w.(*os.File); ok {
    // ...use f...
}

个方式是利用一个接口值可以持有各种具体类型值的能力,将这个接口认为是这些类型的联合。**类型断言用来动态地区别这些类型,使得对每一种情况都不一样。**在这个方式中,重点在于具体的类型满足这个接口,而不在于接口的方法(如果它确实有一些的话),并且没有任何的信息隐藏。我们将以这种方式使用的接口描述为discriminated unions(可辨识联合)。

解决多类型判定的方案:

最简单的形式中,一个类型分支像普通的switch语句一样,它的运算对象是x.(type)——它使用了关键词字面量type——并且每个case有一到多个类型。一个类型分支基于这个接口值的动态类型使一个多路分支有效。这个nil的case和if x == nil匹配,并且这个default的case和如果其它case都不匹配的情况匹配。一个对sqlQuote的类型分支可能会有这些case:

switch x.(type) {
case nil:       // ...
case int, uint: // ...
case bool:      // ...
case string:    // ...
default:        // ...
}

和(§1.8)中的普通switch语句一样,每一个case会被顺序的进行考虑,并且当一个匹配找到时,这个case中的内容会被执行。当一个或多个case类型是接口时,case的顺序就会变得很重要,因为可能会有两个case同时匹配的情况。default case相对其它case的位置是无所谓的。它不会允许落空发生。

注意到在原来的函数中,对于bool和string情况的逻辑需要通过类型断言访问提取的值。因为这个做法很典型,类型分支语句有一个扩展的形式,它可以将提取的值绑定到一个在每个case范围内都有效的新变量。

switch x := x.(type) { /* ... */ }

使用类型分支的扩展形式来重写会让这个函数更加的清晰:

func sqlQuote(x interface{}) string {
    switch x := x.(type) {
    case nil:
        return "NULL"
    case int, uint:
        return fmt.Sprintf("%d", x) // x has type interface{} here.
    case bool:
        if x {
            return "TRUE"
        }
        return "FALSE"
    case string:
        return sqlQuoteString(x) // (not shown)
    default:
        panic(fmt.Sprintf("unexpected type %T: %v", x, x))
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值