类型转换、赋值和值比较规则大全

此篇文章将列出Go中所有的类型转换、赋值和值比较规则。

类型转换规则大全

在Go中,如果一个值v可以被显式地转换为类型T,则此转换可以使用语法形式(T)(v)来表示。 在大多数情况下,特别是T为一个类型名(即一个标识符)时,此形式可简化为T(v)

当我们说一个值x可以被隐式转换为一个类型T,这同时也意味着x可以被显式转换为类型T

1. 显然的类型转换规则

如果两个类型表示着同一个类型,则它们的值可以相互隐式转换为这两个类型中的任意一个。
比如,

此条规则没什么可解释的,无论你是否认为此种情况中发生了转换。

2. 底层类型相关的类型转换规则

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

(注意:两处“忽略掉结构体字段标签”从Go 1.8开始生效。)

一个例子:
package main

func main() {
	// 类型[]int、IntSlice和MySlice共享底层类型:[]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`}

	// 这两行隐式转换编译不通过。
	/*
	is = ms
	ms = is
	*/

	// 必须使用显式转换。
	is = IntSlice(ms)
	ms = MySlice(is)
	x = struct{n int `foo`}(y)
	y = struct{n int `bar`}(x)

	// 这些隐式转换是没问题的。
	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)  // pi的类型为*int
	var ip IntPtr = pi // 没问题,因为底层类型相同
	                   // 并且pi的类型为非定义类型。

	// var _ *MyInt = pi // 不能隐式转换
	var _ = (*MyInt)(pi) // 显式转换是没问题的

	// 类型*int的值不能被直接转换为类型MyIntPtr,
	// 但是可以间接地转换过去。
	/*
	var _ MyIntPtr = pi  // 不能隐式转换
	var _ = MyIntPtr(pi) // 也不能显式转换
	*/
	var _ MyIntPtr = (*MyInt)(pi)  // 间接隐式转换没问题
	var _ = MyIntPtr((*MyInt)(pi)) // 间接显式转换没问题

	// 类型IntPtr的值不能被直接转换为类型MyIntPtr,
	// 但是可以间接地转换过去。
	/*
	var _ MyIntPtr = ip  // 不能隐式转换
	var _ = MyIntPtr(ip) // 也不能显式转换
	*/
	// 间接隐式或者显式转换都是没问题的。
	var _ MyIntPtr = (*MyInt)((*int)(ip))  // ok
	var _ = MyIntPtr((*MyInt)((*int)(ip))) // ok
}

3. 数据通道相关的类型转换规则

假设类型Tx是一个双向数据通道类型,T也是一个数据通道类型(无论是双向的还是单向的),如果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,因为底层类型相同
	ca = cb // ok,因为底层类型相同

	// 这4行都满足此第2条转换规则的条件。
	var _, _ chan<- string = ca, cb // ok
	var _, _ <-chan string = ca, cb // ok
	var _ C1 = cb                   // ok
	var _ C2 = cb                   // ok

	// 类型C的值不能直接转换为类型C1或C2。
	/*
	var _ = C1(ca) // compile error
	var _ = C2(ca) // compile error
	*/

	// 但是类型C的值可以间接转换为类型C1或C2。
	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的接口值。此接口值包裹了
给定一个动态类型为T的接口值xx可以通过类型断言的方式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条规则中将介绍的字符串转换为字节切片或者码点切片的情况。)

给定一个常量值x和一个类型T,如果x可以表示成类型T的一个值,则x可以被显式地转换为类型T;特别地,如果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) // 这里必须显式转换

	const C1, C2 complex64 = F, I
	const I5 = int(C2) // 这里必须显式转换
}

7. 非常量数值转换规则

非常量浮点数和整数值可以被显式转换为任何浮点数和整数类型。
非常量复数值可以被显式转换为任何复数类型。
注意, 一个例子:
package main

import "fmt"

