2.5. 类型声明
变量或表达式的类型定义了对应存储值的特征, 例如数值的存储大小(或者是元素的bit个数), 它们在内部是如何表达的, 是否支持一些操作符, 以及它们自己关联的方法集,
在任何程序中都会有一些变量有着相同的内部实现, 但是表示完全不同的概念. 例如, int 类型的变量可以用来表示一个循环的迭代索引, 或者一个时间戳, 或者一个文件描述符, 或者一个月份; 一个 float64 类型的变量可以用来表示每秒几米的速度, 或者是不同温度单位的温度; 一个字符串可以用来表示一个密码或者一个颜色的名称.
一个类型的声明创建了一个新的类型名称, 和现有类型具有相同的底层结构. 新命名的类型提供了一个方法, 用来分隔不同概念的类型, 即使它们底层类型相同也是不兼容的.
type name underlying-type
类型的声明一般出现在包级别, 因此如果新创建的类型名字名字的首字符大写, 则在外部包也可以使用.
为了说明类型声明, 我们将不同温度单位分别定义为不同的类型:
为了说明类型声明,让我们把不同温度范围分为不同的类型:
gopl.io/ch2/tempconv0
// Package tempconv performs Celsius and Fahrenheit temperature computations.
package tempconv
import "fmt"
type Celsius float64 // 摄氏温度
type Fahrenheit float64 // 华氏温度
const (
AbsoluteZeroC Celsius = -273.15 // 绝对零度
FreezingC Celsius = 0 // 结冰点温度
BoilingC Celsius = 100 // 沸水问题
)
func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }
这个包定义了两种类型, Celsius 和 Fahrenheit 分别对应不同的温度单位. 它们都有着相同的底层类型 float64, 但是它们是不同的数据类型, 因此它们不可以被相互比较或混在一个表达式计算. 可以区分类型, 可以避免一些像无意中结合单位的温度进行计算的错误; 因为需要一个类似 Celsius(t) 或 Fahrenheit(t) 显式的转型操作才能将 float64 转为对应的类型. Celsius(t) 和 Fahrenheit(t) 是类型转换操作, 并不是函数调用. 类型转换不会改变值本身, 但是会使它们的语义发生变化. 另一方面, 函数 CToF 和 FToC 则是对两个不同的温度单位进行转换, 它们会返回不同的值.
对于每一个类型 T, 都有一个对应的类型转换操作 T(x), 用于将 x 转为 T 类型. 只有当两个类型的底层基础类型相同时, 才允许这种转型操作, 或者是两者都是指向相同底层结构的指针类型, 这些转换只改变类型而不会影响值本身. 如果x是可以赋值给T类型的, 那么x必然可以被转为T类型, 但是一般没有必要.
数值类型之间的转型也是允许的, 并且在字符串和一些特定切片之间也是可以转换的, 在下一章我们会看到这样的例子. 这类转换可能改变值的表现. 例如, 将一个浮点数转为整数将丢弃小数部分, 将一个字符串转为 []byte 切片将拷贝一个字符串数据的副本. 在任何情况下, 运行时不会发送转换失败的错误(译注: 错误只会发生在编译阶段).
底层数据类型决定了内部结构和表达方式, 也包决定是否可以像底层类型一样对内置运算符的支持. 这意味着, Celsius 和 Fahrenheit 类型的算术行为和底层的 float64 类型一样, 正如你所期望的.
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
比较运算符 ==
和 <
也可以用来比较一个命名类型的变量和另一个有相同类型的变量或相同的底层类型的值做比较.
但是如果两个值有着不同的类型, 则不能直接进行比较:
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"!
注意最后那个语句. 尽管看起来想函数调用, 但是Celsius(f)类型转换, 并不会改变值, 它仅仅是改变值的类型而已. 测试为真的原因是因为 c 和 g 都是零值.
一个命名的类型可以提供符号方便, 特别是可以避免一遍又一遍地书写复杂类型(译注: 例如用匿名的结构体定义变量). 虽然对于像float64这种简单的底层类型没有简洁很多, 但是如果是复杂的类型将会简洁很多, 正如我们即将讨论的结构体类型:
命名类型还可以为该类型的值定义新的行为. 这些行为表示为一组关联到类型的函数, 我们成为类型的方法集. 我们将在第六章讨论方法的细节, 这里值说写简单用法.
下面的声明, Celsius 类型的参数 c 出现在了函数名的前面, 表示声明一个 Celsius 类型的 名叫 String 的方法, 方法返回 带着 °C 温度单位 的参数 c 的数字打印字符串:
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
许多类型都会定义个 String 方法, 因为当然用 fmt 包的打印方法时, 将会优先使用 String 方法返回的结果打印, 将在 7.1节 讲述.
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