更多优质内容
请关注公众号

Go入门系列(十三) 接口——类型断言(下)-张柏沛IT博客

正文内容

Go入门系列(十三) 接口——类型断言(下)

栏目:Go语言 系列:Go入门系列 发布时间:2021-01-10 11:45 浏览量:3269

类型断言

类型断言可以判断某一个接口值变量是否是某种类型。语法为 x.(T)T是要断言的类型,x是接口值,这一句话的意思是判断x是否是T这种类型的变量。但是x.(T)不只返回一个bool,还会返回一个转换为T类型的x变量,因此类型断言还可以用于类型转换(断言的第一个返回值就是类型转换的结果,第二个返回值就是判断类型是否正确的布尔值)。

当这个接口值x的动态类型实现了T的所有方法(就说明x是T类型),就会断言成功,否则断言失败。

例如:

var w io.Writer	
w = os.Stdout
x, ok := w.(*os.File)		// w接口值对 *os.File类型进行断言。即,判断w是否为*os.File类型

fmt.Printf("%#v\n", w)		// &os.File{file:(*os.file)(0xc000072280)}
fmt.Printf("%#v\n%v\n", x, ok)		// &os.File{file:(*os.file)(0xc000072280)}   true


关键点:

1.断言只能用于接口值变量不能用于普通的变量,否则会报错。

例如

s := os.Stdout
x, ok := s.(*os.File) // 报错,s不是一个接口值,而是普通变量


2.我们可以用1个或者2个变量接收断言的返回结果

如果是用一个接收,那么断言失败(即w不属于*os.File类型的话)就会引发panic异常,程序终止。例如:

x := w.(*bytes.Buffer) // 断言失败,w不是 *bytes.Buffer类型,引发panic

如果是用2个接收,那么断言失败不会引发panic异常,而是会将断言结果(false)传给第二个接收值 ok ,并将一个有类型的nil返回给第一个接收值x

以上面的例子来说:

x, ok := w.(*bytes.Buffer)    // 断言失败,因为w的动态类型是 *os.File而不是*bytes.Buffer。不过返回的x是一个(*bytes.Buffer)(nil)(而不是一个单纯的nil)。


如果断言成功,第一个接收值会被返回一个断言的类型的变量,如果断言失败,会被返回一个断言类型的nil。但是无论成功还是失败,返回的都是一个普通变量而不是接口值变量。比如:

var w io.Writer

w = os.Stdout

x := w.(*os.File) // 断言会成功。返回的x*os.File类型,但不是io.Writer类型


x.Write([]byte("Hello!"))

x.Close() // 这里没有报错。这一句想证明x不是一个io.Writer类型的接口值而只是一个普通的*os.File变量。因为io.Writer屏蔽了除了Write方法外的所有方法。

_, _ = x.(*bytes.Buffer) // 报错。这一句也证明了x不是一个接口值而是普通的变量。



上面的例子中,断言的类型是一个普通的类型(*os.File) 其实,断言的类型T除了是一个普通类型之外,还可以是接口类型。请看下面的例子:

func main(){
	var w io.Writer
	w = os.Stdout
	x, ok := w.(io.ReadWriteCloser)		// 断言成功,说明w的动态类型也是ReadWriteClose类型
	fmt.Printf("%#v %T %v\n", x, x, ok)

	y, ok := w.(io.Writer)		// 断言成功,但是这里只起到了判断类型的作用没有起到转换类型的作用,因为w本身是io.Writer类型
	fmt.Printf("%#v %T %v\n", y, y, ok)
	
	x.Write([]byte("Hi!"))
	x.Close()		// 正常

	y.Write([]byte("Hi!"))
	y.Close()		// 报错
}


断言一个接口成功与否取决于这个变量的动态类型是否实现被断言的接口的方法。例如上面的 w 它的动态类型是 *os.File类型,这个类型实现了 Read()/Write()/Close()方法,所以他是属于 io.ReadWriteCloser这个类型的。并不会因为w被声明为io.Writer类型就导致它断言ReadWriteCloser失败。

