2016-02-15 03:06:34 +00:00
## 2.5. 类型
2015-12-09 07:45:11 +00:00
2016-02-15 03:06:34 +00:00
变量或表达式的类型定义了对应存储值的属性特征, 例如数值在内存的存储大小( 或者是元素的bit个数) , 它们在内部是如何表达的, 是否支持一些操作符, 以及它们自己关联的方法集等。
2015-12-09 07:45:11 +00:00
2016-02-15 03:06:34 +00:00
在任何程序中都会存在一些变量有着相同的内部结构, 但是却表示完全不同的概念。例如, 一个int类型的变量可以用来表示一个循环的迭代索引、或者一个时间戳、或者一个文件描述符、或者一个月份; 一个float64类型的变量可以用来表示每秒移动几米的速度、或者是不同温度单位下的温度; 一个字符串可以用来表示一个密码或者一个颜色的名称。
2015-12-09 07:45:11 +00:00
2016-02-15 03:06:34 +00:00
一个类型声明语句创建了一个新的类型名称,和现有类型具有相同的底层结构。新命名的类型提供了一个方法,用来分隔不同概念的类型,这样即使它们底层类型相同也是不兼容的。
2015-12-09 07:45:11 +00:00
```Go
2016-02-15 03:06:34 +00:00
type 类型名字 底层类型
2015-12-09 07:45:11 +00:00
```
2016-02-15 03:06:34 +00:00
类型声明语句一般出现在包一级,因此如果新创建的类型名字的首字符大写,则在外部包也可以使用。
2015-12-09 07:45:11 +00:00
2016-02-15 03:06:34 +00:00
译注: 对于中文汉字, Unicode标志都作为小写字母处理, 因此中文的命名默认不能导出; 不过国内的用户针对该问题提出了不同的看法, 根据RobPike的回复, 在Go2中有可能会将中日韩等字符当作大写字母处理。下面是RobPik在 [Issue763 ](https://github.com/golang/go/issues/5763 ) 的回复:
2015-12-09 07:45:11 +00:00
2015-12-26 12:03:47 +00:00
> A solution that's been kicking around for a while:
>
2016-02-15 03:06:34 +00:00
> For Go 2 (can't do it before then): Change the definition to “lower case letters and _ are package-local; all else is exported”. Then with non-cased languages, such as Japanese, we can write 日本语 for an exported name and _日本语 for a local name. This rule has no effect, relative to the Go 1 rule, with cased languages. They behave exactly the same.
2015-12-26 12:03:47 +00:00
2016-02-15 03:06:34 +00:00
为了说明类型声明,我们将不同温度单位分别定义为不同的类型:
2015-12-09 07:45:11 +00:00
2016-01-20 15:16:19 +00:00
< u > < i > gopl.io/ch2/tempconv0< / i > < / u >
2015-12-09 07:45:11 +00:00
```Go
// Package tempconv performs Celsius and Fahrenheit temperature computations.
package tempconv
import "fmt"
2016-02-15 03:06:34 +00:00
type Celsius float64 // 摄氏温度
type Fahrenheit float64 // 华氏温度
2015-12-09 07:45:11 +00:00
const (
2016-02-15 03:06:34 +00:00
AbsoluteZeroC Celsius = -273.15 // 绝对零度
FreezingC Celsius = 0 // 结冰点温度
BoilingC Celsius = 100 // 沸水温度
2015-12-09 07:45:11 +00:00
)
func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }
```
2016-02-15 03:06:34 +00:00
我们在这个包声明了两种类型: Celsius和Fahrenheit分别对应不同的温度单位。它们虽然有着相同的底层类型float64, 但是它们是不同的数据类型, 因此它们不可以被相互比较或混在一个表达式运算。刻意区分类型, 可以避免一些像无意中使用不同单位的温度混合计算导致的错误; 因此需要一个类似Celsius(t)或Fahrenheit(t)形式的显式转型操作才能将float64转为对应的类型。Celsius(t)和Fahrenheit(t)是类型转换操作, 它们并不是函数调用。类型转换不会改变值本身, 但是会使它们的语义发生变化。另一方面, CToF和FToC两个函数则是对不同温度单位下的温度进行换算, 它们会返回不同的值。
2015-12-09 07:45:11 +00:00
2016-02-15 03:06:34 +00:00
对于每一个类型T, 都有一个对应的类型转换操作T(x), 用于将x转为T类型( 译注: 如果T是指针类型, 可能会需要用小括弧包装T, 比如`(*int)(0)`) 。只有当两个类型的底层基础类型相同时, 才允许这种转型操作, 或者是两者都是指向相同底层结构的指针类型, 这些转换只改变类型而不会影响值本身。如果x是可以赋值给T类型的值, 那么x必然也可以被转为T类型, 但是一般没有这个必要。
2015-12-09 07:45:11 +00:00
2016-02-15 03:06:34 +00:00
数值类型之间的转型也是允许的, 并且在字符串和一些特定类型的slice之间也是可以转换的, 在下一章我们会看到这样的例子。这类转换可能改变值的表现。例如, 将一个浮点数转为整数将丢弃小数部分, 将一个字符串转为`[]byte`类型的slice将拷贝一个字符串数据的副本。在任何情况下, 运行时不会发生转换失败的错误( 译注: 错误只会发生在编译阶段)。
2015-12-09 07:45:11 +00:00
2016-02-15 03:06:34 +00:00
底层数据类型决定了内部结构和表达方式, 也决定是否可以像底层类型一样对内置运算符的支持。这意味着, Celsius和Fahrenheit类型的算术运算行为和底层的float64类型是一样的, 正如我们所期望的那样。
2015-12-09 07:45:11 +00:00
```Go
fmt.Printf("%g\n", BoilingC-FreezingC) // "100" °C
boilingF := CToF(BoilingC)
fmt.Printf("%g\n", boilingF-CToF(FreezingC)) // "180" °F
fmt.Printf("%g\n", boilingF-FreezingC) // compile error: type mismatch
```
2016-02-15 03:06:34 +00:00
比较运算符`==`和`< `也可以用来比较一个命名类型的变量和另一个有相同类型的变量,或有着相同底层类型的未命名类型的值之间做比较。但是如果两个值有着不同的类型,则不能直接进行比较:
2015-12-09 07:45:11 +00:00
```Go
var c Celsius
var f Fahrenheit
fmt.Println(c == 0) // "true"
fmt.Println(f >= 0) // "true"
fmt.Println(c == f) // compile error: type mismatch
fmt.Println(c == Celsius(f)) // "true"!
```
2016-02-15 03:06:34 +00:00
注意最后那个语句。尽管看起来想函数调用, 但是Celsius(f)是类型转换操作, 它并不会改变值, 仅仅是改变值的类型而已。测试为真的原因是因为c和g都是零值。
2015-12-09 07:45:11 +00:00
2016-02-15 03:06:34 +00:00
一个命名的类型可以提供书写方便, 特别是可以避免一遍又一遍地书写复杂类型( 译注: 例如用匿名的结构体定义变量) 。虽然对于像float64这种简单的底层类型没有简洁很多, 但是如果是复杂的类型将会简洁很多, 特别是我们即将讨论的结构体类型。
2015-12-09 07:45:11 +00:00
2016-02-15 03:06:34 +00:00
命名类型还可以为该类型的值定义新的行为。这些行为表示为一组关联到该类型的函数集合,我们称为类型的方法集。我们将在第六章中讨论方法的细节,这里值说写简单用法。
2015-12-09 07:45:11 +00:00
2016-02-15 03:06:34 +00:00
下面的声明语句, Celsius类型的参数c出现在了函数名的前面, 表示声明的是Celsius类型的一个叫名叫String的方法, 该方法返回该类型对象c带着°C温度单位的字符串:
2015-12-09 07:45:11 +00:00
```Go
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
```
2016-02-15 03:06:34 +00:00
许多类型都会定义一个String方法, 因为当使用fmt包的打印方法时, 将会优先使用该类型对应的String方法返回的结果打印, 我们将在7.1节讲述。
2015-12-09 07:45:11 +00:00
```Go
c := FToC(212.0)
fmt.Println(c.String()) // "100°C"
fmt.Printf("%v\n", c) // "100°C"; no need to call String explicitly
fmt.Printf("%s\n", c) // "100°C"
fmt.Println(c) // "100°C"
fmt.Printf("%g\n", c) // "100"; does not call String
fmt.Println(float64(c)) // "100"; does not call String
```