Go中的类型转换,分配和比较规则

183 阅读11分钟
原文链接: www.jianshu.com

本文翻译自Value Conversion, Assignment and Comparison Rules in Go

类型转换规则

注意:本文中的转换定义与Go规范不完全相同。Go规范中的转换意味着显式转换。本文中的转换包括显式和隐式转换。

在Go中,如果一个值v可以被显式转换为类型T,则转换可以表示为形式(T)(v)。对于大多数情况,特别是T是类型名称(标识符)的时候,形式可以简化为T(v)

我们应该知道的一个事实是,当某个值x可以隐式转换为类型T时,那么也代表着x可以显式转换为类型T

1. 明确的转换规则

如果两种不同的类型都表示一个相同的类型,那么它们的值可以隐式转换为两种类型中的任意一种。

例如:

  • byteuint8类型的值可以相互转换
  • runeint32类型的值可以相互转换
  • []byte[]uint8类型的值可以相互转换

2. 基础类型相关的转换规则

给定一个非接口类型的值x和一个非接口类型T,假设x的类型是Tx

  • 如果TxT共享相同的基础类型(忽略结构体tags),那么x可以被显式转换为T
  • 如果TxT是非定义类型,并且它们的基础类型相同(考虑结构体tags),那么x可以隐式转为T
  • 如果TxT具有不同的基础类型,但是TxT都是非定义的指针类型,并且它们都指向相同的基础类型(忽略结构体tags),那么x可以(并且必须)显式转换为T

例如:

package main

func main() {
    // []int, IntSlice and MySlice share
    // the same underlying type: []int
    type IntSlice []int
    type MySlice  []int

    var s  = []int{}
    var is = IntSlice{}
    var ms = MySlice{}
    var x struct{n int `foo`}
    var y struct{n int `bar`}

    // The two implicit conversions both doesn't work.
    /*
    is = ms // error
    ms = is // error
    */

    // Must use explicit conversions here.
    is = IntSlice(ms)
    ms = MySlice(is)
    x = struct{n int `foo`}(y)
    y = struct{n int `bar`}(x)

    // Implicit conversions are okay here.
    s = is
    is = s
    s = ms
    ms = s
}

与指针相关的转换示例:

package main

func main() {
    type MyInt int
    type IntPtr *int
    type MyIntPtr *MyInt

    var pi = new(int)  // the type of pi is *int
    // ip and pi have the same underlying type,
    // and the type of pi is non-defined, so
    // the implicit conversion works.
    var ip IntPtr = pi

    // var _ *MyInt = pi // can't convert implicitly
    var _ = (*MyInt)(pi) // ok, must explicitly

    // Values of *int can't be converted to MyIntPtr
    // directly, but can indirectly.
    /*
    var _ MyIntPtr = pi  // can't convert implicitly
    var _ = MyIntPtr(pi) // can't convert explicitly
    */
    var _ MyIntPtr = (*MyInt)(pi)  // ok
    var _ = MyIntPtr((*MyInt)(pi)) // ok

    // Values of IntPtr can't be converted to
    // MyIntPtr directly, but can indirectly.
    /*
    var _ MyIntPtr = ip  // can't convert implicitly
    var _ = MyIntPtr(ip) // can't convert explicitly
    */
    var _ MyIntPtr = (*MyInt)((*int)(ip))  // ok
    var _ = MyIntPtr((*MyInt)((*int)(ip))) // ok
}

什么是基础类型? 在Go中,每种类型都有一个基础类型。规则:

  • 对于内置的基本类型,基础类型就是它本身
  • 对于所有的unsafe pointer类型,基础类型都是unsafe.Pointer
  • 对于一个非定义类型,也就是复合类型(比如结构体或者切片)来说,基础类型就是它本身
  • 在类型声明中,新声明的类型和源类型具有相同的基础类型

例如:

// The underlying types of the following ones are both int.
type (
  MyInt int
  Age   MyInt
)

// The following new types have different underlying types.
type (
  IntSlice   []int   // underlying type is []int
  MyIntSlice []MyInt // underlying type is []MyInt
  AgeSlice   []Age   // underlying type is []Age
)

// The underlying types of []Age, Ages, and AgeSlice
// are all the non-defined type []Age.
type Ages AgeSlice

如何跟踪给定用户声明类型的基础类型?规则是,当遇到内置基本类型,unsafe.Pointer或非定义类型时,则停止跟踪。以上面的类型声明为例,让我们跟踪它们的基础类型。
MyInt → int
Age → MyInt → int
IntSlice → []int
MyIntSlice → []MyInt → []int
AgeSlice → []Age → []MyInt[]int
Ages → AgeSlice → []Age → []MyInt[]int

