编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

  • Java入门精通

  • Go入门到精通

    • 入门教程

      • README
      • Go简史
      • 基础语法
      • 数据类型
        • 目录介绍
        • 3.1 本章学习目标
        • 3.2 整数类型
          • 3.2.1 int 与平台依赖性
          • 3.2.2 固定宽度 int8/16/32/64
          • 3.2.3 无符号 uint 与 uintptr
          • 3.2.4 整数字面量与下划线分组
        • 3.3 浮点数类型
          • 3.3.1 float32 vs float64
          • 3.3.2 浮点比较的陷阱
          • 3.3.3 math.NaN/Inf 与 IEEE 754
        • 3.4 字符串类型
          • 3.4.1 字符串本质:只读字节序列
          • 3.4.2 byte vs rune
          • 3.4.3 字符串拼接与 strings.Builder
          • 3.4.4 原始字符串 ` `` 与转义
        • 3.5 布尔类型
        • 3.6 复数类型
        • 3.7 类型零值
        • 3.8 类型转换的严格性
          • 3.8.1 int32 不能直接给 int64
          • 3.8.2 字符串与字节切片的互转
          • 3.8.3 字符串与 rune 切片的互转
        • 3.9 自定义类型与类型别名
        • 3.10 综合示例:UTF-8 字节切片转码
        • 3.11 本章底层原理(简介)
        • 3.12 Go 新手陷阱 Top 5
          • ❌ 陷阱 1:len(s) 当字符数
          • ❌ 陷阱 2:浮点 == 比较
          • ❌ 陷阱 3:int 跨平台传输
          • ❌ 陷阱 4:string([]byte{0xFF, 0xFE}) 不是 UTF-8
          • ❌ 陷阱 5:NaN 永远不等于自己
        • 3.13 思考题
        • 3.14 推荐阅读
          • 卷内
          • 跨卷
          • 外部资料
      • 运算符
      • 复合类型
      • 流程语句
      • 函数
      • 指针与逃逸
      • 结构体与方法
      • 接口与多态
      • 错误处理
      • 并发goroutine
      • 通道channel
      • 同步sync包
      • IO和文件
      • 标准库与泛型
      • 工程化与模块
      • 特性图谱
    • 综合案例

    • 专栏博客

    • 开发技巧

  • JavaScript入门

  • CodeX
  • Go入门到精通
  • 入门教程
杨充
2026-05-21
目录

数据类型

# 第 3 章 数据类型

Go 的基础类型:固定宽度整数、浮点、字符串、rune/byte、零值、类型转换的"严格"。 关键词:int/int8/16/32/64、uint、float32/64、string、rune、byte、零值、显式转换


# 目录介绍

  • 3.1 本章学习目标
  • 3.2 整数类型
    • 3.2.1 int 与平台依赖性
    • 3.2.2 固定宽度 int8/16/32/64
    • 3.2.3 无符号 uint 与 uintptr
    • 3.2.4 整数字面量与下划线分组
  • 3.3 浮点数类型
    • 3.3.1 float32 vs float64
    • 3.3.2 浮点比较的陷阱
    • 3.3.3 math.NaN/Inf 与 IEEE 754
  • 3.4 字符串类型
    • 3.4.1 字符串本质:只读字节序列
    • 3.4.2 byte vs rune
    • 3.4.3 字符串拼接与 strings.Builder
    • 3.4.4 原始字符串 `` 与转义
  • 3.5 布尔类型
  • 3.6 复数类型
  • 3.7 类型零值
  • 3.8 类型转换的严格性
    • 3.8.1 int32 不能直接给 int64
    • 3.8.2 字符串与字节切片的互转
    • 3.8.3 字符串与 rune 切片的互转
  • 3.9 自定义类型与类型别名
  • 3.10 综合示例:UTF-8 字节切片转码
  • 3.11 本章底层原理(简介)
  • 3.12 Go 新手陷阱 Top 5
  • 3.13 思考题
  • 3.14 推荐阅读

# 3.1 本章学习目标

  • ✅ 区分 int / int32 / int64 的使用场景,知道哪个宽度依赖平台
  • ✅ 理解 string 的本质是只读字节切片,不是字符序列
  • ✅ 默写 8 大基础类型(bool/int/uint/float/complex/string/byte/rune)的零值
  • ✅ 理解为什么 Go 不允许 int32 隐式转 int64,要写显式 int64(x)
  • ✅ 区分 byte 与 rune、UTF-8 字节数与字符数
  • ✅ 写得出 string ↔ []byte ↔ []rune 三向互转代码并说明拷贝语义
  • ✅ 区分自定义类型 type X int 与类型别名 type X = int 的差异

# 3.2 整数类型

Go 的整数类型分两大组:平台依赖(int、uint、uintptr)与固定宽度(int8/16/32/64、uint8/16/32/64)。

# 3.2.1 int 与平台依赖性

int 与 uint 的宽度由 CPU 架构决定:

平台 int 宽度 uint 宽度 取值范围
32 位(armv7、386) 4 字节(32 bit) 4 字节 -2³¹ ~ 2³¹-1
64 位(amd64、arm64) 8 字节(64 bit) 8 字节 -2⁶³ ~ 2⁶³-1
package main

import (
    "fmt"
    "math"
    "unsafe"
)

func main() {
    var x int
    fmt.Printf("int 占 %d 字节,最大值 = %d\n",
        unsafe.Sizeof(x), math.MaxInt)
}
1
2
3
4
5
6
7
8
9
10
11
12
13

在 64 位 macOS / Linux 上输出:

int 占 8 字节,最大值 = 9223372036854775807
1

💡 何时该用 int、何时不该用?

  • 该用:循环计数器、len() / cap() 的返回值(标准库就这么定义的)、本地业务计数。
  • 不该用:跨网络/磁盘传输的协议字段、需要保证跨平台一致的哈希值。这些场景必须用 int64/uint64,否则同一份数据在 32/64 位机上读出不同结果。

# 3.2.2 固定宽度 int8/16/32/64

无论什么平台,宽度都固定:

类型 字节 位 范围
int8 1 8 -128 ~ 127
int16 2 16 -32 768 ~ 32 767
int32 4 32 -2 147 483 648 ~ 2 147 483 647
int64 8 64 -9.22e18 ~ 9.22e18
package main

import (
    "fmt"
    "math"
)

func main() {
    fmt.Println("int8 :", math.MinInt8, math.MaxInt8)
    fmt.Println("int16:", math.MinInt16, math.MaxInt16)
    fmt.Println("int32:", math.MinInt32, math.MaxInt32)
    fmt.Println("int64:", math.MinInt64, math.MaxInt64)
}
1
2
3
4
5
6
7
8
9
10
11
12
13

溢出是"环绕"而不是 panic——这一点和 C 一致,与 Python 的"无限大整数"不同:

var x int8 = 127
x++
fmt.Println(x) // -128(环绕)
1
2
3

如果你需要"溢出即报错",要么自己加判断,要么用 math/bits (opens new window) 的 Add64 / Mul64 等带溢出位的版本。

# 3.2.3 无符号 uint 与 uintptr

无符号整数:uint8/16/32/64、uint、uintptr。

  • uint8 = byte(别名,完全等价)
  • uintptr:能容纳一个指针的整数,用于 unsafe 包做地址运算。业务代码 99% 用不到。
var b byte = 0xFF        // 等价于 uint8
var p uintptr = uintptr(unsafe.Pointer(&b)) // 仅在 unsafe 场景用
1
2

⚠️ 不要随便用 uint。Go 官方建议除非真的需要"非负"语义(如位运算、哈希、计数器),否则一律用 int。原因:

  • uint 之间相减容易"下溢"为巨大正数:var a, b uint = 1, 2; fmt.Println(a-b) 输出 18446744073709551615。
  • 与标准库 len() / cap()(返回 int)混用要频繁转换。

# 3.2.4 整数字面量与下划线分组

Go 1.13 起字面量支持下划线分组(仅作可读性,不影响值):

const (
    Million = 1_000_000          // 一百万
    MaxU64  = 0xFFFF_FFFF_FFFF_FFFF
    BinMask = 0b1010_1010
    Octal   = 0o755              // 0o 前缀(建议替代旧的 0755)
)
1
2
3
4
5
6

四种进制前缀:

前缀 进制 例子
无 十进制 42
0b / 0B 二进制 0b1010 = 10
0o / 0O 八进制 0o755 = 493(推荐写法)
0x / 0X 十六进制 0xFF = 255

⚠️ Go 仍接受 0755 这种"裸 0"八进制,但 gofmt 与 lint 推荐 0o755,更醒目。


# 3.3 浮点数类型

Go 只提供两种浮点:float32(IEEE 754 单精度,~7 位十进制有效数字)、float64(双精度,~15-17 位有效数字)。

# 3.3.1 float32 vs float64

默认就用 float64,除非内存或带宽极度敏感(图形、音频、嵌入式)。

package main

import "fmt"

func main() {
    var a float32 = 0.1 + 0.2
    var b float64 = 0.1 + 0.2

    fmt.Printf("float32: %.20f\n", a)
    fmt.Printf("float64: %.20f\n", b)
}
1
2
3
4
5
6
7
8
9
10
11

输出:

float32: 0.30000001192092895508
float64: 0.30000000000000004441
1
2

两个都不是精确的 0.3——这是 IEEE 754 的本质,不是 Go 的 bug。

# 3.3.2 浮点比较的陷阱

❌ 千万别这样写:

if 0.1+0.2 == 0.3 { // false!
    fmt.Println("equal")
}
1
2
3

✅ 正确做法是引入"容差":

import "math"

func almostEqual(a, b, eps float64) bool {
    return math.Abs(a-b) <= eps
}

func main() {
    fmt.Println(almostEqual(0.1+0.2, 0.3, 1e-9)) // true
}
1
2
3
4
5
6
7
8
9

容差选多大要看场景:

场景 推荐 eps
一般业务计算 1e-9
图形 / 物理仿真 1e-6
金融计算 不要用浮点,用整数("分"为单位)或 math/big (opens new window)

# 3.3.3 math.NaN/Inf 与 IEEE 754

浮点的三种"特殊值",Go 都遵循 IEEE 754:

package main

import (
    "fmt"
    "math"
)

func main() {
    posInf := math.Inf(+1)            // +∞
    negInf := math.Inf(-1)            // -∞
    nan := math.NaN()                 // NaN

    fmt.Println(1.0/0.0 == posInf)    // 编译错:除零是常量除零
    var zero float64                  // = 0
    fmt.Println(1.0/zero == posInf)   // true,运行期除零 = +Inf

    fmt.Println(nan == nan)           // false!NaN 不等于自己
    fmt.Println(math.IsNaN(nan))      // true,必须用 IsNaN 判断
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

关键事实:

  • NaN != NaN——意味着用 map[float64]X 存浮点 key 时,NaN 永远查不到。
  • 浮点除零不 panic,得到 ±Inf 或 NaN;整数除零会 panic。

# 3.4 字符串类型

Go 的 string 是把"字符串"和"字节流"统一起来的类型,深刻理解它是写好 Go 的基石。

# 3.4.1 字符串本质:只读字节序列

string 在底层是一个只读、不可变的字节切片——可以用一个二元组表达:

string = (ptr *byte, len int)   // 16 字节(64 位平台)
1
package main

import (
    "fmt"
    "unsafe"
)

func main() {
    s := "hello"
    fmt.Println(unsafe.Sizeof(s)) // 16(在 64 位机器上)
    fmt.Println(len(s))            // 5
    fmt.Println(s[0])              // 104,即字节 'h'
    // s[0] = 'H' // ❌ 编译错:字符串不可变
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

💡 字符串 len() 返回的是字节数,不是字符数:

s := "你好"
fmt.Println(len(s)) // 6(每个汉字 UTF-8 占 3 字节)
1
2

# 3.4.2 byte vs rune

类型 别名 含义 用途
byte uint8 一个字节 处理二进制 / ASCII
rune int32 一个 Unicode 码点 处理"一个字符"

Go 源码默认 UTF-8 编码,字符串字面量也是 UTF-8 字节流:

package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    s := "Go语言"

    fmt.Println(len(s))                     // 8 = 2(Go) + 3(语) + 3(言)
    fmt.Println(utf8.RuneCountInString(s))  // 4

    // 按字节遍历
    for i := 0; i < len(s); i++ {
        fmt.Printf("%d ", s[i])
    }
    fmt.Println()

    // 按 rune 遍历(推荐处理 Unicode 文本时用)
    for i, r := range s {
        fmt.Printf("byte 偏移 %d: %c (%U)\n", i, r, r)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

输出:

8
71 111 232 175 173 232 168 128 
byte 偏移 0: G (U+0047)
byte 偏移 1: o (U+006F)
byte 偏移 2: 语 (U+8BED)
byte 偏移 5: 言 (U+8A00)
1
2
3
4
5
6

注意:for i, r := range s 中 i 是字节偏移而不是字符序号——这是 Go 字符串的核心规则。

# 3.4.3 字符串拼接与 strings.Builder

字符串不可变意味着每次 += 都要分配新内存并拷贝。少量拼接没事,大量循环里就会成为性能杀手。

// ❌ 慢:N 次拼接 = O(N²) 字节拷贝
s := ""
for i := 0; i < 10000; i++ {
    s += "a"
}

// ✅ 推荐:strings.Builder(内部维护 []byte 缓冲)
var b strings.Builder
b.Grow(10000) // 可选:预分配避免扩容
for i := 0; i < 10000; i++ {
    b.WriteByte('a')
}
s := b.String()
1
2
3
4
5
6
7
8
9
10
11
12
13

不同方式的性能对照(10 000 次拼接,单核 amd64 实测量级):

方式 耗时数量级 何时用
s += t 几十毫秒 极少量、固定次数
strings.Join 微秒级 已有 []string,一次性合并
strings.Builder 微秒级 循环拼接(最常用)
bytes.Buffer 微秒级 需要 io.Writer 兼容场景
fmt.Sprintf 较慢 需要格式化时再用

# 3.4.4 原始字符串 ` 与转义

