Skip to content

Go 基本数据类型:数值、布尔、字符与字符串

面向有 Java 经验的开发者,基于 Go 1.26。

Go 的基本数据类型并不难记,真正容易出错的是边界:int 在当前机器上占多少位,1.0 为什么是 float64'中' 保存了什么,len("中国") 为什么是 6,金额又为什么不能直接用浮点数判断相等。这些问题单独看都不大,一旦进入协议、数据库或并发服务,就可能变成很难排查的 bug。

因此,这一章不会从一张需要背诵的类型表开始。我们先看 Go 的类型和值模型,再逐项讨论取值范围、字面量、运算、转换和工程用法。遇到 JSON 精度丢失、整数溢出或 Unicode 截断时,也可以直接回到对应小节查边界。

目录

1. Go 有哪些基本类型

Go 规范把预声明的基本类型分为三组:

布尔类型

go
bool

数值类型

go
// 有符号整数
int
int8
int16
int32
int64

// 无符号整数
uint
uint8
uint16
uint32
uint64
uintptr

// 浮点数
float32
float64

// 复数
complex64
complex128

另外有两个别名:

go
byte // uint8 的别名
rune // int32 的别名

字符串类型

go
string

有些教程会把“字符型”单列为基本类型,这种说法不够准确。Go 没有独立的 char 类型:

  • byteuint8 的别名,适合表达一个原始字节;
  • runeint32 的别名,适合表达一个 Unicode 码点;
  • 单引号写出来的是 rune literal,也就是一个整数常量。

指针、数组、slice、map、struct、function、interface、channel 属于复合类型或其他类型类别,会在后续章节展开。

2. 预声明类型、别名与定义类型

2.1 预声明不等于关键字

intstringbool 等名字是在 universe block 中预声明的标识符,不是语法关键字。理论上可以遮蔽:

go
func f() {
	string := "不推荐"
	fmt.Println(string)
}

这样写会遮蔽预声明的 string,当前作用域内便无法再正常使用这个类型。代码虽然可以解释,阅读成本却没有必要,工程中不要用基本类型名作为变量名。

2.2 byterune 是别名

规范中的关系是:

go
type byte = uint8
type rune = int32

别名两边是同一个类型:

go
var b byte = 65
var u uint8 = b // 不需要转换

var r rune = ''
var i int32 = r // 不需要转换

选哪个名字取决于语义:

  • 处理文件、网络、哈希、二进制协议:byte
  • 处理 Unicode 码点:rune
  • 单纯 8 位或 32 位整数:uint8int32

类型名本身就在传递语义。虽然底层表示相同,[]byte[]uint8 更容易让读者看出“这是一段原始数据”。

2.3 定义类型不是别名

go
type UserID int64
type OrderID int64

UserIDOrderIDint64 是三个不同的定义类型:

go
var uid UserID = 10
var oid OrderID = 10

// uid = oid          // 编译错误
uid = UserID(oid)     // 显式转换
raw := int64(uid)

定义类型可以增加方法:

go
type UserID int64

func (id UserID) Valid() bool {
	return id > 0
}

它的目的与 Java 使用 value object 包装 long 相近,但 Go 的定义类型通常不需要额外的对象分配。编译器会直接阻止调用方把订单 ID 误传成用户 ID。

2.4 自定义别名

go
type ID = int64

IDint64 完全相同,不能在 ID 上定义只属于它的新方法。别名主要用于 API 迁移、兼容旧名字,不适合用来建立新的领域边界。

2.5 底层类型

每个定义类型都有 underlying type:

go
type UserID int64       // 底层类型 int64
type Name string        // 底层类型 string
type Enabled bool       // 底层类型 bool

底层类型决定允许的运算和显式转换,也决定泛型中 ~int64 这类约束的含义。

3. 字面量、常量与默认类型

3.1 字面量最初可能没有具体类型

go
42
3.14
true
''
"hello"
1 + 2i

这些字面量在常量表达式中通常是无类型常量。它们不是一出现就立刻塞进某种固定宽度的运行期存储。

go
const Big = 1 << 100 // 合法,常量可以拥有很高精度

只有进入需要具体类型的上下文时,才检查能否表示:

go
var x uint64 = 1 << 63 // 合法
// var y uint64 = 1 << 100 // 编译错误:超出 uint64

3.2 默认类型

没有其他类型信息时,无类型常量使用默认类型:

常量类别默认类型
布尔常量bool
rune 常量rune,即 int32
整数常量int
浮点常量float64
复数常量complex128
字符串常量string
go
i := 42       // int
f := 3.14     // float64
c := 1 + 2i   // complex128
b := true     // bool
r := ''     // rune / int32
s := "hello"  // string

%T 可以确认:

go
fmt.Printf("%T %T %T %T %T %T\n", i, f, c, b, r, s)

3.3 上下文可以改变落地类型

go
var a int8 = 42
var b float32 = 3.14
var c complex64 = 1 + 2i

常量值能被目标类型精确或按规则表示时,赋值合法。变量之间则没有这种宽松的隐式转换:

go
var x int8 = 42
// var y int64 = x // 编译错误
var y int64 = int64(x)

4. 整数类型

4.1 固定宽度整数

类型位数最小值最大值
int88-128127
uint8 / byte80255
int1616-32,76832,767
uint1616065,535
int32 / rune32-2,147,483,6482,147,483,647
uint323204,294,967,295
int6464-9,223,372,036,854,775,8089,223,372,036,854,775,807
uint6464018,446,744,073,709,551,615

对于 n 位整数:

text
无符号:0 到 2ⁿ - 1
有符号:-2ⁿ⁻¹ 到 2ⁿ⁻¹ - 1

Go 的 n 位整数使用补码运算。不能简单理解成“有符号整数拿出一位不用来存数据”。最高位参与补码表示,因此 int8 有 128 个负数、127 个正数和一个零。

4.2 intuint

go
int
uint

它们在规范中是 32 位或 64 位,并且宽度相同,取决于目标架构,不是操作系统界面显示“64 位”就绝对可以推断所有构建目标都是 64 位。

查看当前编译目标:

go
fmt.Println(strconv.IntSize) // 32 或 64
fmt.Println(math.MinInt)
fmt.Println(math.MaxInt)
fmt.Println(math.MaxUint)

一般业务计数、slice 索引、len 结果和循环变量优先使用 int

go
for i := 0; i < len(items); i++ {
	// i 和 len(items) 都是 int
}

