Data Types(数据类型)
Go 是静态类型语言:每个变量在编译时都有确定的类型,类型决定它能保存什么值、支持哪些操作,以及赋值和比较是否合法。Go 不会在不同命名类型之间随意隐式转换,这使许多类型错误能在程序运行前被发现。
从一个最小示例开始
下面的程序声明了几种常用类型,并使用 %T 输出值的动态类型:
package main
import "fmt"
func main() {
var enabled bool = true
var count int = 3
var ratio float64 = 0.75
var name string = "Go"
fmt.Printf("%T %T %T %T\n", enabled, count, ratio, name)
}
输出如下:
bool int float64 string
显式类型适合强调范围或 API 契约;初始值已经能清楚表达类型时,也可以让编译器推断。
基础类型
Go 的预声明基础类型主要包括:
| 类别 | 类型 | 说明 |
|---|---|---|
| 布尔 | bool | 只包含 true 和 false |
| 有符号整数 | int、int8、int16、int32、int64 | int 的宽度取决于目标平台,至少为 32 位 |
| 无符号整数 | uint、uint8、uint16、uint32、uint64 | 不能表示负数;uintptr 用于底层指针相关运算 |
| 浮点数 | float32、float64 | 通常优先使用精度更高的 float64 |
| 复数 | complex64、complex128 | 实部和虚部分别使用对应精度的浮点数 |
| 字符串 | string | 不可变的字节序列,通常用于保存 UTF-8 编码的文本 |
byte 是 uint8 的别名,通常表示原始字节;rune 是 int32 的别名,通常表示 Unicode 码点。一个中文字符在 UTF-8 中可能占多个字节,因此字符串的字节数和码点数不一定相同:
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
text := "Go语言"
fmt.Println(len(text)) // 8:字节数
fmt.Println(utf8.RuneCountInString(text)) // 4:码点数
}
复合类型
基础类型可以组成更复杂的数据结构:
| 类型 | 主要用途 |
|---|---|
数组 [N]T | 保存固定数量、相同元素类型的值 |
切片 []T | 表示数组的一段动态长度视图,是最常用的序列类型 |
映射 map[K]V | 按键查找值;键类型必须可比较 |
结构体 struct | 把不同类型的字段组合为一个值 |
指针 *T | 保存变量地址,允许间接读取或修改值 |
函数 func(...) ... | 函数本身也有类型,可以赋值和传递 |
接口 interface | 描述一组方法;具体类型通过实现方法隐式满足接口 |
通道 chan T | 在 goroutine 之间传递指定类型的值并提供同步语义 |
本目录分别讲解 Array(数组)、Slice(切片)、Map(映射)、Pointer(指针) 和 Struct(结构体)。函数、接口、泛型和通道需要结合方法集、类型约束或并发语义继续学习,不应只依据这张表使用。
package main
import "fmt"
type User struct {
Name string
Scores []int
}
func main() {
user := User{Name: "Ada", Scores: []int{90, 95}}
lookup := map[string]bool{"Ada": true}
fmt.Println(user.Name, user.Scores[0], lookup[user.Name])
}
切片、映射、通道、函数、指针和接口的零值是 nil,但它们不是同一种“引用类型”。例如,向 nil 映射写入会 panic,而向 nil 切片执行 append 是合法的。应分别学习每种类型的表示和操作规则,不要只依赖“像引用一样”的笼统类比。
零值
变量声明后即使没有显式初始化,也会获得该类型的零值:
package main
import "fmt"
func main() {
var count int
var ready bool
var title string
var values []int
fmt.Printf("%d %t %q %v\n", count, ready, title, values)
}
输出如下:
0 false "" []
结构体的零值由每个字段的零值组成,数组的每个元素也会被置为零值。设计自定义类型时,可以尽量让零值具有明确且安全的含义;如果零值不可用,则应提供构造函数并清楚说明约束。
定义类型与别名
type 可以定义一个新类型。新类型与底层类型是不同的命名类型,即使底层表示相同,也不能直接互相赋值:
package main
import "fmt"
type Celsius float64
type Fahrenheit float64
func main() {
var c Celsius = 20
var f Fahrenheit = Fahrenheit(c*9/5 + 32)
fmt.Println(f)
}
定义类型可以附加方法,并阻止语义不同的数据被误混用。类型别名则使用 =,别名和原类型代表同一个类型:
type ID = int64
别名常用于兼容或迁移已有 API;如果目标是建立新的领域含义,通常应定义新类型,而不是使用别名。
类型转换
Go 使用 T(value) 进行显式转换:
package main
import "fmt"
func main() {
total := 7
count := 2
average := float64(total) / float64(count)
fmt.Println(average) // 3.5
}
转换不等于安全校验。把较大的整数转换为较窄的整数类型可能截断高位;浮点数转换为整数会舍弃小数部分。处理外部数据时,应先检查范围,再执行转换。
字符串与数字之间也不是通过普通类型转换完成解析。应使用 strconv.Atoi、strconv.ParseInt、strconv.FormatInt 等标准库函数,并处理它们返回的错误。
可比较性
类型是否可使用 == 和 != 由语言规则决定:
- 布尔值、数值、字符串、指针和通道可比较。
- 数组在元素类型可比较时可比较。
- 结构体在所有字段都可比较时可比较。
- 切片、映射和函数不能彼此比较,只能与
nil比较。 - 接口值相互比较时,如果其中保存的动态值不可比较,程序会在运行时 panic。
映射的键必须可比较,因此切片不能直接作为映射键。需要复合键时,可以使用字段均可比较的结构体。
与 TypeScript 的关键差异
- TypeScript 的类型主要用于静态检查,运行时仍由 JavaScript 值模型决定;Go 类型直接参与编译、内存表示和运行时行为。
- Go 的数值类型有明确宽度和有无符号之分,不等同于 TypeScript 常用的
number。 - Go 的
string是字节序列,按索引取得的是字节;JavaScript 字符串索引基于 UTF-16 代码单元。 - Go 的数组长度属于类型的一部分,
[2]int与[3]int是不同类型;切片才更接近常用的动态数组。 - Go 的
nil不等于 JavaScript 的null或undefined,它只适用于特定类型,并受静态类型约束。
延伸学习
- Type(类型):可赋值性、底层类型和更完整的类型关系。
- Interface(接口):动态类型、动态值、类型断言和 typed nil。
- Generic(泛型):类型参数、约束和类型推断。
小结
- 每个 Go 值都有确定类型,类型决定合法操作与赋值规则。
- 基础类型可组成数组、切片、映射、结构体、接口等复合类型。
- 未显式初始化的变量会获得零值,但不同类型的零值可用性不同。
- 定义类型用于建立新的语义和方法集合,类型别名仍代表原类型。
- 类型转换必须显式进行,并不自动提供溢出或范围保护。
- 可比较性是类型规则的一部分,切片、映射和函数不能彼此使用
==。