断言一个接口得到的返回值 x y 是断言接口类型的类型,比如x是由ReadWriteCloser类型断言而得到的,因此x就是一个ReadWriteCloser的类型,因此x就有  Read()/Write()/Close()方法。但是y是一个Writer的类型断言得到的,所以yWriter类型,它就无法调用 Close()方法,即使它的动态类型是 *os.File类型。



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

func main(){
	var w io.Writer
	w = (*bytes.Buffer)(nil)		// 创建一个bytes.Buffer指针类型的nil(即nil指针)
	//fmt.Printf("%#v", w)		//	w 不是nil,因为w的具体类型不为nil,只是值为nil而已
	x, ok := w.(io.Writer)		// 断言成功,ok是true
	fmt.Println(x, ok)
}
func main(){
	var w io.Writer
	w = nil		// 创建一个bytes.Buffer指针类型的nil(即nil指针)
	//fmt.Printf("%#v", w)		//	w 是nil,nil相当于是接口类型的零值
	x, ok := w.(io.Writer)		// 断言失败,ok是false
	fmt.Println(x, ok)
}

上面的例子说明,对一个纯nil的接口变量断言是会失败的。但是用具体值为nil的接口变量断言不会失败。


通过类型断言访问行为

这个标题的意思是根据断言判断出一个变量的类型后就可以根据类型调用这个类型才有的方法和行为(其实就是判断这个变量有没有某个方法)。而这也是断言的一大用法。下面我们来看一个例子:

func main(){
	w := os.Stdout
	WriterHeader(w, "text/html")
}

// 假设这是一个往http响应添加响应头的函数
// w是一个响应流,向这个w响应流写入的数据会send()发送到客户端(如浏览器)
func WriterHeader(w io.Writer, contentType string) error{
	if _, err := w.Write([]byte("Content-type: " + contentType)); err != nil{
		return err
	}
	return nil
}

我们可以看到,在 WriterHeader 函数的内部,通过w.Write()方法向响应流写入header信息。


现在有一个问题:

Write需要传入一个[]byte的类型,但是由于contentType是字符串类型,因此要用 []byte()对字符串转换类型。

这个过程会直接开辟一个和contentType相同大小的内存空间,并进行数据拷贝。并且拷贝出来的[]byte的空间会很快就释放。

这样的内存分配方式会降低web服务器的效率,因此有没有办法可以避免这种拷贝?


其实 *os.File 有一个 WriteString 方法,这个方法可以直接往一个文件句柄中写入字符串,这个方法会避免去分配一个临时的拷贝。

不仅是 *os.File,像 *bytes.Buffer *bufio.Writer 这些类型都有这个方法。


可是,我们不能保证所有的io.Writer接口类型都有这个WriteString方法。


因此我们需要先判断一个变量w是否有WriteString 方法,如果有就调用这个方法,如果没有就还是调用Write方法。

这放在python很简单:

hasattr(w, WriteString)


但是go中就会比较复杂,我们看看具体的代码:

func main(){
	w := os.Stdout
	WriterHeader(w, "text/html")
}

// 假设这是一个往http响应添加响应头的函数
// w是一个响应流,向这个w响应流写入的数据会send()发送到客户端(如浏览器)
func WriterHeader(w *os.File, contentType string) error{
	if _, err := writeString(w, "Content-type: " + contentType); err != nil{
		return err
	}
	return nil
}

func writeString(w io.Writer, s string) (n int, err error){
	// 定义一个接口,这个接口用于之后判断w是否有WriteString方法
	type ws interface {
		WriteString(s string) (n int, err error)
	}

	// 通过断言类型判断w是否属于ws这个接口类型从而判断这个w是否有WriteString方法
	if w, ok := w.(ws); ok {
		fmt.Println("Use function WriteString")
		return w.WriteString(s)
	}

	// 如果w没有WriteString方法,那就只能调Write方法了,那就只能用[]byte转类型了。
	return w.Write([]byte(s))
}

