golang 类型断言 VS 类型转换

golang 中类型断言和类型转换两个概念很容易困惑,它们看上去提供了相同的功能(把变量从一个类型转到类型)。但是 golang 为什么会有两个功能相似的概念呢?那么在本文中,我们将了解类型断言和类型转换本质的区别,并深入了解在 go 中使用它们会发生什么?

首先让我们来看一下在 go 中如何使用它们:

类型断言

var greeting interface{} = "hello world"
greetingStr := greeting.(string)

类型转换

greeting := []byte("hello world")
greetingStr := string(greeting)

最明显的不同是它们有着不同的语法: variable.(type) vs type(variable)。

类型断言

类型断言顾名思义,它是用来断言变量具有哪些类型的。类型断言只能作用在接口上。在上面类型断言的例子中 greeting 就是一个接口(interface{})类型,我们给他分配了字符串(string)类型的变量,那么我们现在可以说greeting具有了字符串类型,虽然greeting依然是接口类型,但是我们可以通过类型断言获取字符串类型:

注意:这里我们要考虑一个问题:在使用类型断言时是否一定要提前知道接口变量的原始类型呢?如果我们不知道原始类型,或者接口变量不具备我们想目标类型会有什么结果呢?

我们来关注一下类型断言的另外一种用法:

var greeting interface{} = "42"
greetingStr, ok := greeting.(string)

这种用法,类型断言有两个返回值,第一个值ok时bool类型,当断言成功时返回true,否则返回false。如果我们采用第一中写法(只有一个返回值),在发生断言错误时会抛出panic。

注意:上面这些表明了类型断言是在程序运行时执行的。

类型断言之 type switch

当我们不确定一个接口的底层变量类型时,这时候 type switch 就是一个很好的语法结构

var greeting interface{} = 42

switch g := greeting.(type) {
  case string:
    fmt.Println("g is a string with length", len(g))
  case int:
    fmt.Println("g is an integer, whose value is", g)
  default:
    fmt.Println("I don't know what g is")
}

什么是断言

上面的例子中看上去我们是把变量greeing从interface{}类型转为了string或者int,但是实际上greeing的类型是固定的,依然是它初始化时的类型。将greeing分配给接口类型时,不会更改其基础类型。 同样的,当我们断言它的类型时,我们只是在使用整个原始类型的功能,而不是接口暴露的有限方法(函数)。

类型转换

在讲解类型转换前,首先让我们来理解一下什么是类型?golang 中类型定义了两个事情:

  • 数据结构:变量在底层以什么形式存储
  • 行为:变量具有哪些方法或者说是函数

golang 中变量类型可以有两类:基础类型和混合类型,基础类型有 string,int 等等;混合类型包含:struct,map,slice 等。

基于基础类型我们还可以声明一个新的类型:

// myInt 是一个新的变量类型,它的基础类型是 int
type myInt int

// 函数 AddOne 工作在 myInt 上,和 int 没有任何关系
func (i myInt) AddOne() myInt { return i + 1}

func main() {
    var i myInt = 4
    fmt.Println(i.AddOne())
}

我们基于基础类型int声明了一个新的类型myInt,它们底层的数据结构都是一样的,但是myInt 多了一个函数 AddOne。因为它们的底层类型是一样的,我们可以使用类型转换,把变量从一种类型转为另一种类型

var i myInt = 4
originalInt := int(i)

上面i的类型是 myint,originalInt 的类型是 int。

什么时候我们可以使用类型转换?

只有当两个类型的底层数据结构相同的时候,才可以使用类型转换,在做类型转换时不会检查类型的方法。让我们来看一个结构体的例子:

type person struct {
    name string
    age int
}

type child struct {
    name string
    age int
}

type pet {
  name string
}

func main() {
    bob := person{
	name: "bob",
	age: 15,
    }
    babyBob := child(bob)
    // "babyBob := pet(bob)" would result in a compilation error
    fmt.Println(bob, babyBob)
}

上面代码中person和child有着相同的底层数据结构:

struct {
   name string
   age int
}

它们就可以相互进行类型转换

如果像上面这种声明多个具有相同的底层数据结构的结构体类型,可以有一个简洁的写法:

type person struct {
    name string
    age int
}

type child person

为什么叫做类型转换

正如上面提到的不同的类型,在其上的限制和方法是不同的,即使它们有着相同的底层数据结构。在我们使用 golang 的类型转换时我们改变的是这个变量的行为(因为不同的类型有不同的函数),而不是仅仅暴露这个变量的原始类型(后者是类型断言做的事)。另外,如果两个类型不能相互转换,golang 会在编译时报错,不像类型断言时在运行时报错。

总结

类型转换和类型断言不仅仅是语法上的不同,通过这两个概念进一步强调了 golang 中接口类型和非接口类型(具体类型)不同。接口类型没有底层数据结构,它仅仅是暴露了预先定义在具体类型中的方法。类型断言是获取隐藏在接口类型变量之下的具体类型,类型转换是改变我们操作变量底层数据的方法。