这些场景使用 int64 反而会产生大量转换。

固定宽度场景使用明确类型:

  • 网络协议字段;
  • 文件格式;
  • 数据库列和跨语言接口;
  • 哈希或位运算算法;
  • 必须跨架构保持布局的数据。

4.3 uintptr

uintptr 是足以保存指针地址“无解释位模式”的无符号整数。它主要服务于 unsafe、系统调用和底层运行时:

go
var address uintptr

它不是普通指针:

  • GC 不会把 uintptr 当作对象引用;
  • 它不会阻止对象被回收;
  • 即使将来 GC 移动物体,也不会自动更新它;
  • 从指针转成 uintptr 后长期保存再转回通常不安全。

业务 ID、文件大小、内存长度都不该因为“看起来很大”而使用 uintptr

4.4 用标准常量表示范围

不要用浮点函数计算整数上限:

go
// 不推荐:math.Pow 返回 float64
limit := math.Pow(2, 63) - 1

浮点数在这个数量级不能逐个表示相邻整数,2^63 - 1 可能已经被舍入。

直接使用:

go
fmt.Println(math.MaxInt64)
fmt.Println(math.MinInt64)
fmt.Println(math.MaxUint64)
fmt.Println(math.MaxInt)

或者用常量表达式:

go
const MaxInt64 = 1<<63 - 1
const MinInt64 = -1 << 63

5. 整数的字面量与运算

5.1 不同进制

go
decimal := 42
binary := 0b0010_1010
octal := 0o52
hex := 0x2A

fmt.Println(decimal == binary, binary == octal, octal == hex) // true true true

下划线用于提高可读性:

go
million := 1_000_000
mask := 0xFF_FF_00_00

下划线有语法位置限制,不能随意放在小数点或数字开头结尾。

旧式前导零八进制仍可用:

go
legacy := 052

新代码优先 0o52,意图更清楚,也避免把 010 误以为十进制 10。

5.2 算术运算

go
a + b
a - b
a * b
a / b
a % b

整数除法截断小数部分,结果向零靠近:

go
fmt.Println(7 / 3)   // 2
fmt.Println(-7 / 3)  // -2
fmt.Println(7 / -3)  // -2
fmt.Println(-7 / -3) // 2

取余满足:

text
x = q*y + r
|r| < |y|

除数为零会在运行期 panic;常量除以零会在编译期报错。

5.3 位运算

go
x & y  // 与
x | y  // 或
x ^ y  // 异或
x &^ y // 位清除:x AND NOT y
x << n // 左移
x >> n // 右移

权限标志:

go
type Permission uint8

const (
	PermissionRead Permission = 1 << iota
	PermissionWrite
	PermissionDelete
)

permissions := PermissionRead | PermissionWrite

if permissions&PermissionWrite != 0 {
	fmt.Println("可以写")
}

permissions &^= PermissionWrite // 清除写权限

移位计数必须是整数或无类型整数,不能为负。移位后的结果是否能放入目标类型,要看表达式最终上下文。

5.4 位算法用 math/bits

标准库提供:

go
bits.OnesCount64(x)
bits.LeadingZeros64(x)
bits.TrailingZeros64(x)
bits.RotateLeft64(x, n)
bits.Reverse64(x)
bits.Add64(x, y, carry)
bits.Mul64(x, y)

这些函数通常比手写循环更清楚,也更容易被编译器映射成硬件指令。

6. 整数溢出与窄化转换

6.1 常量溢出在编译期检查

go
const n = 256

// var b uint8 = n      // 编译错误
// var b = uint8(256)   // 编译错误

无类型常量本身可以很大,但一旦要求转换或赋给固定类型,就必须能够表示。

6.2 运行期整数可以溢出

无符号整数按 2ⁿ 取模:

go
var u uint8 = 255
u++
fmt.Println(u) // 0

有符号整数也允许溢出,结果按补码表示确定,不会自动 panic:

go
var i int8 = 127
i++
fmt.Println(i) // -128

这与 Java 整数溢出“不抛异常”的体验相似。不同之处是 Go 还提供无符号整数,并且编译器不能假定有符号溢出永远不会发生。

6.3 窄化转换保留低位

go
x := 256
b := uint8(x)
fmt.Println(b) // 0

转换过程会先扩展到隐含的无限精度有符号值,再截断到目标宽度;不断重复符号扩展或截断,直到得到目标类型。

另一个容易意外的例子:

go
var i int8 = -1
u := uint8(i)
fmt.Println(u) // 255

转换没有错误返回值。若输入来自用户、数据库或网络,转换前必须检查范围:

go
func ToUint8(n int) (uint8, error) {
	if n < 0 || n > math.MaxUint8 {
		return 0, fmt.Errorf("%d 超出 uint8 范围", n)
	}
	return uint8(n), nil
}

6.4 检测加法溢出

无符号加法:

go
func AddUint64(a, b uint64) (uint64, bool) {
	sum := a + b
	overflow := sum < a
	return sum, overflow
}

也可以使用:

go
sum, carry := bits.Add64(a, b, 0)
if carry != 0 {
	// 溢出
}

有符号整数要根据符号和边界判断:

go
func AddInt64(a, b int64) (int64, error) {
	if b > 0 && a > math.MaxInt64-b {
		return 0, errors.New("int64 正溢出")
	}
	if b < 0 && a < math.MinInt64-b {
		return 0, errors.New("int64 负溢出")
	}
	return a + b, nil
}

6.5 循环不要无意识地使用无符号数

go
for i := uint(10); i >= 0; i-- {
	// 永远不会因为 i < 0 结束;i 从 0 减 1 后绕回最大值
}

长度和索引用 int 不只是为了少转换,也避免无符号下溢让边界判断失效。

7. 浮点类型

7.1 两种浮点类型

类型IEEE 754 格式有效十进制数字约数最大有限值
float32binary32约 6~9 位math.MaxFloat32,约 3.4e38
float64binary64约 15~17 位math.MaxFloat64,约 1.8e308

最小正非零值:

go
math.SmallestNonzeroFloat32
math.SmallestNonzeroFloat64

它们是最小正 subnormal 值,不是最小负数。浮点负数范围关于零大致对称。

7.2 默认是 float64

go
f := 1.25
fmt.Printf("%T\n", f) // float64

显式使用 float32

go
var f32 float32 = 1.25

float32 更省空间,但计算精度更低。除非协议、GPU、海量数组或外部 API 明确要求,普通计算通常使用 float64

