3Go程序设计语言

基础数据类型

数字、字符串与布尔

整数浮点字符串

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第三章 基础数据类型 (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 提供了丰富的整数类型,分为有符号和无符号两类。

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

go
var max int32 = math.MaxInt32
fmt.Println(max + 1) // -2147483648(溢出)

3.2 浮点数类型

Go 提供两种浮点数类型:float32(单精度)和 float64(双精度)。

go
var f32 float32 = 3.14
var f64 float64 = 3.141592653589793

// 浮点数字面量默认为 float64
pi := 3.14159 // float64

精度问题:浮点数不能精确表示所有十进制小数,比较时不应使用 ==:

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

特殊值:

go
inf := math.Inf(1)    // 正无穷大
nan := math.NaN()     // 非数字
fmt.Println(math.IsNaN(nan))   // true
fmt.Println(math.IsInf(inf, 0)) // true

3.3 布尔类型

Go 的 bool 类型只有两个值:true 和 false。与 C 语言不同,Go 不允许将整数与布尔值互转。

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 编码字节序列。字符串一旦创建就不能修改(但可以通过拼接创建新字符串)。

go
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()

字符串转换:

go
// 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)

原始字符串字面量:

go
// 原始字符串(反引号),不处理转义
raw := `line1
line2
\t not a tab`

// 解释字符串(双引号),处理转义
interpreted := "line1\nline2\n\ttab"

3.5 常量与 iota

Go 的常量系统在语言中独具特色。

无类型常量:无类型常量具有比任何类型化常量更高的精度(至少 256 位),只在需要时才获得类型:

go
const Pi = 3.14159265358979323846264338327950288419716939935
const Answer = 42

// 无类型常量可以赋给任何兼容类型
var f float32 = Pi  // 自动截断为 float32 精度
var d float64 = Pi  // 保留更高精度
var i int = Answer

iota 枚举器:iota 在每个 const 块中从 0 开始递增:

go
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 不允许隐式类型转换。所有转换必须显式进行。

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 字符串操作的常用模式。

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 和位运算实现权限系统。

go
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" 分别转换为对应的类型。

3/10基础数据
0%