在Go中:

  • 基础类型为bool的类型称为布尔类型
  • 基础类型为任意内置整数的类型称为整型
  • 基础类型为float32float64的类型称为浮点型
  • 基础类型为complex64complex128的类型称为复数类型
  • 整数,浮点数和复数类型统称为数字类型
  • 基础类型为string的类型称为字符串类型

3. 通道特定转换规则

假设Tx是双向信道类型,T也是信道类型(双向或非双向),如果TxT具有相同的元素类型,并且TxT是非定义类型,则x可以隐式转换为T

例如:

package main

func main() {
    type C chan string
    type C1 chan<- string
    type C2 <-chan string

    var ca C
    var cb chan string

    cb = ca // ok, same underlying type
    ca = cb // ok, same underlying type

    // The 4 lines compile okay for this 2nd rule.
    var _, _ chan<- string = ca, cb // ok
    var _, _ <-chan string = ca, cb // ok
    var _ C1 = cb                   // ok
    var _ C2 = cb                   // ok

    // Values of C can't be converted
    // to C1 and C2 directly.
    /*
    var _ = C1(ca) // compile error
    var _ = C2(ca) // compile error
    */

    // Values of C can be converted
    // to C1 and C2 indirectly.
    var _ = C1((chan<- string)(ca)) // ok
    var _ = C2((<-chan string)(ca)) // ok
    var _ C1 = (chan<- string)(ca)  // ok
    var _ C2 = (<-chan string)(ca)  // ok
}

4. 接口实现相关的转换规则

给定一个值x和一个接口类型I,如果x的类型(或默认类型)是Tx并且Tx实现了I,那么x可以被隐式转换为类型I。转换的结果是一个接口值(类型I),它封装了:

  • 如果Tx是非接口类型,则为x的副本
  • 如果Tx是接口类型,则为x动态值的副本

给定一个接口值x,其动态类型为Tx可以通过类型断言语法x.(T)安全地转换为T

给定一个接口值x和一个接口类型I,如果x的动态类型实现了I,则x可以通过类型断言语法x.(I)安全地转换为类型I

5. 无类型值转换规则

如果无类型值可以表示为类型T的值,那么可以将无类型值隐式转换为类型T

例如:

package main

func main() {
    var _ []int = nil
    var _ map[string]int = nil
    var _ chan string = nil
    var _ func()() = nil
    var _ *bool = nil
    var _ interface{} = nil

    var _ int = 123.0
    var _ float64 = 123
    var _ int32 = 1.23e2
    var _ int8 = 1 + 0i
}

6. 常量转换规则

(这条规则与上一条重叠)

通常,转换常量仍会产生一个常量。(除了将常量字符串转换为下面第8条规则中描述的字节切片或rune切片外)

给定一个常量值x和一个类型T,如果x可以表示为类型T的值,那么x可以被显式转换为类型T的值。特别是如果x是无类型值,则x可以隐式转换为类型T的值

例如:

package main

func main() {
    const I = 123
    const I1, I2 int8 = 0x7F, -0x80
    const I3, I4 int8 = I, 0.0

    const F = 0.123456789
    const F32 float32 = F
    const F32b float32 = I
    const F64 float64 = F
    const F64b = float64(I3) // must be explicit

    const C1, C2 complex64 = F, I
    const I5 = int(C2) // must be explicit
}

7. 非常数转换规则

非常量浮点数和整数可以被显式转换为任意浮点类型和整型

非常量复数值可以被显式转换为任意复数类型

注意:

  • 复数的非常量值无法转换为浮点和整数类型
  • 浮点数和整数不能被转换为复数类型
  • 在非常量转换中允许数据溢出和舍入。将浮点数转换为整数时,会丢弃小数(截断为零)

例如:

package main

import "fmt"

func main() {
    var a, b = 1.6, -1.6 // both are float64
    fmt.Println(int(a), int(b)) // 1 -1

    var i, j int16 = 0x7FFF, -0x8000
    fmt.Println(int8(i), uint16(j)) // -1 32768

    var c1 complex64 = 1 + 2i
    var _ = complex128(c1)
}

8. 字符串转换规则

如果值的类型(或默认类型)是整数类型,则可以将值显式转换为字符串类型

例如:

string(65)          // "A"
string(-1)          // "\uFFFD"
string(0xFFFD)      // "\uFFFD"
string(0x2FFFFFFFF) // "\uFFFD"

一个字符串值可以被显式转换为一个基础类型为[]byte的切片(a.k.a uint8),反之亦然

一个字符串值可以被显式转换为一个基础类型为[]rune的切片(a.k.a []int32),反之亦然

9. unsafe pointers 转换规则

任意类型的指针值都可以显式转换为基础类型为unsafe.Pointer的类型,反之亦然

可以将uintptr类型的值显式转换为基础类型为unsafe.Pointer的类型,反之亦然