7.3 浮点字面量

go
a := 1.25
b := .25
c := 1.
d := 1.25e3
e := 0x1.8p1 // 十六进制浮点,等于 3.0

e 形式的指数以 10 为底;十六进制浮点的 p 指数以 2 为底。

7.4 二进制浮点不能精确表示多数十进制小数

go
x := 0.1
y := 0.2
fmt.Printf("%.20f\n", x+y)
// 0.30000000000000004441 附近

这不是 Go 特有问题,而是 IEEE 754 二进制浮点的性质。

不要把格式化为两位小数误认为内部值已经变精确:

go
fmt.Printf("%.2f\n", x+y) // 看起来是 0.30

格式化只改变显示,不改变存储值。

7.5 浮点相等

来自同一确定计算路径的值有时可以直接比较;测量、累计或不同计算路径得到的浮点数,通常需要容差:

go
func AlmostEqual(a, b, absTol, relTol float64) bool {
	diff := math.Abs(a - b)
	if diff <= absTol {
		return true
	}

	scale := max(math.Abs(a), math.Abs(b))
	return diff <= relTol*scale
}

只用固定 epsilon:

go
math.Abs(a-b) < 1e-9

在数量级变化很大时可能不可靠。容差必须来自业务误差模型,不存在适合所有程序的万能值。

7.6 NaN、正负无穷和负零

go
nan := math.NaN()
posInf := math.Inf(1)
negInf := math.Inf(-1)
negZero := math.Copysign(0, -1)

判断:

go
math.IsNaN(nan)
math.IsInf(posInf, 1)
math.IsInf(negInf, -1)
math.Signbit(negZero)

NaN 与任何值比较都不相等,也包括它自己:

go
fmt.Println(nan == nan) // false

因此不能写:

go
if value == math.NaN() {
	// 永远不会进入
}

必须使用 math.IsNaN

cmp.Compare 为排序提供了稳定的 NaN 规则;普通 <== 仍遵守 IEEE 行为。语法虽然允许浮点数作为 map key,但 NaN 键无法再用“相等的 NaN”正常取回,这样的接口通常得不偿失。

7.7 float 转 int

go
f := 12.9
i := int(f)
fmt.Println(i) // 12,直接丢弃小数部分,向零截断

负数:

go
fmt.Println(int(-12.9)) // -12

若非常量浮点值超出目标整数范围,结果依赖具体实现。转换前应检查:

go
func Float64ToInt64(f float64) (int64, error) {
	if math.IsNaN(f) || math.IsInf(f, 0) {
		return 0, errors.New("不是有限数")
	}
	if f < math.MinInt64 || f >= -float64(math.MinInt64) {
		return 0, errors.New("超出 int64 范围")
	}
	return int64(f), nil
}

float64(math.MaxInt64) 会舍入到 2^63,所以上界判断不能直接写成 f > float64(math.MaxInt64)。涉及边界的转换应当配测试;如果数据在业务上本来就是整数,更稳妥的办法是不要先让它经过浮点类型。

7.8 舍入函数

go
math.Floor(1.8)    // 1
math.Ceil(1.2)     // 2
math.Trunc(-1.8)   // -1
math.Round(1.5)    // 2,半数远离零
math.RoundToEven(2.5) // 2,银行家舍入

选择函数之前,先把业务舍入规则写清楚。金融结算不能靠“看起来像四舍五入”来决定结果。

7.9 序列化特殊值

JSON 不支持 NaN 和正负无穷:

go
_, err := json.Marshal(math.NaN())
// err 为 *json.UnsupportedValueError

数值进入 JSON、数据库或 RPC 边界之前,应先确认它是有限值。

8. 复数类型

8.1 complex64complex128

go
var c64 complex64   // 实部和虚部各为 float32
var c128 complex128 // 实部和虚部各为 float64

默认复数字面量类型是 complex128

go
c := 3 + 4i
fmt.Printf("%T %v\n", c, c) // complex128 (3+4i)

8.2 构造与拆分

go
c := complex(3.0, 4.0)

fmt.Println(real(c)) // 3
fmt.Println(imag(c)) // 4

complex 参数的浮点类型决定结果:

go
var r32, i32 float32 = 3, 4
c32 := complex(r32, i32) // complex64

8.3 运算

go
a := 1 + 2i
b := 3 - 4i

fmt.Println(a + b)
fmt.Println(a - b)
fmt.Println(a * b)
fmt.Println(a / b)

math/cmplx 提供:

go
cmplx.Abs(3 + 4i)  // 5
cmplx.Conj(1 + 2i) // 1 - 2i
cmplx.Phase(c)
cmplx.Polar(c)
cmplx.Sqrt(c)
cmplx.Exp(c)

8.4 比较限制

复数支持 ==!=,不支持 <<=>>=

go
fmt.Println((1 + 2i) == (1 + 2i))
// fmt.Println((1 + 2i) < (2 + 3i)) // 编译错误

排序必须先定义业务规则,例如按模、实部或相位排序。复数分量也可能包含 NaN,届时相等比较同样会表现出 NaN 的特殊性。

9. 布尔类型

9.1 值与零值

go
var enabled bool // false

enabled = true
enabled = false

只有两个预声明布尔常量:

go
true
false

9.2 没有 truthy / falsy

Go 不允许把整数、字符串、指针当布尔条件:

go
count := 1

// if count { }      // 编译错误
if count != 0 { }    // 正确

name := "go"
if name != "" { }    // 正确

也不能在 bool 和整数之间转换:

go
// b := bool(1) // 编译错误
// n := int(true)

这减少了 0、空字符串、nil 混入条件判断的歧义。

9.3 运算

go
!a
a && b
a || b
a == b
a != b

&&|| 短路:

go
if user != nil && user.Enabled {
	// user 为 nil 时不会访问 user.Enabled
}

表达式从左到右求值。可以利用短路保证安全,但不要在条件右侧塞入难以理解的副作用。

9.4 布尔命名

优先让名字读起来像判断:

go
isReady
hasPermission
canRetry
enabled

避免:

go
flag
status
value

当 bool 已无法表达状态时,不要继续叠加 isX 字段。比如任务可能是 pending、running、failed、done,更适合定义枚举类型。

10. byte、rune 与 Unicode

10.1 单引号是 rune literal

go
a := 'a'
zhong := ''

fmt.Printf("%T %d %c %U\n", a, a, a, a)
fmt.Printf("%T %d %c %U\n", zhong, zhong, zhong, zhong)