这里的关键是定义了一个便捷函数 writeString ,这个函数会兼容WriteStringWrite两种方法。如果wWriteStirng则调用WriteString

而判断w是否有WriteString的技巧就是声明一个需要实现WriteString方法的临时接口类型(为什么叫临时接口呢,因为这个接口是在函数中定义的而不是在包级作用域定义的,所以函数调用完毕后这个接口类型就会释放掉)。然后对这个接口进行断言。

这里也告诉了我们接口和断言的一大用法:判断对象(这个对象必须是一个接口值)是否有某一个或某多个方法。



类似的应用还可以在fmt包的一个formatOneValue的非导出函数中看到。

这个函数的作用是:将任意类型的一个变量转为字符串并返回。

package fmt

func formatOneValue(x interface{}) string {
    if err, ok := x.(error); ok {
        return err.Error()
    }
    if str, ok := x.(Stringer); ok {
        return str.String()
    }
    // ...all other types...
}


参数是 x interface{} 空接口类型,表示formatOneValue的参数可以接收任何类型的值。

这个函数通过断言的方式判断x属于种接口类型间接判断出x是否有哪个方法,如果是这种接口类型就调用这种接口类型的格式化字符串方法(error对应Error(), Stringer对应String()方法)




如果,我希望判断一个普通变量(非接口值)是否是某个类型该怎么做呢?因为我们知道非接口值的普通变量是不能够进行断言的。

此时很简单,只需要将这个普通变量经过空接口转为一个接口值即可。例如

w := os.Stdout
x, ok := (interface{})(w).(io.Writer)
fmt.Println(x, ok)


又或者:

// 判断一个变量是否是int类型
func isInt (x interface{}) bool {
	_, ok := x.(int)
	return ok
}

func main(){
	fmt.Println(isInt(10))

	num := 100
	fmt.Println(isInt(num))
}

这个例子请注意几点:

1.isInt的形参是 x interface{} ,因此当实参传入isInt的时候,这个实参就会自动从普通变量转为接口值变量。

2.isInt(10)没报错,说明 x interface{}空接口作为形参,是允许传入字面量而不是变量。




类型分支

类型分支是通过 switch x.(type) 的语法判断x的类型是否为某个接口类型的方式。

举一个例子,下面这是一个sql逻辑

import "database/sql"

func listTracks(db sql.DB, artist string, minYear, maxYear int) {
    result, err := db.Exec(
        "SELECT * FROM tracks WHERE artist = ? AND ? <= year AND year <= ?",
        artist, minYear, maxYear)
    // ...
}

db.Exec 方法内部会将第一参以后的参数(可以是任意类型)转为string并拼接到db.Exec的第一参的sql语句中。这个方法是sqlQuote

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))
    }
}

这里使用了 switch + 断言类型的方式判断x的类型。


其中 x.(type) 这个断言语法只能够在switch中使用,不能够在其他地方使用。

switch x := x.(type) 等式左边的x是断言得到的新变量,以方便在case中使用x

switch语句隐式的创建了一个词法块,因此新变量x的定义不会和外面块中的x变量冲突。




那么说到这里,基本上所有关于接口的知识已经介绍完毕。

最后是使用接口的一些建议:

接口不要滥用,不要随便就创建一个接口。只有当有两个或两个以上的具体类型必须实现相同的方法时才去定义一个接口。例如 *os.File *bytes.Buffer *bufio.Writer都需要有一个Write方法,所以才有必要创建一个io.Writer接口来规范这三个类型。





更多内容请关注微信公众号
zbpblog微信公众号

如果您需要转载,可以点击下方按钮可以进行复制粘贴;本站博客文章为原创,请转载时注明以下信息

张柏沛IT技术博客 > Go入门系列(十三) 接口——类型断言(下)

热门推荐
推荐新闻