值的分配规则

可以将分配视为隐式转换。隐式转换规则列在上一节中的所有转换规则中。

除了这些规则之外,赋值中的目标值必须是可寻址的,映射索引表达式或空白标识符。

在赋值中,源值将复制到目标值。准确地说,源值的直接部分被复制到目标值。

注意,参数传递和结果返回实际上都是在赋值。

值比较规则

Go 规范

In any comparison, the first operand must be assignable to the type of the second operand, or vice versa.

因此,比较规则很像分配规则。换句话说,如果其中一个操作数可以隐式转换为另一个操作数的类型,则两个值是可比较的。对吗?几乎是,因为上述基本比较规则有一个例外。

如果比较中的两个操作数之一是接口类型,而另一个操作数是一个不可比较类型的非接口值(应该实现了前一个操作数的接口类型),则比较规则无效,即使非接口值可以隐式转换为接口类型

可比较类型与不可比较类型
当前(Go 1.12),以下类型不支持比较操作(使用==!=运算符)

  • 切片类型
  • map类型
  • 函数类型
  • 任何字段为不可比较类型的结构体类型,以及任何元素类型为不可比较类型的数组类型

上面列出的类型称为不可比较类型,所有其他类型称为可比较类型。编译器禁止比较两种不可比较类型的值。

任何map类型的key类型必须是可比较类型

请注意,虽然切片/map/函数类型的值不可比较,但可以将它们与无类型的nil值进行比较。

上述基本规则并未涵盖所有情况。如果比较中的两个操作数都是无类型(常量)值,那该怎么办?附加规则很简单:

  • 无类型的布尔值可以与无类型的布尔值进行比较
  • 无类型数值可以与无类型数值进行比较
  • 无类型字符串值可以与无类型字符串值进行比较

比较两个无类型数值的结果服从直觉。

注意,一个无类型的nil值不能和另一个无类型的nil值进行比较。

任意比较操作的结果都是一个无类型的布尔值。

以下示例展示了一些与不可比较类型相关的比较:

package main

// Some variables of uncomparable types.
var s []int
var m map[int]int
var f func()()
var t struct {x []int}
var a [5]map[int]int

func main() {
    // The following lines fail to compile.
    /*
    _ = s == s
    _ = m == m
    _ = f == f
    _ = t == t
    _ = a == a
    _ = nil == nil
    _ = s == interface{}(nil)
    _ = m == interface{}(nil)
    _ = f == interface{}(nil)
    */

    // The following lines compile okay.
    _ = s == nil
    _ = m == nil
    _ = f == nil
    _ = 123 == interface{}(nil)
    _ = true == interface{}(nil)
    _ = "abc" == interface{}(nil)
}

两个值如何比较?

假设两个值是可比较的,并且它们具有相同的类型T。(如果它们具有不同的类型,则其中一个必须可隐式转换为另一个的类型。这里我们不考虑两个值都是无类型的情况)

  1. 如果T是布尔类型,那么只有当两者都为true或都为false时,这两个值才相等
  2. 如果T是整数类型,那么只有当两者在内存中具有相同表示的情况下,这两个值才相等
  3. 如果T是浮点类型,那么当满足以下条件时它们相等:
    • 3.1 它们都是+Inf
    • 3.2 它们都是-Inf
    • 3.3 它们中的每一个都是-0.0+0.0
    • 3.4 它们都不是NaN,并且它们在内存中具有相同的字节表示
  4. 如果T是复数类型,那么只有当它们的实部(作为浮点值)和虚部(作为浮点值)都相等时,这两个值才相等
  5. 如果T是指针类型(safe或unsafe),那么只有它们指向的内存地址相同时,这两个值才相等
  6. 如果T是通道类型,如果它们都引用相同的底层内部通道结构或者它们都是nil,这两个值才相等
  7. 如果T是结构体类型,那么将比较两个结构体每个对应字段的值
  8. 如果T是数组类型,那么将比较两个数组的每个对应元素的值
  9. 如果T是接口类型,请参考how two interface values are compared
  10. 如果T是字符串类型,请参考how two string values are compared

请注意,将两个具有相同非比较动态类型的接口进行比较时会产生panic,以下是一些比较中会出现panic的例子:

package main

func main() {
    type T struct {
        a interface{}
        b int
    }
    var x interface{} = []int{}
    var y = T{a: x}
    var z = [3]T{}

    // Each of the following line can produce a panic.
    _ = x == x
    _ = y == y
    _ = z == z
}

请注意,在任意版本的Go SDK中,包含z的两行代码都可以通过go buildgo install命令,但是在Go SDK 1.9和Go SDK 1.10中运行go run命令会失败,这是一个已知问题,在Go SDK 1.11中已解决。