默认类型:

text
rune,也就是 int32

'中' 的数值是 Unicode 码点 U+4E2D,即十进制 20013。它不是“UTF-8 编码值”。这个码点编码成 UTF-8 后才是三个字节:

text
E4 B8 AD

码点和编码是两个层次:

text
字符抽象:Unicode code point U+4E2D
UTF-8 编码:E4 B8 AD
Go 表达码点:rune(0x4E2D)
Go 表达编码字节:[]byte{0xE4, 0xB8, 0xAD}

10.2 byte

go
var b byte = 'A'
fmt.Println(b)        // 65
fmt.Printf("%c\n", b) // A

ASCII 范围的码点与单字节 UTF-8 值相同,所以 'A' 可以放进 byte:

go
var a byte = 'A'

中文码点超出 uint8:

go
// var z byte = '中' // 编译错误:溢出

10.3 rune 的合法范围

Unicode 最大码点:

go
utf8.MaxRune // U+10FFFF

rune 本身是 int32,能表示的整数范围比合法 Unicode 码点更大:

go
r := rune(0x7FFFFFFF)
fmt.Println(utf8.ValidRune(r)) // false

把非法 rune 转为 string 时会得到 Unicode replacement character U+FFFD

go
fmt.Printf("%q\n", string(r))

10.4 一个“用户看到的字符”可能有多个 rune

例如带组合音标的字符、国旗、家庭 emoji,可能由多个 Unicode 码点组成。于是存在三个不同的长度:

text
字节数
rune / code point 数
grapheme cluster / 用户感知字符数

Go 标准库的 len 给字节数,utf8.RuneCountInString 给 rune 数;界面光标、文本宽度和“最多 20 个字符”这类产品要求,往往需要专门的 Unicode grapheme 库。

10.5 Java char 不能直接类比 rune

Java char 是一个 UTF-16 code unit,占 16 位;超出 BMP 的码点需要 surrogate pair。Go rune 是 32 位整数,可容纳一个合法 Unicode 码点。

因此:

text
Java char ≠ Go rune
Java code point(int)更接近 Go rune

11. 字符串

11.1 string 是不可变字节序列

go
var name string = "枫枫知道"

规范对 string 的定义是“不可变的字节序列”。它不保证内容一定是 UTF-8:

go
s := string([]byte{0xff, 0xfe})
fmt.Println(utf8.ValidString(s)) // false

Go 源文件必须是 UTF-8;普通字符串字面量中直接写入的 Unicode 文本会形成 UTF-8 字节,但运行期 string 可以保存任意字节。

11.2 不可修改

go
s := "hello"
// s[0] = 'H' // 编译错误

要修改,转换为 []byte[]rune

go
bytes := []byte(s)
bytes[0] = 'H'
s = string(bytes)

字符串变量可以重新赋值:

go
s = "world"

“不可变”指原字符串值中的字节不能原地改变,不是变量永远不能指向新字符串。

11.3 len 返回字节数

go
s := "中国"

fmt.Println(len(s))                    // 6
fmt.Println(utf8.RuneCountInString(s)) // 2

因为每个中文码点在 UTF-8 中占 3 字节。

11.4 字符串比较

go
a == b
a != b
a < b
a <= b
a > b
a >= b

排序按字节字典序进行,不是人类语言的排序规则:

go
fmt.Println("Z" < "a") // true

大小写无关比较:

go
strings.EqualFold(a, b)

本地化排序、拼音排序、重音处理需要 Unicode collation 能力,不能依赖 <

11.5 string 可以作为 map key

go
counts := map[string]int{
	"go": 1,
}

string 可比较,因此适合作为 map key。键比较按字节进行,视觉上相同但 Unicode 规范化形式不同的字符串可能是两个不同键。

如果业务要求用户名或搜索键规范化,需要在边界统一处理,而不是假定所有输入天然同形同码。

12. 转义字符与原始字符串

12.1 解释型字符串

双引号字符串会解释反斜杠转义:

go
fmt.Println("枫枫\t知道")           // 水平制表符
fmt.Println("枫枫\n知道")           // 换行 LF
fmt.Println("\"枫枫\"知道")         // 双引号
fmt.Println("枫枫\r知道")           // 回车 CR,回到当前行行首
fmt.Println("C:\\pprof\\main.exe") // 反斜杠

原教程把 \n 叫“回车”,不够准确:

  • \n:line feed,LF,换行;
  • \r:carriage return,CR,回到行首;
  • Windows 文本行常用 \r\n
  • Unix 通常使用 \n

终端如何显示裸 \r 取决于环境。它常用于进度条覆盖当前行,不适合当通用换行。

12.2 常用转义

写法含义
\a响铃
\b退格
\f换页
\n换行 LF
\r回车 CR
\t水平制表符
\v垂直制表符
\\反斜杠
\"双引号
\'单引号,主要用于 rune literal
\xNN两位十六进制字节
\uNNNN4 位十六进制 Unicode 码点
\UNNNNNNNN8 位十六进制 Unicode 码点
go
fmt.Println("\x41")       // A
fmt.Println("\u4E2D")     // 中
fmt.Println("\U0001F600") // 😀

\xNN 写入一个字节,可能构造出非法 UTF-8;\u\U 表达合法 Unicode 码点并以 UTF-8 放入字符串。

12.3 原始字符串

反引号中的 raw string literal 基本按原样保存:

go
sql := `SELECT id, name
FROM users
WHERE enabled = true`

其中 \n 不会变成换行:

go
path := `C:\pprof\main.exe`
text := `hello\nworld`

text 中是反斜杠和字母 n。

限制:

  • 不能直接包含反引号;
  • 源码中的实际换行会成为字符串内容;
  • 为代码缩进加入的空格和 tab 也会进入内容。

原始字符串适合 SQL、正则表达式、JSON 示例和多行模板,但不是“自动去缩进”的文本块。

13. 字符串的遍历、截取与转换

13.1 按索引访问得到 byte

go
s := "中A"

fmt.Printf("%T %x\n", s[0], s[0]) // uint8 e4

s[0] 是“中”的第一个 UTF-8 字节,不是 rune '中'

逐字节遍历:

go
for i := 0; i < len(s); i++ {
	fmt.Printf("byte[%d]=%02x\n", i, s[i])
}

13.2 range 解码 UTF-8

go
for byteIndex, r := range s {
	fmt.Printf("byteIndex=%d rune=%c code=%U\n", byteIndex, r, r)
}