func main() {
	var a, b = 1.6, -1.6 // 类型均为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. 字符串相关的转换规则

如果一个值的类型(或者默认类型)为一个整数类型,则此值可以被当作一个码点值(rune值)显式转换为任何字符串类型。
一个字符串可以被显式转换为一个字节切片类型,反之亦然。 字节切片类型是指底层类型为[]byte的类型。
一个字符串可以被显式转换为一个码点切片类型,反之亦然。 码点切片类型是指底层类型为[]rune的类型。

请阅读字符串一文获取更多详情和示例。

9. 非类型安全指针相关的类型转换规则

非类型安全指针类型是指底层类型为unsafe.Pointer的类型。

任何类型安全指针类型的值可以被显式转化为一个非类型安全指针类型,反之亦然。
任何uintptr值可以被显式转化为一个非类型安全指针类型,反之亦然。

请阅读非类型安全指针一文获取详情和示例。

赋值规则

赋值可以看作是隐式类型转换。 各种隐式转换规则在上一节中已经列出。

除了这些规则,赋值语句中的目标值必须为一个可寻址的值、一个映射元素或者一个空标识符。

在一个赋值中,源值被复制给了目标值。精确地说,源值的直接部分被复制给了目标值。

注意:函数传参和结果返回其实都是赋值。

值比较规则

Go白皮书提到

在任何比较中,第一个比较值必须能被赋值给第二个比较值的类型,或者反之。

所以,值比较规则和赋值规则非常相似。 换句话说,两个值是否可以比较取决于其中一个值是否可以隐式转换为另一个值的类型。 很简单,对吧?此规则描述基本正确,但是有一个例外:

如果两个值中有一个为接口值,而另一个为非接口值并且它的类型为一个不可比较类型,则这两个值不可比较,即使此非接口值可以被隐式转换为接口值的类型(即此不可比较类型实现了接口值的类型)。

注意,尽管切片/映射/函数类型为不可比较类型,但是它们的值可以和类型不确定的预声明nil标识符比较。

上述规则并未覆盖所有的情况。如果两个值均为类型不确定值,它们可以比较吗?这种情况的规则比较简单:

两个类型不确定的数字值的比较结果服从直觉。

注意,两个类型不确定的nil值不能相互比较。

任何比较的结果均为一个类型不确定的布尔值。

一些值比较的例子:
package main

// 一些类型为不可比较类型的变量。
var s []int
var m map[int]int
var f func()()
var t struct {x []int}
var a [5]map[int]int

func main() {
	// 这些比较编译不通过。
	/*
	_ = s == s
	_ = m == m
	_ = f == f
	_ = t == t
	_ = a == a
	_ = nil == nil
	_ = s == interface{}(nil)
	_ = m == interface{}(nil)
	_ = f == interface{}(nil)
	*/

	// 这些比较编译都没问题。
	_ = s == nil
	_ = m == nil
	_ = f == nil
	_ = 123 == interface{}(nil)
	_ = true == interface{}(nil)
	_ = "abc" == interface{}(nil)
}

两个值是如何进行比较的?

假设两个值可以相互比较,并且它们的类型同为T。 (如果它们的类型不同,则其中一个可以转换为另一个的类型。这里我们不考虑两者均为类型不确定值的情形。)
  1. 如果T是一个布尔类型,则这两个值只有在它们同为true或者false的时候比较结果才为true
  2. 如果T是一个整数类型,则这两个值只有在它们在内存中的表示完全一致的情况下比较结果才为true
  3. 如果T是一个浮点数类型, 则这两个值只要满足下面任何一种情况,它们的比较结果就为true
    • 它们都为+Inf
    • 它们都为-Inf
    • 它们都为-0.0或者都为+0.0
    • 它们都不是NaN并且它们在内存中的表示完全一致。
  4. 如果T是一个复数类型,则这两个值只有在它们的实部和虚部均做为浮点数进行进行比较的结果都为true的情况下比较结果才为true
  5. 如果T是一个指针类型(类型安全或者非类型安全),则这两个值只有在它们所表示的地址值相等或者它们都为nil的情况下比较结果才为true
  6. 如果T是一个数据通道类型,则这两个值只有在它们引用着相同的底层内部数据通道或者它们都为nil时比较结果才为true
  7. 如果T是一个结构体类型,则它们的相应字段将逐对进行比较。只要有一对字段不相等,这两个结构体值就不相等。
  8. 如果T是一个数组类型,则它们的相应元素将逐对进行比较。只要有一对元素不相等,这两个结构体值就不相等。
  9. 如果T是一个接口类型,请参阅两个接口值是如何进行比较的
  10. 如果T是一个字符串类型,请参阅两个字符串值是如何进行比较的
请注意,动态类型均为同一个不可比较类型的两个接口值的比较将产生一个恐慌。比如下面的例子:
package main

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

	// 这三个比较均会产生一个恐慌。
	_ = x == x
	_ = y == y
	_ = z == z
}

请注意,上例中含有z的两行使用官方Go SDK各个版本中的go buildgo install子命令均编译没问题,但是当使用Go SDK 1.9和1.10中的 go run子命令运行上例时,编译将报错。 这是Go SDK 1.9和1.10中一个已知的bug

Go语言101项目目前同时托管在GithubGitlab上。 欢迎各位在这两个项目中通过提交bug和PR的方式来改进完善Go语言101中的各篇文章。

本书微信公众号名称为"Go 101"。每个工作日此公众号将尽量发表一篇和Go语言相关的原创短文。各位如果感兴趣,可以搜索关注一下。

赞赏