备注:本章是对《Go 程序设计语言》第 7 章的一些笔记,仅供参考

定义

类型断言(type assertion)是一个作用在 接口值 上的操作,写出来的形式为:

x.(T)
x 是一个接口类型的表达式
T 是一个类型(断言类型)

类型断言会检查作为操作数的动态类型是否满足指定的断言类型

形式

此处有两种可能:

  • 断言类型 T 是一个具体类型

    此时类型断言会检查 x 的动态类型是否就是 T。如果检查成功,类型断言的结果就是 x 的动态值,类型当然就是 T。如果检查失败,操作崩溃。即从操作数中把具体类型的值提取出来的操作

    如:

    1
    2
    3
    4
    
    var w io.Writer        // 接口类型
    w = os.Stdout
    f := w.(*os.File)      // 成功,f == os.Stdout
    c := w.(*bytes.Buffer) // 崩溃:接口持有的是 *os.File,不是 *bytes.Buffer
    
  • 断言类型 T 是一个接口类型

    此时类型断言会检查 x 的动态类型是否满足 T。如果检查成功,动态值并没有提取出来,结果仍然是一个接口值,接口值的类型和值部分也没有变更,只是结果的类型为接口类型 T。

    换句话说,类型断言是一个接口值表达式,从一个接口类型变为拥有另外一套方法的接口类型(通常是方法数量增多),但保留了接口值中的动态类型和动态值部分。

    如:

    1
    2
    3
    4
    5
    
    var w io.Writer
    w = os.Stdout
    rw := w.(io.ReadWriter)  // 成功
    w = new(ByteCounter)
    rw = w.(io.ReadWriter)  // 崩溃,*ByteCounter 没有 Read 方法
    

应用

一般来说,我们将类型断言应用在需要两个结果的赋值表达式中,那么类型断言不会在失败时崩溃,而是会多返回一个布尔型的返回值来指示断言是否成功。

1
2
3
var w io.Writer = os.Stdout
f, ok := w.(*os.File)      // 成功: ok, f == os.stdout
b, ok := w.(*bytes.Buffer) // 失败: !ok, b == nil

按照惯例,一般会把第二个返回值赋给一个名为 ok 的变量。如果操作失败,okfalse

经常可以写出如下代码:

1
2
3
4
5
6
7
if f, ok := w.(*os.File); ok {
	// use f ...
}
        
if w, ok := w.(*os.File); ok {
	// 返回值的名字与操作数变量名一致,原有的值被新的返回值掩盖
}

1. 使用类型断言来识别错误

如以下这个例子所展示的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// IsNotExist 函数判断错误是否等于 syscall.ENOENT,
// 或者等于另一个错误 os.ErrNotExist,或 *PathError

import (
        "syscall"
)

var ErrNotExist = error.New("file does not exist")

// PathError 是一个具体的类型,具体定义为:
// type PathError struct {
//    Op  string
//    Path string
//    Err  error
// }

// error 是一个 interface 类型,其接口满足:
// type error interface {
//     Error() string
// }

func IsNotExist(err error) bool {
        if pe, ok := err.(*PathError); ok {
                err = pe.Err
        }
        return err == syscall.ENOENT || err == ErrNotExist
}

2. 通过接口类型来查询特性

如果接口符合某种特性(接口),就执行某段逻辑。

3. 类型分支

接口有两种不同的风格:

  • 接口上的各种方法突出了满足这个接口的具体类型之间的相似性,但隐藏了各个具体类型的布局和各自特有的功能。这种风格强调方法,而不是具体类型。

    io.Readerio.Writerfmt.Stringersort.Interface 等。

  • 第二种风格则充分利用了接口值能够容纳多种具体类型的能力,它把接口作为这些类型的联合(union)来使用。类型断言用来在运行时区分这些类型并分别处理。

    如下所示:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    func sqlQuote(x interface{}) string {
            if x == nil {
                    return "NULL"
            } else if _, ok := x.(int); ok {
                    return fmt.Sprintf("%d", x)
            } else if _, ok := x.(uint); ok {
                    return fmt.Sprintf("%d", x)
            } else if b, ok := x.(bool); ok {
                    if b {
                            return "TRUE"
                    }
                    return "FALSE"
            } else if s, ok := x.(string); ok {
                    return sqlQuoteString(s) // (not shown)
            } else {
                    panic(fmt.Sprintf("unexpected type %T: %v", x, x))
            }
    }
    

    可将其用 switch-case 的方式改写成:

    1
    2
    3
    4
    5
    6
    7
    8
    
    // 将每个分支中提取出来的原始值绑定到一个新的变量
    switch x := x.(type) {
    case nil: // ...
    case int, uint: // ...
    case bool: // ...
    case string: // ...
    default: // ...
    }
    

参考资料