第一项是 rune 起点的字节偏移,不是第几个 rune:

text
byteIndex=0 rune=中
byteIndex=3 rune=A

遇到非法 UTF-8,range 产生 utf8.RuneError(U+FFFD),并前进 1 字节。

13.3 截取字符串按字节

go
s := "中国"

first := s[:3] // "中"

错误边界会产生非法 UTF-8:

go
broken := s[:2]
fmt.Println(utf8.ValidString(broken)) // false

按 rune 截取:

go
runes := []rune(s)
firstRune := string(runes[:1])

这会分配并解码整个字符串。大文本或高频路径可以用 range 找到目标字节边界,只截取需要的部分。

按 rune 截取仍可能切开一个 grapheme cluster,例如 emoji 组合。产品说的“字符”需要先定义清楚。

13.4 string[]byte

go
s := "hello"
b := []byte(s)
s2 := string(b)

语义上它们是独立值,正常转换通常会复制数据。编译器在能证明安全时可能消除部分复制,但代码不能依赖这种实现优化。

修改 b 不应影响 s

go
b[0] = 'H'
fmt.Println(s)         // hello
fmt.Println(string(b)) // Hello

不要为了“零拷贝”随意使用 unsafe.Stringunsafe.Slice。一旦底层字节被修改或生命周期结束,字符串不可变性和内存安全都会被破坏。

13.5 string[]rune

go
runes := []rune("中国")
fmt.Println(runes)         // [20013 22269]
fmt.Println(string(runes)) // 中国

适合需要按码点修改:

go
runes[0] = ''
result := string(runes)

如果只是检查、查找或替换,优先使用 stringsunicodeunicode/utf8 的现成函数,避免不必要地转完整 rune slice。

13.6 拼接

少量固定片段:

go
fullName := firstName + " " + lastName

循环或大量片段:

go
var b strings.Builder
b.Grow(128)

for _, part := range parts {
	b.WriteString(part)
}

result := b.String()

二进制和字符串混合处理可以使用 bytes.Buffer。不要在没有 benchmark 的情况下为了几次拼接引入复杂池化。

13.7 substring 可能保留大底层数据

字符串切片通常复用原字符串数据:

go
small := huge[:10]

如果 small 长期存活,可能让整个 huge 的底层数据无法回收。需要主动复制时:

go
small = strings.Clone(huge[:10])

这类问题只在“大字符串切出极小片段并长期保存”时值得关注。普通局部切片不要过早优化。

14. 零值

14.1 基本类型零值

类型零值
所有整数0
所有浮点数0.0
所有复数0 + 0i
boolfalse
string""
go
var i int
var f float64
var c complex128
var b bool
var s string

fmt.Printf("%#v\n", i) // 0
fmt.Printf("%#v\n", f) // 0
fmt.Printf("%#v\n", c) // (0+0i)
fmt.Printf("%#v\n", b) // false
fmt.Printf("%#v\n", s) // ""

14.2 零值不等于“未初始化”

Go 不允许读取一块未初始化的局部基本类型内存。变量声明后总有确定值:

go
var count int
fmt.Println(count) // 合法,0

这与 Java 字段的默认值相似,但 Java 局部变量必须在读取前显式赋值;Go 局部变量也自动拥有零值。

14.3 零值可能是合法业务值

0false"" 既可能表示“未提供”,也可能是真实值。不要只凭零值猜字段是否出现:

go
type UpdateUser struct {
	Age     int
	Enabled bool
}

无法区分:

text
客户端没有提供 Enabled
客户端明确提供 Enabled=false

可选字段常用指针:

go
type UpdateUser struct {
	Age     *int
	Enabled *bool
}

Go 1.26 可以方便地构造:

go
req := UpdateUser{
	Age:     new(18),
	Enabled: new(false),
}

也可以定义显式 optional 类型,避免业务层到处解引用指针。

15. 类型转换与可赋值性

15.1 Go 没有数字隐式提升

go
var a int32 = 10
var b int64 = 20

// c := a + b // 编译错误
c := int64(a) + b

即使当前平台 intint64 都是 64 位,它们仍是不同类型:

go
var i int = 10
var j int64 = 20

// j = i
j = int64(i)

这比 Java 的 numeric promotion 更严格,代价是转换多一点,收益是跨架构和有符号/无符号混用更显式。

15.2 转换语法

go
T(value)

例子:

go
f := float64(10)
i := int(3.9)
s := string('')
runes := []rune("中国")
bytes := []byte("hello")

转换不是 Java cast 的完整等价物。Go 只允许规范规定的类型转换,不能把任意对象强制转换成无关类型。

15.3 int 转 string 不是十进制格式化

go
fmt.Println(string(65)) // "A"

整数转 string 把整数解释为 Unicode 码点,不是得到 "65"

十进制文本:

go
strconv.Itoa(65)
strconv.FormatInt(65, 10)
fmt.Sprintf("%d", 65)

静态检查工具会特别提示可疑的 string(intValue)

15.4 定义类型之间需要转换

go
type Celsius float64
type Fahrenheit float64

var c Celsius = 100
// var f Fahrenheit = c
var f Fahrenheit = Fahrenheit(c*9/5 + 32)

显式转换只改变类型和按规则得到的表示,不自动应用领域语义。Fahrenheit(c) 只是数值类型转换,不会自动完成摄氏到华氏公式。

15.5 字符串转换的特殊规则

go
string([]byte{0xE4, 0xB8, 0xAD}) // "中"
string([]rune{'', ''})       // "中国"
string(0x4E2D)                   // "中"

整数值不是合法 Unicode 码点时,结果为 replacement rune:

go
fmt.Printf("%q\n", string(-1)) // "�"

16. 格式化与解析

16.1 输出格式

整数:

go
fmt.Printf("%d\n", n)   // 十进制
fmt.Printf("%b\n", n)   // 二进制
fmt.Printf("%o\n", n)   // 八进制
fmt.Printf("%x\n", n)   // 十六进制
fmt.Printf("%c\n", n)   // Unicode 字符
fmt.Printf("%U\n", n)   // U+XXXX

浮点:

go
fmt.Printf("%f\n", f)
fmt.Printf("%.2f\n", f)
fmt.Printf("%e\n", f)
fmt.Printf("%g\n", f)

类型和 Go 表示:

go
fmt.Printf("%T\n", value)
fmt.Printf("%#v\n", value)

16.2 strconvfmt.Sscan 更适合单值解析