Go 提供两种字符串字面量:

// 1. 解释字符串(双引号):转义字符生效
s1 := "line1\nline2\t\"quoted\""

// 2. 原始字符串(反引号):所见即所得,不解析转义
s2 := `line1\nline2\t"quoted"`
fmt.Println(s2) // line1\nline2\t"quoted"
1
2
3
4
5
6

原始字符串特别适合:

  • 正则表达式:regexp.MustCompile(\d+.\d+),省去 \\d+\\.\\d+ 的双反斜杠
  • 多行 SQL / JSON 模板
  • Windows 路径:`C:\Users\foo`

唯一不能放进原始字符串的就是 ` 自身,需要时只能拆开拼接。


# 3.5 布尔类型

bool 只有 true / false 两个取值,零值是 false。Go 的布尔与整数完全不可互转:

var b bool = true
var i int = int(b)  // ❌ 编译错:cannot convert b (type bool) to type int
if 1 { }            // ❌ 编译错:non-bool 1 used as if condition
1
2
3

要在 bool 与 int 之间转换,写函数:

func boolToInt(b bool) int {
    if b {
        return 1
    }
    return 0
}
1
2
3
4
5
6

这种"明确大于隐式"的设计避免了 C 里 if (x = 0) 这种"等于打成赋值"的经典 bug——Go 编译器会因为 x = 0 不是 bool 直接拒绝。


# 3.6 复数类型

Go 内置两种复数:complex64(实部虚部各 float32)、complex128(实部虚部各 float64)。

package main

import (
    "fmt"
    "math/cmplx"
)

func main() {
    c := complex(3, 4)               // 3 + 4i,类型 complex128
    fmt.Println(real(c), imag(c))    // 3 4
    fmt.Println(cmplx.Abs(c))        // 5
}
1
2
3
4
5
6
7
8
9
10
11
12

实际工程里很少用——只在数字信号处理、傅里叶变换、特定数学题里出现。一般业务代码可以忽略这一节。


# 3.7 类型零值

Go 的所有变量声明后自动初始化为零值——这是它"安全"的核心特征之一,避免了 C 里 int x; printf("%d", x); 打印随机内存的灾难。

类型 零值
bool false
数值类型(int / float / complex 全部) 0
string ""(空串,非 nil)
指针 / 接口 / 函数 / channel / map / slice nil
数组 每个元素的零值
结构体 每个字段的零值
type Person struct {
    Name string
    Age  int
}

var p Person
fmt.Printf("%+v\n", p) // {Name: Age:0}
1
2
3
4
5
6
7

💡 零值即可用是 Go 的设计风格——var b strings.Builder、var m sync.Mutex、var wg sync.WaitGroup 都不需要额外初始化就能直接用。这是 Go 与 Java(必须 new)的关键区别。


# 3.8 类型转换的严格性

Go 没有隐式数值转换——任意两种不同类型间都要写显式 T(x)。这是 Go 与 C / Java 最大的语法差异之一。

# 3.8.1 int32 不能直接给 int64

var a int32 = 100
var b int64 = a       // ❌ 编译错:cannot use a (type int32) as type int64
var c int64 = int64(a) // ✅ 显式转换
1
2
3

即使是 int 与 int64:

var x int = 1
var y int64 = x       // ❌ 也不行(哪怕 64 位机上 int 实际就是 int64)
var y int64 = int64(x) // ✅
1
2
3

设计哲学:宁可让程序员多敲键盘,也不让"自动扩展"或"自动截断"埋藏 bug。

截断与符号扩展:

var big int32 = 0x12345678
var small int8 = int8(big) // = 0x78 = 120(直接砍掉高位字节)

var neg int8 = -1          // 0xFF(补码)
var u uint16 = uint16(neg) // = 0xFFFF(符号扩展)
1
2
3
4
5

转换规则与 C 一致:缩小取低位,扩大按符号扩展。

# 3.8.2 字符串与字节切片的互转

s := "hello"

// string -> []byte:拷贝一份,得到的 []byte 可改
b := []byte(s)
b[0] = 'H'
fmt.Println(string(b)) // Hello
fmt.Println(s)         // hello(原字符串没受影响)

// []byte -> string:同样拷贝
s2 := string(b)
b[0] = 'X'
fmt.Println(s2) // 还是 Hello
1
2
3
4
5
6
7
8
9
10
11
12

关键:两种转换都会拷贝底层字节,不是零拷贝。这保证了 string 的不可变性。

💡 想避免拷贝?用 unsafe 可以,但属于"违反约定"——只在性能极敏感场景下,且必须 100% 确定使用范围。卷四会专门讲这种黑魔法。

# 3.8.3 字符串与 rune 切片的互转

如果要操作"一个字符"而不是"一个字节",转 []rune:

s := "Go语言"
rs := []rune(s)

fmt.Println(len(s))   // 8(字节数)
fmt.Println(len(rs))  // 4(字符数)

rs[2] = '编'
rs[3] = '程'
fmt.Println(string(rs)) // Go编程
1
2
3
4
5
6
7
8
9

转换代价:[]rune(s) 要遍历整个字符串做 UTF-8 解码,不是 O(1)——长字符串频繁转换有性能损失。

三种切片表示对照:

字符串:"Go语言"
   字节切片:[0x47 0x6F 0xE8 0xAF 0xAD 0xE8 0xA8 0x80]   8 个 byte
   rune切片:[0x47 0x6F 0x8BED 0x8A00]                    4 个 rune
1
2
3

# 3.9 自定义类型与类型别名

Go 提供两种"取新名字"的语法,含义完全不同:

// 1. 自定义类型(type definition):产生新类型,与原类型不可隐式互换
type UserID int

// 2. 类型别名(type alias,Go 1.9+):仅是同一类型的另一个名字
type MyInt = int
1
2
3
4
5

对照测试:

package main

import "fmt"

type UserID int   // 新类型
type MyInt = int  // 别名

func main() {
    var u UserID = 1
    var i int = 2
    var m MyInt = 3

    // u + i  // ❌ 编译错:mismatched types UserID and int
    _ = int(u) + i // ✅ 显式转换可以

    _ = i + m  // ✅ 别名就是同一个类型,自由相加
    fmt.Println(u, i, m)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
语法 是否新类型 能否定义方法 主要用途
type X int(无 =) 新类型 ✅ 能 业务建模、类型安全(区分 UserID / OrderID)
type X = int(有 =) 同一类型 ❌ 不行(方法在原类型上) 重构 / 迁移过渡(byte = uint8、any = interface{})

经典应用:自定义类型 + 方法 = 类型安全枚举

type Status int

const (
    StatusPending Status = iota
    StatusRunning
    StatusDone
)

func (s Status) String() string {
    return [...]string{"Pending", "Running", "Done"}[s]
}

func main() {
    s := StatusRunning
    fmt.Println(s) // Running(fmt 自动调用 String)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

经典应用:类型别名做大重构

Go 1.9 引入别名的直接动机就是支持 Go 标准库内部把 bytes.Buffer 之类的类型搬家时不破坏外部代码。byte = uint8、rune = int32、any = interface{}(Go 1.18+)都是别名。


# 3.10 综合示例:UTF-8 字节切片转码

把本章知识串起来,写一个小工具:从命令行读入一段文本,输出每个字符的 UTF-8 字节序、码点、字节数。

// utf8inspector/main.go
package main

import (
    "fmt"
    "os"
    "strings"
    "unicode/utf8"
)

func main() {
    if len(os.Args) < 2 {
        fmt.Println("用法: utf8inspector <文本>")
        os.Exit(1)
    }
    s := strings.Join(os.Args[1:], " ")

    fmt.Printf("原文: %q\n", s)
    fmt.Printf("字节数: %d\n", len(s))
    fmt.Printf("字符数: %d\n", utf8.RuneCountInString(s))
    fmt.Println(strings.Repeat("-", 50))
    fmt.Printf("%-6s %-6s %-10s %-12s %s\n",
        "序号", "偏移", "码点", "字节序列", "字符")

    idx := 0
    for byteOffset, r := range s {
        size := utf8.RuneLen(r)
        bytes := []byte(string(r))
        fmt.Printf("%-6d %-6d U+%04X     % -12X %c\n",
            idx, byteOffset, r, bytes, r)
        idx++
        _ = size
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

跑一下:

$ go mod init example.com/utf8inspector
$ go run . "Go 语言 🚀"
原文: "Go 语言 🚀"
字节数: 14
字符数: 7
--------------------------------------------------
序号   偏移   码点       字节序列     字符
0      0     U+0047     47           G
1      1     U+006F     6F           o
2      2     U+0020     20            
3      3     U+8BED     E8 AF AD     语
4      6     U+8A00     E8 A8 80     言
5      9     U+0020     20            
6      10    U+1F680    F0 9F 9A 80  🚀
1
2
3
4
5
6
7
8
9
10
11
12
13
14

这个例子用上了:

章节知识点 在哪一行体现
字符串本质 = 字节序列 len(s)
rune = Unicode 码点 for _, r := range s
字节偏移而非字符序号 byteOffset
string ↔ []byte 互转 []byte(string(r))
utf8 标准库 RuneCountInString / RuneLen

# 3.11 本章底层原理(简介)

string 的内存结构(runtime/string.go: type stringStruct)

┌──────────────┬──────────────┐
│   *byte      │     int      │   16 字节(64 位平台)
│   数据指针    │   字节长度    │
└──────────────┴──────────────┘
       │
       ▼
┌──┬──┬──┬──┬──┬──┬──┬──┐
│ G│ o│  │ 语 (3B)  │ 言 (3B) │   只读区(编译期常量)或堆(运行期构造)
└──┴──┴──┴──┴──┴──┴──┴──┘
1
2
3
4
5
6
7
8
9
10
11

更深的内容留到卷三第 4 章「字符串与切片底层」详讲:

  • 字符串字面量在 ELF / Mach-O 的只读段(.rodata),多个 goroutine 共享同一份
  • string([]byte) 编译器在某些场景能消除拷贝(如 m[string(b)] 查 map)
  • unsafe.String / unsafe.StringData(Go 1.20+)允许零拷贝重解释

本章你只需记住:字符串是不可变的字节序列,拷贝便宜,遍历用 rune。


# 3.12 Go 新手陷阱 Top 5

# ❌ 陷阱 1:len(s) 当字符数

s := "你好"
fmt.Println(len(s)) // 6,不是 2
1
2

修复:用 utf8.RuneCountInString(s) 或 len([]rune(s))。

# ❌ 陷阱 2:浮点 == 比较

if 0.1+0.2 == 0.3 { ... } // 永远 false
1

修复:用 math.Abs(a-b) < eps;金融场景根本不要用 float,用整数(分)或 math/big。

# ❌ 陷阱 3:int 跨平台传输

// 32 位机产出
var hash int = 0x80000000  // 在 64 位机上是正数,32 位机上溢出为负
binary.Write(w, binary.BigEndian, hash) // 跨机器读出宽度不同!
1
2
3

修复:协议字段、文件格式、哈希值一律用 int32/int64 等固定宽度。

# ❌ 陷阱 4:string([]byte{0xFF, 0xFE}) 不是 UTF-8

b := []byte{0xFF, 0xFE}
s := string(b) // 不是 UTF-8 序列,但不会报错
fmt.Println(utf8.ValidString(s)) // false
1
2
3

string(b) 不做 UTF-8 校验,"任何字节"都能塞进去。处理外部输入时记得 utf8.Valid() 检查。

# ❌ 陷阱 5:NaN 永远不等于自己

nan := math.NaN()
fmt.Println(nan == nan)  // false
m := map[float64]int{nan: 1}
fmt.Println(m[nan])      // 0(查不到)
1
2
3
4

修复:用 math.IsNaN(x) 判断;不要用 float 做 map key。


# 3.13 思考题

  1. 为什么 Go 设计成 len(s) 返回字节数而不是字符数?这种设计在性能与易用性上的取舍是什么?
  2. 写一段代码:输入一个字符串,输出它在 UTF-8、UTF-16(big-endian)、UTF-32 下分别占几字节。
  3. 为什么 var i int; var j int64 = i 编译不过,但 const i = 1; var j int64 = i 能过?请用 Go 规范的"无类型常量"概念解释。
  4. 自己实现一个安全相加 func AddInt32(a, b int32) (int32, error),溢出时返回 error。提示:用 math/bits.Add32 或自己判符号位。
  5. type UserID int 与 type UserID = int 的差别?请写两个版本的 Save(u UserID) 函数,演示前者能阻止误传普通 int、后者不能。
  6. 为什么 Go 字符串可以做 map 的 key 而 slice 不行?请联系第 5 章「复合类型」预先思考。

# 3.14 推荐阅读

# 卷内

  • 第 2 章 基础语法与 Hello World
  • 第 4 章 运算符(位运算、为什么没有指针算术)
  • 第 5 章 复合类型(slice/map/struct 与字符串的呼应)

# 跨卷

  • 卷三第 4 章 字符串与切片底层(深挖 stringStruct 与零拷贝)
  • 卷四第 9 章 性能优化(字符串拼接基准测试与逃逸)

# 外部资料

  • Go 官方文档:Types (opens new window)
  • Strings, bytes, runes and characters in Go (opens new window) — 官方博客必读
  • The Go Programming Language Specification: Constants (opens new window)(无类型常量规则)
  • unicode/utf8 包文档 (opens new window)
上次更新: 2026/06/10, 11:13:41
基础语法
运算符

← 基础语法 运算符→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式