Go 是一种强类型语言,这意味着任何存储(或生成)值的语言元素都有一个与之关联的类型。在本章中,读者将了解类型系统的功能,并探索该语言支持的常见数据类型,如下所述:
- Go
- 数字类型
- 布尔型
- 指针
- 类型声明
- 类型转换
为了帮助启动关于类型的对话,让我们看一下可用的类型。Go 实现了一个简单的类型系统,程序员可以直接控制内存的分配和布局。当程序声明变量时,必须发生两件事:
- 变量必须接收一个类型
- 变量也将被绑定到一个值(即使没有赋值)
这允许类型系统分配存储声明值所需的字节数。声明变量的内存布局直接映射到它们声明的类型。没有发生类型装箱或自动类型转换。您期望分配的空间实际上是内存中保留的空间。
为了证明这一事实,下面的程序使用一个名为unsafe
的特殊包来绕过类型系统并提取声明变量的内存大小信息。需要注意的是,这纯粹是说明性的,因为大多数程序通常不使用unsafe
包。
package main
import (
"fmt"
"unsafe"
)
var (
a uint8 = 72
b int32 = 240
c uint64 = 1234564321
d float32 = 12432345.232
e int64 = -1233453443434
f float64 = -1.43555622362467
g int16 = 32000
h [5]rune = [5]rune{'O', 'n', 'T', 'o', 'p'}
)
func main() {
fmt.Printf("a = %v [%T, %d bits]\n", a, a, unsafe.Sizeof(a)*8)
fmt.Printf("b = %v [%T, %d bits]\n", b, b, unsafe.Sizeof(b)*8)
fmt.Printf("c = %v [%T, %d bits]\n", c, c, unsafe.Sizeof(c)*8)
fmt.Printf("d = %v [%T, %d bits]\n", d, d, unsafe.Sizeof(d)*8)
fmt.Printf("e = %v [%T, %d bits]\n", e, e, unsafe.Sizeof(e)*8)
fmt.Printf("f = %v [%T, %d bits]\n", f, f, unsafe.Sizeof(f)*8)
fmt.Printf("g = %v [%T, %d bits]\n", g, g, unsafe.Sizeof(g)*8)
fmt.Printf("h = %v [%T, %d bits]\n", h, h, unsafe.Sizeof(h)*8)
}
golang.fyi/ch04/alloc.go
执行程序时,它会打印出每个声明变量所消耗的内存量(以位为单位):
$>go run alloc.go
a = 72 [uint8, 8 bits]
b = 240 [int32, 32 bits]
c = 1234564321 [uint64, 64 bits]
d = 1.2432345e+07 [float32, 32 bits]
e = -1233453443434 [int64, 64 bits]
f = -1.43555622362467 [float64, 64 bits]
g = 32000 [int16, 16 bits]
h = [79 110 84 111 112] [[5]int32, 160 bits]
从前面的输出中,我们可以看到变量a
(类型uint8
)将使用八位(或一个字节)存储,变量b
使用 32 位(或四个字节)存储,依此类推。通过影响内存消耗的能力,再加上 Go 对指针类型的支持,程序员能够强烈控制程序中内存的分配和消耗。
本章将介绍下表中列出的类型。它们包括基本类型,如数字、布尔和字符串:
| **型** | **说明** | | `string` | 用于存储文本值的类型 | | `rune` | 用于表示字符的整数类型(int32)。 | | 这和分母的分母是一样的,这就是分母的分母。 | 用于存储整数值的类型。 | | `float32`、`float64` | 用于存储浮点十进制值的类型。 | | `complex64`、`complex128` | 可以用实部和虚部表示复数的类型。 | | `bool` | 输入布尔值。 | | `*T`,指向类型 T 的指针 | 表示存储 T 型值的存储器地址的一种类型。 |Go 支持的其他类型,如下表中列出的类型,包括复合类型、接口类型、函数类型和通道类型。这些类型将在后面专门介绍各自主题的章节中介绍。
| **型** | **说明** | | 数组`[n]T` | `T`类型元素的数字索引序列的固定大小`n`的有序集合。 | | 切片`[]T` | `T`类型元素的数字索引序列的未指定大小的集合。 | | `struct{}` | 结构是由称为字段的元素组成的复合类型(想想对象)。 | | `map[K]T` | 由任意类型的键`K`索引的`T`类型元素的无序序列。 | | `interface{}` | 一组命名的函数声明,定义一组可由其他类型实现的操作。 | | `func (T) R` | 表示具有给定参数类型`T`和返回类型`R`的所有函数的类型。 | | `chan T` | 用于内部通信信道发送或接收类型为`T`的值的类型。 |Go 的数字类型包括对整数和十进制值的支持,大小从 8 位到 64 位不等。每个数字类型在内存中都有自己的布局,类型系统认为它们是唯一的。为了实现这一点,并避免在不同平台上进行移植时出现任何混乱,数字类型的名称反映了其大小要求。例如, *int16*
类型表示使用 16 位进行内部存储*的整数类型。*这意味着在赋值、表达式和操作中跨越类型边界时,必须显式转换数字值。
下面的程序不是所有的函数,因为所有的值都被分配给空白标识符。但是,它说明了 Go 中支持的所有数字数据类型。
package main
import (
"math"
"unsafe"
)
var _ int8 = 12
var _ int16 = -400
var _ int32 = 12022
var _ int64 = 1 << 33
var _ int = 3 + 1415
var _ uint8 = 18
var _ uint16 = 44
var _ uint32 = 133121
var i uint64 = 23113233
var _ uint = 7542
var _ byte = 255
var _ uintptr = unsafe.Sizeof(i)
var _ float32 = 0.5772156649
var _ float64 = math.Pi
var _ complex64 = 3.5 + 2i
var _ complex128 = -5.0i
func main() {
fmt.Println("all types declared!")
}
golang.fyi/ch04/nums。
下表列出了可表示无符号整数的所有可用类型及其在 Go 中的存储要求:
| **型** | **尺寸** | **说明** | | `uint8` | 无符号 8 位 | 范围 0-255 | | `uint16` | 无符号 16 位 | 范围 0-65535 | | `uint32` | 无符号 32 位 | 范围 0-4294967295 | | `uint64` | 无符号 64 位 | 范围 0-18446744073709551615 | | `uint` | 具体实施 | 一种预先声明的类型,用于表示 32 位或 64 位整数。从 Go 的 1.x 版本开始,`uint`表示一个 32 位无符号整数。 | | `byte` | 无符号 8 位 | `unit8`类型的别名。 | | `uintptr` | 未签名 | 一种无符号整数类型,用于存储底层计算机体系结构的指针(内存地址)。 |下表列出了可表示有符号整数的所有可用类型及其在 Go 中的存储要求:
| **型** | **尺寸** | **说明** | | `int8` | 有符号 8 位 | 范围-128-127 | | `int16` | 符号 16 位 | 范围-32768-32767 | | `int32` | 有符号 32 位 | 范围-2147483648-2147483647 | | `int64` | 有符号 64 位 | 范围-9223372036854775808-9223372036854775807 | | `int` | 具体实施 | 一种预先声明的类型,用于表示 32 位或 64 位整数。从 Go 的 1.x 版本开始,`int`表示一个 32 位有符号整数。 |Go 支持使用 IEEE 标准表示十进制值的以下类型:
| **型** | **尺寸** | **说明** | | `float32` | 有符号 32 位 | IEEE-754 单精度浮点值的标准表示法。 | | `float64` | 有符号 64 位 | IEEE-754 双精度浮点值的标准表示法。 |Go 还支持用虚部和实部表示复数,如下表所示:
| **型** | **尺寸** | **说明** | | `complex64` | 浮动 32 | 表示实数和虚数部分存储为`float32`值的复数。 | | `complex128` | 浮动 64 | 表示实数和虚数部分存储为`float64`值的复数。 |Go 支持使用数字序列自然表示整数值,并结合符号和小数点(如前一示例所示)。或者,Go 整数文字还可以表示十六进制和八进制数,如以下程序所示:
package main
import "fmt"
func main() {
vals := []int{
1024,
0x0FF1CE,
0x8BADF00D,
0xBEEF,
0777,
}
for _, i := range vals {
if i == 0xBEEF {
fmt.Printf("Got %d\n", i)
break
}
}
}
golang.fyi/ch04/intslit.go
十六进制值以0x
或0X
前缀开头,而八进制值以数字 0 开头,如前一示例所示。浮点值可以使用十进制和指数符号表示,如以下示例所示:
package main
import "fmt"
func main() {
p := 3.1415926535
e := .5772156649
x := 7.2E-5
y := 1.616199e-35
z := .416833e32
fmt.Println(p, e, x, y, z)
}
golang.fyi/ch04/floats.go
前面的程序显示了 Go 中浮点文本的几种表示形式。数字可以包括一个可选的指数部分,该部分由数字末尾的e
(或E
表示。例如,代码中的1.616199e-35
表示数值 1.616199×10-35。最后,Go 支持用文字表示复数,如以下示例所示:
package main
import "fmt"
func main() {
a := -3.5 + 2i
fmt.Printf("%v\n", a)
fmt.Printf("%+g, %+g\n", real(a), imag(a))
}
golang.fyi/ch04/complex.go
在前面的示例中,变量a
被分配了一个复数,该复数具有实部和虚部。虚文字是一个浮点数,后跟字母i
。请注意,Go 还提供了两个内置函数,real()
和imag(),
分别将复数分解为实部和虚部。
在 Go 中,布尔二进制值使用bool
类型存储。尽管类型为bool
的变量存储为 1 字节值,但它不是数值的别名。Go 提供两个预先声明的文本true
和false
,以表示布尔值,如以下示例所示:
package main
import "fmt"
func main() {
var readyToGo bool = false
if !readyToGo {
fmt.Println("Come on")
} else {
fmt.Println("Let's go!")
}
}
golang.fyi/ch04/bool.go
为了开始我们关于rune
和string
类型的讨论,需要一些背景语境。Go 可以将其源代码中的字符和字符串文字常量视为 Unicode。它是一个全球标准,其目标是通过为每个字符指定一个数值(称为代码点)来为已知书写系统的符号编目。
默认情况下,Go 固有地支持 UTF-8,这是编码和存储 Unicode 数值的有效方法。这就是继续这个主题所需要的全部背景知识。由于超出了本书的范围,因此将不再讨论更多细节。
那么,rune
类型与 Unicode 到底有什么关系呢?符文是int32类型的别名。它专门用于存储编码为 UTF-8 的 Unicode 整数值。让我们看看以下程序中的一些符文文字:
golang.fyi/ch04/rune.go
前一个程序中的每个变量都将一个 Unicode 字符存储为rune
值。在 Go 中,rune
可以指定为字符串文字常量,并用单引号括起来。文字可以是以下内容之一:
- 可打印字符(如变量[T0]、[T1]和[T2]所示)
- 对于不可打印的控制值,如制表符、换行符、换行符等,使用反斜杠转义单个字符
\u
后面直接跟 Unicode 值(\u0369
)\x
后跟两个十六进制数字- 反斜杠后跟三个八进制数字([T0])
不管单引号中的[T0]文字值如何,编译器编译并分配一个整数值,如前面变量的打印输出所示:
$>go run runes.go
8
9
10
632
2438
35486
873
250
37
在 Go 中,字符串被实现为不可变字节值的片段。将字符串值分配给变量后,该字符串的值将永远不会更改。通常,字符串值表示为双引号内的常量文字,如以下示例所示:
golang.fyi/ch04/string.go
前面的代码片段显示变量txt
被分配了一个字符串文本,包含七个字符,包括两个嵌入的汉字。如前所述,Go 编译器将自动将字符串文字值解释为 Unicode 字符,并使用 UTF-8 对其进行编码。这意味着,在封面下,每个文字字符都存储为一个rune
,并且每个可见字符可能会占用多个字节的存储空间。事实上,当程序执行时,它将txt
的长度打印为11
,而不是字符串预期的七个字符,这说明了用于中文符号的额外字节。
下面的代码段(来自上一个示例)包括两个分别分配给变量txt2
和txt3
的字符串文本。正如您所看到的,这两个文本具有完全相同的内容,但是编译器将以不同的方式处理它们:
var (
txt2 = "\u6C34\x20brings\x20\x6c\x69\x66\x65."
txt3 = `
\u6C34\x20
brings\x20
\x6c\x69\x66\x65\.
`
)
golang.fyi/ch04/string.go
分配给变量txt2
的文字值用双引号括起来。这称为解释字符串。解释字符串可能包含正常的可打印字符以及反斜杠转义值,这些转义值被解析并解释为rune
文本。因此,当打印txt2
时,转义值转换为以下字符串:
解释字符串中的每个符号对应一个转义值或一个可打印符号,如下表所示:
|  | **<空间>** | **带来** | **<空间>** | **生命** | . | | \u6C34 | \x20 | 带来 | \x20 | \x6c\x69\x66\x65 | . |另一方面,分配给变量txt3
的文字值被严重的重音字符```go`包围。这将在 Go 中创建称为原始字符串的内容。原始字符串值在忽略转义序列且所有有效字符在文本中显示时都进行编码的情况下不被解释。
打印变量txt3
时,产生以下输出:
\u6C34\x20brings\x20\x6c\x69\x66\x65.
```go
请注意,打印的字符串包含原始字符串文字中显示的所有反斜杠转义值。未解释的字符串文字是在源代码主体中嵌入大型多行文本内容而不破坏其语法的好方法。
# 指针
在 Go 中,当一段数据存储在内存中时,可以直接访问该数据的值,或者可以使用指针来引用数据所在的内存地址。与其他 C 族语言一样,Go 中的指针提供了一定程度的间接性,使程序员能够更高效地处理数据,而无需在每次需要时复制实际数据值。
然而,与 C 不同,Go 运行时在运行时维护对指针管理的控制。程序员不能向指针添加任意整数值以生成新的指针地址(这种做法称为指针算术)。一旦某个内存区域被指针引用,该区域中的数据将保持可访问状态,直到不再被任何指针变量引用为止。此时,未引用的值就可以进行垃圾收集。
## 指针类型
与 C/C++类似,Go 使用`*`操作符将类型指定为指针。以下代码段显示了几个具有不同基础类型的指针:
package main import "fmt"
var valPtr *float32 var countPtr *int var person *struct { name string age int } var matrix *[1024]int var row []*int64
func main() { fmt.Println(valPtr, countPtr, person, matrix, row) }
golang.fyi/ch04/pointers.go
给定一个类型为`T`的变量,Go 使用表达式`*T`作为其指针类型。类型系统认为`T`和`*T`是不同的,不可替代。指针不指向任何东西时的零值是地址 0,由文字*常量*nil 表示。
## 地址运算符
指针值只能分配给其声明类型的地址。在 Go 中执行此操作的一种方法是使用地址运算符`&`(与符号)获取变量的地址值,如下例所示:
package main import "fmt"
func main() { var a int = 1024 var aptr *int = &a
fmt.Printf("a=%v\n", a) fmt.Printf("aptr=%v\n", aptr) }
golang.fyi/ch04/pointers.go
指针类型为`*int`的变量`aptr`被初始化,并使用表达式`&a`为变量`a`分配地址值,如下所示:
var a int = 1024 var aptr *int = &a
当变量`a`存储实际值时,我们说`aptr`指向`a`。以下显示了变量`a`值及其分配给`aptr`的存储位置的程序输出:
a=1024 aptr=0xc208000150
分配的地址值将始终相同(始终指向`a`),无论在代码中的何处可以访问`aptr`。还值得注意的是,Go 不允许对数字、字符串和布尔类型使用带文字常量的地址运算符。因此,以下内容将不会编译:
var aptr *int = &1024
fmt.Printf("a ptr1 = %v\n", aptr)
但是,当使用文字常量初始化复合类型(如 struct 和 array)时,该规则有一个语法上的例外。以下程序说明了此类场景:
package main import "fmt"
func main() { structPtr := &struct{ x, y int }{44, 55} pairPtr := &[2]string{"A", "B"}
fmt.Printf("struct=%#v, type=%T\n", structPtr, structPtr) fmt.Printf("pairPtr=%#v, type=%T\n", pairPtr, pairPtr) }
golang.fyi/ch04/address2.go
在前面的代码段中,address 运算符直接与复合文字`&struct{ x, y int }{44, 55}`和`&[2]string{"A", "B"}` 一起使用,分别返回指针类型`*struct { x int; y int }`和`*[2]string`。这是一种语法上的甜点,它消除了将值分配给变量,然后检索其分配的地址的中间步骤。
## 新的()函数
内置函数*新增(<类型>*也可用于初始化指针值。它首先为指定类型的零值分配适当的内存。然后,函数返回新创建的值的地址。以下程序使用`new()`函数初始化变量`intptr`和`p`:
package main import "fmt"
func main() { intptr := new(int) *intptr = 44
p := new(struct{ first, last string }) p.first = "Samuel" p.last = "Pierre"
fmt.Printf("Value %d, type %T\n", *intptr, intptr) fmt.Printf("Person %+v\n", p) }
golang.fyi/ch04/newptr.go
变量`intptr`初始化为`*int`,变量`p`初始化为`*struct{first, last string}`。一旦初始化,这两个值将在代码的后面相应地更新。当初始化时实际值不可用时,您可以使用`new()`函数以零值初始化指针变量。
## 指针间接寻址-访问引用值
如果您只有一个地址,您可以通过对指针值本身应用`*`运算符(或取消引用)来访问它所指向的值。下面的程序在函数`double()`和`cap()`中说明了这一思想:
package main import ( "fmt" "strings" )
func main() { a := 3 double(&a) fmt.Println(a) p := &struct{ first, last string }{"Max", "Planck"} cap(p) fmt.Println(p) }
func double(x *int) { *x = *x * 2 }
func cap(p *struct{ first, last string }) { p.first = strings.ToUpper(p.first) p.last = strings.ToUpper(p.last) }
golang.fyi/ch04/derefptr.go
在前面的代码中,函数`double()`中的表达式`*x = *x * 2`可以分解如下,以了解其工作原理:
<colgroup><col> <col></colgroup>
| **表达式** | **步骤** |
|
*x * 2
| 原始表达式,其中 `x`为`*int`类型。 |
|
*(*x) * 2
| 通过将`*` 应用于地址值来解引用指针。 |
|
3 * 2 = 6
| `*(*x) = 3`的解引用值。 |
|
*(*x) = 6
| 此表达式的右侧取消引用`x`的值。它将使用结果 6 进行更新。 |
在函数`cap()`中,使用类似的方法访问和更新`struct{first, last string}`类型的复合变量`p`中的字段。然而,在处理复合材料时,这个成语更宽容。无需写入`*p.first`即可访问指针的字段值。我们可以放下`*`直接使用`p.first = strings.ToUpper(p.first).`
# 类型声明
在 Go 中,可以将一个类型绑定到一个标识符,以创建一个新的命名类型,该类型可以在需要该类型的地方被引用和使用。声明类型采用以下常规格式:
*类型<名称标识符><基础类型名称>*
类型声明以关键字`type`开头,后跟*名称标识符*和现有*基础类型*的名称。基础类型可以是内置的命名类型,如以下类型声明片段中所示的数值类型、布尔类型或字符串类型之一:
type truth bool type quart float64 type gallon float64 type node string
### 注
类型声明还可以使用复合*类型文字*作为其基础类型。复合类型包括数组、切片、映射和结构。本节重点介绍非复合类型。有关复合类型的详细信息,请参阅[第 7 章](07.html "Chapter 7. Composite Types")、*复合类型*。
以下示例说明了命名类型如何以其最基本的形式工作。示例中的代码转换温度值。每个温度单位由一个声明的类型表示,包括`fahrenheit`、`celsius`和`kelvin`。
package main import "fmt"
type fahrenheit float64 type celsius float64 type kelvin float64
func fharToCel(f fahrenheit) celsius { return celsius((f - 32) * 5 / 9) }
func fharToKel(f fahrenheit) celsius { return celsius((f-32)*5/9 + 273.15) }
func celToFahr(c celsius) fahrenheit { return fahrenheit(c*5/9 + 32) }
func celToKel(c celsius) kelvin { return kelvin(c + 273.15) }
func main() { var c celsius = 32.0 f := fahrenheit(122) fmt.Printf("%.2f \u00b0C = %.2f \u00b0K\n", c, celToKel(c)) fmt.Printf("%.2f \u00b0F = %.2f \u00b0C\n", f, fharToCel(f)) }
golang.fyi/ch04/typedef。
在前面的代码段中,新声明的类型都基于底层内置的数字类型`float64`。声明新类型后,可以将其分配给变量并参与表达式,就像其基础类型一样。新声明的类型将具有相同的零值,并且可以在其基础类型之间进行转换。
# 类型转换
一般来说,Go 认为每种类型都是不同的。这意味着在正常情况下,不同类型的值在赋值、函数参数和表达式上下文中是不可替换的。这对于内置类型和声明类型都是如此。例如,由于类型不匹配,以下情况将导致生成错误:
package main import "fmt"
type signal int
func main() { var count int32 var actual int var test int64 = actual + count
var sig signal var event int = sig
fmt.Println(test) fmt.Println(event) }
golang.fyi/ch04/type_conv.go
表达式`actual + count`导致生成时错误,因为两个变量的类型不同。即使变量`actual`和`count`是数值类型,并且`int32`和`int`具有相同的内存表示,编译器仍然拒绝该表达式。
声明的命名类型及其基础类型也是如此。编译器将拒绝分配`var event int = sig`,因为类型`signal`被认为与类型`int`不同。即使`signal`使用`int`作为其基础类型,这也是事实。
要跨越类型边界,Go 支持将值从一种类型转换为另一种类型的类型转换表达式。类型转换使用以下格式完成:
*<目标\类型>(<值或表达式>)*
以下代码段通过将变量转换为正确的类型修复了前面的示例:
type signal int func main() { var count int32 var actual int var test int32 = int32(actual) + count
var sig signal var event int = int(sig) }
golang.fyi/ch04/type_conv2.go
请注意,在前面的代码段赋值表达式中,`var test int32 = int32(actual) + count`将变量`actual`转换为适当的类型,以匹配表达式的其余部分。类似地,表达式`var event int = int(sig)`转换变量`sig`以匹配赋值中的目标类型`int`。
转换表达式通过显式更改封闭值的类型来满足赋值。显然,并非所有类型都可以从一种转换为另一种。下表总结了适当且允许进行类型转换的常见情况:
<colgroup><col> <col></colgroup>
| **说明** | **代码** |
| 目标类型和转换值都是简单的数字类型。 |
var i int
var i2 int32 = int32(i)
var re float64 = float64(i + int(i2))
|
| 目标类型和转换后的值都是复杂的数字类型。 |
var cn64 complex64
var cn128 complex128 = complex128(cn64)
|
| 目标类型和转换后的值具有相同的基础类型。 |
type signal int
var sig signal
var event int = int(sig)
|
| 目标类型是字符串,转换后的值是有效的整数类型。 |
a := string(72)
b := string(int32(101))
c := string(rune(108))
|
| 目标类型是字符串,转换后的值是字节片、int32 或符文。 |
msg0 := string([]byte{'H','i'})
msg1 := string([]rune{'Y','o','u','!'})
|
| 目标类型是字节、int32 或符文值的切片,转换后的值是字符串。 |
data0 := []byte("Hello")
data0 := []int32("World!")
|
此外,当目标类型和转换的值是引用相同类型的指针时,转换规则也起作用。除了上表中的这些场景之外,Go 类型不能显式转换。任何这样做的尝试都将导致编译错误。
# 总结
本章向读者介绍了 Go-type 系统。本章首先概述了类型,并全面探讨了基本的内置类型,如数字、布尔、字符串和指针类型。讨论继续进行,让读者了解其他重要主题,如命名类型定义。本章最后介绍了类型转换的机制。在接下来的章节中,您将有机会进一步了解其他类型,如复合、函数和接口。