go
n, err := strconv.Atoi("42")
f, err := strconv.ParseFloat("3.14", 64)
b, err := strconv.ParseBool("true")

带明确位宽:

go
n, err := strconv.ParseInt("-128", 10, 8)
u, err := strconv.ParseUint("255", 10, 8)

ParseInt 返回 int64,即使指定 bitSize 8:

go
n8 := int8(n)

解析成功后转换是安全的,因为 ParseInt(..., 8) 已经做过范围检查。

16.3 自动识别进制

go
n, err := strconv.ParseInt("0xff", 0, 64)

base 为 0 时识别:

  • 0b:二进制;
  • 00o:八进制;
  • 0x:十六进制;
  • 其他:十进制。

若输入协议只允许十进制,应明确传 10,不要因为方便而接受额外格式。

16.4 解析错误

strconv 返回 *strconv.NumError

go
value, err := strconv.ParseInt(input, 10, 64)
if err != nil {
	var numErr *strconv.NumError
	if errors.As(err, &numErr) {
		switch {
		case errors.Is(numErr.Err, strconv.ErrSyntax):
			return fmt.Errorf("%q 不是合法整数", input)
		case errors.Is(numErr.Err, strconv.ErrRange):
			return fmt.Errorf("%q 超出 int64 范围", input)
		}
	}
	return err
}

不要只返回“参数错误”,应告诉调用方是格式错误还是范围错误,但日志中要避免回显敏感输入。

16.5 格式化为字符串

go
strconv.Itoa(42)
strconv.FormatInt(-42, 10)
strconv.FormatUint(255, 16)
strconv.FormatBool(true)
strconv.FormatFloat(3.14, 'g', -1, 64)
strconv.Quote("hello\n")

FormatFloat 参数:

go
FormatFloat(f, fmt, prec, bitSize)

常见的 'g', -1, 64 会选择能往返还原 float64 的紧凑表示。

批量追加到 []byte

go
buf = strconv.AppendInt(buf, n, 10)
buf = strconv.AppendFloat(buf, f, 'g', -1, 64)

热路径中它通常比 fmt.Sprintf 少分配,但应以 benchmark 为准。

17. 大小、对齐与内存布局

17.1 固定宽度类型的大小

text
int8 / uint8        1 byte
int16 / uint16      2 bytes
int32 / uint32      4 bytes
int64 / uint64      8 bytes
float32             4 bytes
float64             8 bytes
complex64           8 bytes
complex128         16 bytes

boolintuintuintptr、string 描述符以及 struct 布局应通过目标架构和实现分析,不要拿经验值写协议。

17.2 unsafe.Sizeof

go
fmt.Println(unsafe.Sizeof(int(0)))
fmt.Println(unsafe.Sizeof(float64(0)))
fmt.Println(unsafe.Sizeof(""))

unsafe.Sizeof 返回变量本身的大小,不包含它引用的外部数据:

go
s := "一个很长的字符串"
fmt.Println(unsafe.Sizeof(s))

结果只是 string 值内部描述信息的大小,不随字符串内容长度变化。

slice、map、interface 同理:只统计描述符本身,不递归统计底层数组或对象。

17.3 struct padding

go
type A struct {
	Flag bool
	ID   int64
	Code byte
}

字段之间可能为了对齐插入 padding,因此 struct 大小不等于字段大小简单相加。调整字段顺序可能减少内存:

go
type B struct {
	ID   int64
	Flag bool
	Code byte
}

只有在大量对象且 profile 证明内存布局重要时再优化。随意重排公开 struct 字段也会影响可读性、反射输出和位置式复合字面量用户。

17.4 不要用内存布局做文件协议

下面的思路不可移植:

text
把 struct 的内存字节直接写入文件或网络

问题包括:

  • 字节序;
  • padding;
  • int 宽度;
  • 指针和 string 描述符;
  • Go 版本和架构差异;
  • 对齐要求。

使用 encoding/binary、JSON、Protobuf 等明确协议,并为每个字段选择固定宽度类型。

18. 工程边界:金额、时间、ID 与 JSON

18.1 金额

不推荐:

go
price := 19.99 // float64

若只处理固定最小货币单位:

go
type Cents int64

price := Cents(1999)

涉及汇率、不同小数位、税务舍入时,需要明确的十进制定点模型或经过评估的 decimal 库。核心不是“绝不能用 float”,而是金额规则不能由 IEEE 754 的偶然舍入决定。

18.2 时间和持续时间

时间点使用:

go
time.Time

持续时间使用:

go
time.Duration // 底层是 int64,单位为纳秒
go
timeout := 5 * time.Second

不要用裸 int64 同时表达 Unix 秒、Unix 毫秒和纳秒。类型相同不代表单位相同。

跨协议时在字段名或类型中明确单位:

go
type UnixMillis int64

18.3 ID

数据库自增 ID 常用 int64,但领域层可定义:

go
type UserID int64
type OrderID int64

分布式 ID 若可能使用 uint64 全范围,注意:

  • 数据库驱动是否支持;
  • JSON/JavaScript 是否能精确表示;
  • 日志和监控是否按有符号 64 位解析;
  • 跨语言 schema 是否一致。

很多系统把大 ID 作为十进制字符串传给前端,以绕过 JavaScript Number 的精度上限。

18.4 JSON number 的精度陷阱

明确目标 struct 时,按字段类型解析:

go
type Payload struct {
	ID int64 `json:"id"`
}

解码到 any 时,JSON number 默认变成 float64

go
var value any
_ = json.Unmarshal([]byte(`9007199254740993`), &value)

fmt.Printf("%T %v\n", value, value)

大整数可能已经丢失精度。需要保留数字文本:

go
dec := json.NewDecoder(reader)
dec.UseNumber()

var value any
if err := dec.Decode(&value); err != nil {
	return err
}

之后对 json.Number 调用 Int64Float64 或按业务使用十进制库。

18.5 NaN 与 Inf 不能直接进 JSON

go
json.Marshal(math.NaN())   // 错误
json.Marshal(math.Inf(1))  // 错误

API schema 应决定特殊值表示为:

  • 拒绝请求;
  • null
  • 明确字符串;
  • 独立状态字段。

不要悄悄把它改成 0。

18.6 string 不保证 UTF-8,但外部协议往往要求

Go string 可保存任意字节;JSON、数据库文本列、HTTP header 和日志系统通常要求特定编码。

边界检查:

go
if !utf8.ValidString(input) {
	return errors.New("文本不是合法 UTF-8")
}

