第三章 基础数据类型 (Basic Data Types)
一、导读
1.1 本章学习目标
掌握 Go 的整数类型体系:int、int8、int16、int32、int64 及其无符号对应类型
理解浮点数类型 float32 和 float64 的精度差异和使用场景
掌握布尔类型 bool 的特性,理解 Go 中布尔值不能与其他类型互转
深入理解 Go 的字符串(string)类型:不可变性、UTF-8 编码、字符串操作
掌握 rune 和 byte 类型的含义及其与字符串的关系
理解 Go 的常量系统:无类型常量、iota 枚举和常量表达式
掌握类型转换的规则和限制,理解 Go 为什么不允许隐式类型转换
能够根据场景选择合适的数据类型并正确处理类型转换
1.2 为什么本章如此重要
数据类型是编程语言的原子——所有复杂的数据结构都由基础类型组合而成。Go 的基础类型设计体现了其"显式优于隐式"的核心理念。与 C/C++ 允许隐式类型转换不同,Go 要求所有类型转换必须显式进行,这消除了大量因隐式转换导致的 Bug。
字符串处理是 Go 的强项之一。Go 的 string 类型是 UTF-8 编码的只读字节序列,标准库提供了丰富的 Unicode 处理功能。理解 Go 字符串的底层表示对于正确处理国际化文本至关重要——len(s) 返回的是字节数而非字符数,s[i] 访问的是字节而非字符,这些特性在初学者看来可能违反直觉,但它们是正确的 Unicode 处理方式。
Go 的常量系统也有独到之处。无类型常量(Untyped Constants)具有比类型化常量更高的精度,常量表达式在编译期求值。iota 枚举器提供了一种简洁的方式定义递增常量序列。这些特性使得 Go 的常量系统既灵活又安全。
1.3 核心问题引导
Go 的 int 类型在 32 位和 64 位平台上有什么区别?为什么 Go 不固定 int 的大小?
为什么 len("中文") 返回 6 而不是 2?Go 是如何处理 UTF-8 编码的?
rune 和 byte 分别是什么类型的别名?它们在字符串处理中扮演什么角色?
Go 为什么不允许 int 和 int64 直接运算?这种设计的优势是什么?
iota 的工作原理是什么?如何利用 iota 实现非递增的常量序列?
浮点数比较为什么不能直接用 ==?在 Go 中应该如何比较浮点数?
二、核心概念详解
3.1 整数类型
Go 提供了丰富的整数类型,分为有符号和无符号两类。
// 有符号整数
var i int // 32位或64位(平台相关)
var i8 int8 // -128 到 127
var i16 int16 // -32768 到 32767
var i32 int32 // -2^31 到 2^31-1
var i64 int64 // -2^63 到 2^63-1
// 无符号整数
var u uint // 32位或64位
var u8 uint8 // 0 到 255
var u16 uint16
var u32 uint32
var u64 uint64
// 特殊整数类型
var b byte // uint8 的别名
var r rune // int32 的别名,表示 Unicode 码点`int` 的平台相关性:在 64 位平台上,int 是 64 位的;在 32 位平台上,int 是 32 位的。Go 的设计选择是:除非有明确的大小需求,否则使用 int。
整数溢出:Go 的整数运算在溢出时不会 panic,而是静默截断(wrap around):
var max int32 = math.MaxInt32
fmt.Println(max + 1) // -2147483648(溢出)3.2 浮点数类型
Go 提供两种浮点数类型:float32(单精度)和 float64(双精度)。
var f32 float32 = 3.14
var f64 float64 = 3.141592653589793
// 浮点数字面量默认为 float64
pi := 3.14159 // float64精度问题:浮点数不能精确表示所有十进制小数,比较时不应使用 ==:
func floatEqual(a, b, epsilon float64) bool {
diff := a - b
if diff < 0 {
diff = -diff
}
return diff < epsilon
}
// 使用 math 包
func floatEqual2(a, b float64) bool {
return math.Abs(a-b) < 1e-9
}特殊值:
inf := math.Inf(1) // 正无穷大
nan := math.NaN() // 非数字
fmt.Println(math.IsNaN(nan)) // true
fmt.Println(math.IsInf(inf, 0)) // true3.3 布尔类型
Go 的 bool 类型只有两个值:true 和 false。与 C 语言不同,Go 不允许将整数与布尔值互转。
var active bool = true
var disabled = false
// 以下都是编译错误
// var b bool = 1 // 不能将 int 赋给 bool
// var i int = true // 不能将 bool 赋给 int
// if 1 { } // if 条件必须是 bool 类型3.4 字符串深入
Go 的 string 是不可变的 UTF-8 编码字节序列。字符串一旦创建就不能修改(但可以通过拼接创建新字符串)。
s := "Hello, 世界"
// 字节长度
fmt.Println(len(s)) // 13(7 ASCII + 2×3 UTF-8 字节)
// 按字节访问
fmt.Println(s[0]) // 72('H' 的 ASCII 值)
// 按字符遍历(rune)
for i, r := range s {
fmt.Printf("%d: %c (%U)\n", i, r, r)
}
// 字符串切片(按字节)
fmt.Println(s[0:5]) // "Hello"
// 字符串拼接
s1 := "Hello"
s2 := "World"
s3 := s1 + ", " + s2
// 高效拼接(大量字符串)
var builder strings.Builder
for i := 0; i < 1000; i++ {
builder.WriteString("word ")
}
result := builder.String()字符串转换:
// string 与 []byte
s := "hello"
b := []byte(s)
s2 := string(b)
// string 与 []rune
s := "Hello, 世界"
runes := []rune(s)
fmt.Println(len(runes)) // 9(字符数)
s2 := string(runes)
// 数字与字符串
str := strconv.Itoa(42) // int -> string
num, _ := strconv.Atoi("42") // string -> int
// 格式化
s := fmt.Sprintf("Name: %s, Age: %d", "Alice", 30)原始字符串字面量:
// 原始字符串(反引号),不处理转义
raw := `line1
line2
\t not a tab`
// 解释字符串(双引号),处理转义
interpreted := "line1\nline2\n\ttab"3.5 常量与 iota
Go 的常量系统在语言中独具特色。
无类型常量:无类型常量具有比任何类型化常量更高的精度(至少 256 位),只在需要时才获得类型:
const Pi = 3.14159265358979323846264338327950288419716939935
const Answer = 42
// 无类型常量可以赋给任何兼容类型
var f float32 = Pi // 自动截断为 float32 精度
var d float64 = Pi // 保留更高精度
var i int = Answeriota 枚举器:iota 在每个 const 块中从 0 开始递增:
const (
Sunday = iota // 0
Monday // 1
Tuesday // 2
Wednesday // 3
Thursday // 4
Friday // 5
Saturday // 6
)
// 跳过值
const (
Read = 1 << iota // 1
Write // 2
Execute // 4
)
// 自定义递增
const (
KB = 1000
MB = 1000 * KB
GB = 1000 * MB
TB = 1000 * GB
)
// 重复上一个值
const (
A = iota // 0
B // 1
_ // 2(跳过)
D // 3
)3.6 类型转换
Go 不允许隐式类型转换。所有转换必须显式进行。
// 数值类型转换
var i int = 42
var f float64 = float64(i)
var u uint = uint(f)
// 不同大小的 int 之间
var i32 int32 = 100
var i64 int64 = int64(i32)
// 字符串与字节
s := "hello"
b := []byte(s)
s2 := string(b)
// 接口类型转换(类型断言)
var iface interface{} = "hello"
str := iface.(string)类型转换的安全性:数值类型转换可能丢失精度或溢出。字符串到数字的转换可能失败,返回 error。
三、深入分析与讨论
3.1 UTF-8 编码与字符串处理
Go 选择 UTF-8 作为字符串的默认编码是一个深思熟虑的决定。UTF-8 是 ASCII 的超集,纯 ASCII 文本在 UTF-8 中与 ASCII 完全相同。UTF-8 是变长编码:ASCII 字符占 1 字节,拉丁字符占 2 字节,中日韩字符占 3 字节,其他字符占 4 字节。
这意味着 len(s) 返回的是字节数而非字符数。要获取字符数,需要将字符串转换为 []rune:len([]rune(s))。遍历字符应使用 for i, r := range s,它会自动按 UTF-8 解码。
3.2 Go 为什么没有隐式类型转换
Go 刻意禁止隐式类型转换,这与 C/C++ 形成了鲜明对比。在 C 中,int + float 会自动将 int 提升为 float,这种隐式转换经常导致难以发现的 Bug。Go 要求开发者显式写出转换意图,使得代码更加清晰和安全。
这种设计的代价是代码略显冗长——你需要写 float64(i) 而不是直接 i。但 Go 团队认为,这种微小的不便远小于隐式转换带来的风险。
3.3 常量精度与编译期计算
Go 的无类型常量在编译期具有任意精度(至少 256 位),这使得常量表达式可以在编译期进行精确计算,不会丢失精度。只有当常量被赋给变量或用于需要特定类型的上下文时,才会被截断为该类型的精度。
四、实践案例
4.1 字符串处理工具
实现一个字符串处理工具,展示 Go 字符串操作的常用模式。
package main
import (
"fmt"
"strings"
"unicode"
)
func reverseString(s string) string {
runes := []rune(s)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
return string(runes)
}
func countChars(s string) (letters, digits, spaces, others int) {
for _, r := range s {
switch {
case unicode.IsLetter(r):
letters++
case unicode.IsDigit(r):
digits++
case unicode.IsSpace(r):
spaces++
default:
others++
}
}
return
}
func capitalizeWords(s string) string {
return strings.Title(s)
}
func main() {
s := "Hello, 世界! 123"
fmt.Println("原始:", s)
fmt.Println("反转:", reverseString(s))
fmt.Println("字符统计:")
l, d, sp, o := countChars(s)
fmt.Printf(" 字母: %d, 数字: %d, 空格: %d, 其他: %d\n", l, d, sp, o)
}4.2 常量枚举与位掩码
使用 iota 和位运算实现权限系统。
package main
import "fmt"
type Permission uint32
const (
PermRead Permission = 1 << iota // 1
PermWrite // 2
PermExecute // 4
PermDelete // 8
PermAdmin Permission = PermRead | PermWrite | PermExecute | PermDelete
)
func (p Permission) Has(perm Permission) bool {
return p&perm == perm
}
func (p Permission) Add(perm Permission) Permission {
return p | perm
}
func (p Permission) Remove(perm Permission) Permission {
return p &^ perm
}
func (p Permission) String() string {
perms := []string{}
if p.Has(PermRead) { perms = append(perms, "Read") }
if p.Has(PermWrite) { perms = append(perms, "Write") }
if p.Has(PermExecute) { perms = append(perms, "Execute") }
if p.Has(PermDelete) { perms = append(perms, "Delete") }
return strings.Join(perms, "|")
}
func main() {
var userPerm Permission = PermRead | PermWrite
fmt.Println("用户权限:", userPerm)
userPerm = userPerm.Add(PermExecute)
fmt.Println("添加执行权限:", userPerm)
fmt.Println("有删除权限?", userPerm.Has(PermDelete))
}五、常见误区与注意事项
`len()` 对字符串返回字节数:len("中文") 返回 6(每个中文字符 3 字节),不是 2。获取字符数使用 len([]rune(s))。
字符串不可变:s[0] = 'H' 会导致编译错误。字符串是不可变的,要"修改"字符串需要创建新字符串。
隐式类型转换不存在:var f float64 = 42 是编译错误,必须写 float64(42)。不同大小的整数之间也不能直接运算。
`int` 的平台相关性:在 64 位系统上 int 是 64 位,在 32 位系统上是 32 位。如果需要固定大小,使用 int32 或 int64。
浮点数比较:不要用 == 比较浮点数。使用 math.Abs(a-b) < epsilon 或 sort.Float64s 等安全方法。
六、本章小结
本章深入讲解了 Go 的基础数据类型。整数类型体系包括平台相关的 int 和固定大小的 int8/int16/int32/int64。浮点数分为 float32 和 float64,比较时需注意精度问题。字符串是不可变的 UTF-8 字节序列,理解字节与字符(rune)的区别至关重要。Go 的常量系统通过无类型常量和 iota 提供了灵活而精确的常量定义方式。类型转换必须显式进行,这是 Go "显式优于隐式"理念的体现。
七、思考题与练习
编写函数将字符串中的每个单词首字母大写,不使用 strings.Title。
解释为什么 int32 和 int64 是不同的类型。如果 Go 允许它们直接运算,可能导致什么问题?
使用 iota 定义一年的 12 个月份常量。然后定义文件大小单位(B、KB、MB、GB、TB)。
编写函数统计一段文本中每个 Unicode 字符出现的次数。使用 map[rune]int 存储结果。
实现一个函数,将整数转换为二进制字符串表示(不使用 fmt.Sprintf)。
解释 string 的不可变性。为什么 Go 选择让字符串不可变?这种设计有什么优势?
编写程序验证 float32 和 float64 的精度差异。计算 1.0/3.0 在两种精度下的结果。
研究 strconv 包的常用函数。实现一个程序,将 "123"、"3.14"、"true" 分别转换为对应的类型。