Golang 第二章 5.类型声明

  • By v2ray节点

  • 2024-08-17 09:43:17

  • 评论

变量或表达式的类型定义了其可能取值的特征,如大小(位数或元素数量)、内部表示方式、可对其执行的固有操作以及与之关联的方法。

在任何程序中,都有一些变量具有相同的表示形式,但表示非常不同的概念。例如,int 可以用于表示循环索引、时间戳、文件描述符或月份;float64 可以表示以米/秒为单位的速度或几种温标之一的温度;而 string 可以表示密码或颜色的名称。

类型声明定义了一种新的命名类型,该类型与现有类型具有相同的底层类型。命名类型提供了一种方式来分隔底层类型的不同且可能不兼容的使用,以防它们被意外混用。

type name underlying-type

类型声明通常出现在包级别,此时命名类型在整个包中都可见,并且如果名称是导出的(以大写字母开头),它也可以从其他包中访问。

为了说明类型声明的用法,让我们将不同的温度标度转换为不同的类型:

gopl.io/ch2/tempconv0
//tempconv 包执行摄氏度和华氏度温度计算。
package tempconv

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) }

这个包定义了两种类型,CelsiusFahrenheit,分别用于两个温度单位。尽管它们具有相同的底层类型 float64,但它们不是相同的类型,因此不能在算术表达式中进行比较或组合。区分这些类型可以避免错误,比如不小心将不同温标的温度结合在一起;从 float64 转换时,需要显式的类型转换,如 Celsius(t)Fahrenheit(t)Celsius(t)Fahrenheit(t) 是转换操作,而不是函数调用。它们不会以任何方式改变值或表示形式,但会使含义的改变变得明确。另一方面,CToFFToC 函数则用于在两种温标之间转换;它们确实会返回不同的值。

对于每种类型 T,都有一个对应的转换操作 T(x),将值 x 转换为类型 T。如果两者具有相同的底层类型,或两者都是指向相同底层类型的未命名指针类型,则允许从一种类型转换为另一种类型;这些转换仅改变类型而不改变值的表示形式。如果 x 可赋值给 T,则允许进行转换,但通常是多余的。

在数值类型之间,以及在字符串和某些切片类型之间也允许转换,正如我们将在下一章中看到的。这些转换可能会改变值的表示形式。例如,将浮点数转换为整数会丢弃任何小数部分,而将字符串转换为 []byte 切片会分配字符串数据的副本。无论如何,转换在运行时永远不会失败。

命名类型的底层类型决定了其结构和表示形式,也决定了它支持的固有操作集,这些操作与直接使用底层类型时相同。这意味着,算术运算符对 CelsiusFahrenheit 的操作方式与对 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) 的类型转换并不改变其参数的值,只是改变了其类型。该测试为真,因为 cf 都是零。

如果一个命名类型有助于避免重复书写复杂类型,那么它可能提供表示上的便利。当底层类型像 float64 这样简单时,优势不大,但对于复杂类型,这种优势会很明显,我们在讨论结构体时会看到这一点。

命名类型还使得可以为该类型的值定义新的行为。这些行为通过一组与类型关联的函数来表示,称为该类型的方法。我们将在第6章详细讨论方法,但这里会简要介绍一下这一机制。

下面的声明中,Celsius 参数 c 出现在函数名称之前,这为 Celsius 类型关联了一个名为 String 的方法,该方法返回 c 的数值并在后面加上 °C

func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }

许多类型都会声明这种形式的 String 方法,因为它控制了当使用 fmt 包将类型的值打印为字符串时的显示方式,正如我们将在第七章.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


v2ray节点购买