encoding/json 的具体无效 UTF-8 处理还会受到所用 JSON 语义版本和选项影响。安全敏感接口不要依赖“库会替我修复”,应先定义协议要求。

19. 泛型中的基本类型

19.1 comparable

所有基本类型都可比较,因此满足 comparable

go
func Contains[T comparable](values []T, target T) bool {
	for _, value := range values {
		if value == target {
			return true
		}
	}
	return false
}

浮点和复数虽然语法上 comparable,但 NaN 不等于自身。泛型算法若默认“每个值都等于自己”,需要额外说明或排除 NaN。

19.2 cmp.Ordered

标准库:

go
func Min[T cmp.Ordered](a, b T) T {
	if a < b {
		return a
	}
	return b
}

cmp.Ordered 包含:

  • 所有整数及其底层类型;
  • float32float64 及其底层类型;
  • string 及其底层类型。

不包含:

  • bool
  • complex64complex128

19.3 ~ 的意义

go
type Signed interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64
}

~int64 表示底层类型是 int64 的所有类型,包括:

go
int64
UserID
OrderID

若只写 int64,自定义定义类型不会满足约束。

19.4 一个整数求和约束

go
type Integer interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64 |
		~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}

func Sum[T Integer](values []T) T {
	var total T
	for _, value := range values {
		total += value
	}
	return total
}

这个函数仍然会按 T 的宽度溢出。泛型消除了重复代码,不会自动提供大整数或溢出保护。

20. 与 Java 的差异

JavaGo关键差异
byteint8Java byte 有符号,Go byte 是 uint8
无内建无符号整数或使用辅助 APIuint8uint64Go 原生支持无符号运算
shortint16都是 16 位有符号
int 固定 32 位int 为 32 或 64 位跨协议不能用 Go int
longint64都是 64 位有符号
char,UTF-16 code unitrune,Unicode code point不是一一对应
floatfloat32IEEE 754 binary32
doublefloat64Go 浮点字面量默认 float64
无基本复数类型complex64/128Go 内建复数
booleanboolGo 不允许与整数互转
Stringstring都不可变;Go 索引得到 UTF-8 byte
BigIntegermath/big.IntGo 基本整数仍是固定宽度
BigDecimal无标准库直接等价需定点设计、big.Rat 或第三方 decimal
primitive / wrapper没有自动装箱模型interface 可保存值,但模型不同
隐式 numeric promotion大多要求显式转换Go 不自动混合 int32/int64
显式 narrowing cast 仍可能截断数值转换会截断两边都需要在转换前手工检查范围
字段有默认值,局部变量需赋值所有变量都有零值Go 局部变量也有零值

Java 开发者尤其容易踩这四个坑:

  1. 把 Go byte 当成 Java byte,忘记前者无符号;
  2. 把 Go rune 当成 Java char,忽略 UTF-16 surrogate;
  3. 以为 Go int 固定 32 位;
  4. 期待不同整数类型像 Java 一样自动提升。

21. 常见误区

整数

  • math.Pow 计算整数最大值。
  • 认为 int 永远是 int64
  • uintptr 当“大号 uint”用于业务。
  • 认为运行期整数溢出会 panic。
  • 未检查范围就窄化为 uint8int32
  • 用无符号循环变量判断 i >= 0
  • 混用 intint64 后到处随意转换。
  • 用主机内存布局定义网络协议。

浮点和复数

  • 用 float64 表示需要精确结算的金额。
  • 认为 %.2f 会让内部值变精确。
  • 所有场景都用固定 1e-9 判断相等。
  • value == math.NaN() 判断 NaN。
  • 未校验 NaN/Inf 就写 JSON。
  • float 转 int 前不检查有限性和范围。
  • 试图用 < 比较复数。

byte、rune 和 string

  • 说 rune 保存“UTF-8 码值”。
  • 认为 byte 可以保存任意“字符”。
  • 认为 len(string) 返回字符数。
  • 认为 range 的索引是第几个 rune。
  • 按任意字节位置截断 UTF-8。
  • 认为 rune 数等于用户看到的字符数。
  • string(65) 当成十进制 "65"
  • 假定所有 Go string 都是合法 UTF-8。
  • 用普通字节序比较实现自然语言排序。
  • 长期保存巨大字符串的微小切片却不复制。

类型设计

  • 用别名代替领域定义类型,失去编译期隔离。
  • 认为显式转换会自动完成单位换算。
  • 用零值同时表达“未提供”和真实的 false/0。
  • 认为泛型求和会自动防止溢出。
  • 遮蔽 stringintbool 等预声明名字。

22. 速查表

默认类型

go
i := 1       // int
f := 1.0     // float64
c := 1 + 2i  // complex128
r := ''    // rune / int32
b := true    // bool
s := "go"    // string

范围

go
math.MinInt
math.MaxInt
math.MinInt8
math.MaxInt8
math.MinInt16
math.MaxInt16
math.MinInt32
math.MaxInt32
math.MinInt64
math.MaxInt64
math.MaxUint
math.MaxUint8
math.MaxUint16
math.MaxUint32
math.MaxUint64

math.MaxFloat32
math.MaxFloat64
math.SmallestNonzeroFloat32
math.SmallestNonzeroFloat64

当前 int 位宽

go
strconv.IntSize

进制

go
binary := 0b1010
octal := 0o12
decimal := 10
hex := 0x0A

类型转换

go
int64(i)
float64(i)
int(f)
string(r)
[]byte(s)
[]rune(s)
string(bytes)
string(runes)

字符串长度

go
len(s)                    // 字节数
utf8.RuneCountInString(s) // rune 数
utf8.ValidString(s)       // 是否为合法 UTF-8

数字解析

go
n, err := strconv.Atoi(text)
i, err := strconv.ParseInt(text, 10, 64)
u, err := strconv.ParseUint(text, 10, 64)
f, err := strconv.ParseFloat(text, 64)
b, err := strconv.ParseBool(text)

数字格式化

go
strconv.Itoa(n)
strconv.FormatInt(i, 10)
strconv.FormatUint(u, 10)
strconv.FormatFloat(f, 'g', -1, 64)
strconv.FormatBool(b)

特殊浮点

go
math.IsNaN(f)
math.IsInf(f, 0)
math.Inf(1)
math.Inf(-1)
math.NaN()
math.Signbit(f)

常用字符串工具

go
strings.Contains(s, sub)
strings.HasPrefix(s, prefix)
strings.HasSuffix(s, suffix)
strings.TrimSpace(s)
strings.Fields(s)
strings.Split(s, sep)
strings.Join(parts, sep)
strings.EqualFold(a, b)
strings.Clone(s)

23. 可运行示例

本节把最容易“看懂定义、写错边界”的三类问题放进可执行程序:UTF-8 索引、整数溢出和 JSON 数字精度。源码统一位于 examples/ch03

23.1 同时观察字节、rune 与字节索引

同一个字符串有三种计数视角。 len(text)utf8.RuneCountInStringrange text 各自在数什么?

go
package main

import (
	"fmt"
	"unicode/utf8"
)

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

	// len(string) 返回 UTF-8 编码后的字节数,不是“字符数”。
	fmt.Printf("字节数=%d rune数=%d\n", len(text), utf8.RuneCountInString(text))

	// range string 的索引是每个 rune 的起始字节偏移,值才是解码后的 Unicode 码点。
	for byteIndex, r := range text {
		fmt.Printf("byte=%d rune=%q code=%U\n", byteIndex, r, r)
	}
}

运行:

bash
go run ./examples/ch03/unicode

预期输出:

text
字节数=8 rune数=4
byte=0 rune='G' code=U+0047
byte=1 rune='o' code=U+006F
byte=2 rune='语' code=U+8BED
byte=5 rune='言' code=U+8A00

对照输出看清字节与码点。

  1. Go string 保存字节序列;len 得到 8,因为两个 ASCII 字母各占 1 字节,两个汉字各占 3 字节。
  2. RuneCountInString 解码 UTF-8 后得到 4 个码点。
  3. range 的 index 是当前 rune 在原字符串中的字节起点,所以索引从 2 跳到 5。
  4. rune 是 Unicode 码点,不等于用户眼中的“一个完整字符”;组合音标和 emoji 仍可能由多个 rune 构成。

换成组合字符再观察。 把文本改为 "e\u0301",比较字节数、rune 数和肉眼看到的字符数;再加入 text[2],观察它得到的是汉字 UTF-8 编码的第一个字节。

23.2 显式检查无符号整数溢出

溢出不会主动报错。 固定宽度整数相加超出范围时,怎样避免悄悄回绕?

go
package main

import (
	"fmt"
	"math"
	"math/bits"
)

// addChecked 返回结果和是否溢出。
// 无符号加法溢出会按 2^64 回绕,语言不会自动报错;carry 才是边界信号。
func addChecked(left, right uint64) (sum uint64, overflow bool) {
	sum, carry := bits.Add64(left, right, 0)
	return sum, carry != 0
}

func main() {
	sum, overflow := addChecked(40, 2)
	fmt.Printf("40 + 2 = %d, overflow=%t\n", sum, overflow)

	wrapped, overflow := addChecked(math.MaxUint64, 1)
	fmt.Printf("MaxUint64 + 1 = %d, overflow=%t\n", wrapped, overflow)
}

运行:

bash
go run ./examples/ch03/checked-overflow

预期输出:

text
40 + 2 = 42, overflow=false
MaxUint64 + 1 = 0, overflow=true

进位值给出了判断依据。

  1. uint64 运算按模 $2^{64}$ 进行,最大值加一会得到 0,不会自动产生 error 或 panic。
  2. bits.Add64 同时返回低 64 位结果和进位 carry,进位非零就说明发生溢出。
  3. 包装函数把底层的 uint64 carry 转成业务更容易理解的 bool
  4. 若溢出不能接受,调用方必须在使用 sum 前处理标志;打印标志但继续入账仍然是 bug。

把边界向前移动一格。 测试 math.MaxUint64-1 分别加 1 和 2;再用 bits.Sub64 写一个检查无符号减法下溢的函数。

23.3 保住 JSON 整数和金额精度

一次 JSON 解码会暴露两类精度问题。 为什么解码到 any 的大整数会变化?金额为什么更适合用最小货币单位保存?

go
package main

import (
	"encoding/json"
	"fmt"
	"strings"
)

func main() {
	const document = `{"order_id":9007199254740993,"amount_cents":1999}`

	// 解码到 any 时,默认数字会进入 float64;超过 2^53 后,某些整数无法精确表示。
	var loose map[string]any
	if err := json.Unmarshal([]byte(document), &loose); err != nil {
		panic(err)
	}
	fmt.Printf("默认 float64 订单号: %.0f\n", loose["order_id"].(float64))

	// UseNumber 先保留 JSON 数字的十进制文本,再由业务代码决定转成 int64、big.Int 等类型。
	decoder := json.NewDecoder(strings.NewReader(document))
	decoder.UseNumber()
	var exact map[string]json.Number
	if err := decoder.Decode(&exact); err != nil {
		panic(err)
	}

	orderID, err := exact["order_id"].Int64()
	if err != nil {
		panic(err)
	}
	amountCents, err := exact["amount_cents"].Int64()
	if err != nil {
		panic(err)
	}

	// 金额用“分”的整数保存,避免把 19.99 放进二进制浮点后参与精确结算。
	fmt.Printf("精确订单号: %d\n", orderID)
	fmt.Printf("金额: %d.%02d\n", amountCents/100, amountCents%100)
}

运行:

bash
go run ./examples/ch03/json-precision

预期输出:

text
默认 float64 订单号: 9007199254740992
精确订单号: 9007199254740993
金额: 19.99 元

示例分别守住整数和金额边界。

  1. JSON 解码到 map[string]any 时,数字默认成为 float642^53+1 已不能被它精确表示。
  2. Decoder.UseNumber 让数字先以 json.Number 保存原始十进制文本,再显式调用 Int64
  3. 转换仍可能失败或越界,所以 Int64 的 error 不能忽略。
  4. 示例把 19.99 元存成 1999 分的整数,结算加减保持精确;展示时再拆分整数部分和小数部分。

继续把输入推到范围之外。 把订单号改成超过 int64 最大值的数字,观察 Int64 错误;再定义带 int64 字段的结构体直接解码,比较它与 UseNumber 的适用边界。

24. 官方资料

入门文章适合快速认识类型名,整数溢出、浮点边界以及字符串与 Unicode 的规则,则应回到语言规范和标准库文档确认。看到“rune 就是多字节字符”或“字符保存的是 UTF-8 码值”这类表述时尤其要留意:码点、编码和用户感知字符是三个不同的概念。

以 Go 官方规范与标准库文档为准,示例面向 Go 1.26。