程序里的数值、条件和文本,最后都要落到一组确定的比特上。Go 的基本类型看起来不多,但真正容易出错的地方都藏在边界里:整数会不会回绕,浮点数为什么“算不准”,字符串下标拿到的是字节还是字符,常量为什么有时不需要转换。
这篇内容不把类型当作一张需要背诵的表。我们沿着“值怎样表示、运算怎样发生、边界怎样处理”这条线,把整数、浮点数、复数、布尔、字符串和常量串起来。示例都可以单独运行;每一部分后面都有小测,建议先判断,再展开答案。
基本类型包括布尔、数字和字符串。数字又分为整数、浮点数与复数。一个值的含义与可做的运算由类型确定。
byte 是 uint8 的别名,强调“一个原始字节”;rune 是 int32 的别名,强调“一个 Unicode 码点”。别名不是新类型,所以 byte 与 uint8 可以直接互用,rune 与 int32 也一样。
类型名称会提示意图。处理网络报文时写 byte,处理 Unicode 码点时写 rune,比都写成 uint8 或 int32 更容易读懂。
一个 n 位有符号整数使用补码表示,范围是 到 ;同宽无符号整数的范围是 到 。例如 int8 是 -128 到 127,uint8 是 0 到 255。
int 与 uint 的宽度取决于实现,至少为 32 位,常见平台上是 32 位或 64 位。它们与同宽的 int32、int64 仍是不同类型。普通循环、长度与索引优先使用 int;协议字段、磁盘格式或必须固定宽度的数值才用 int32、uint64 之类。
uintptr 能容纳指针的无解释位模式,主要出现在 unsafe 和系统边界。它不是“更大的 uint”,也不应拿来长期保存地址。

整数支持 +、-、*、/ 和 %。两个整数相除仍得到整数,结果向零截断;余数与被除数同号。
package main
import "fmt"
func main() {
fmt.Println(7 / 3) // 2
fmt.Println(-7 / 3) // -2
fmt.Println(-7 % 3) // -1
}固定宽度整数的运行时运算可能溢出。无符号整数按 取模,程序可以依赖这种回绕;有符号整数也允许溢出,结果由补码、操作数和运算确定,不会因为溢出自动 panic。业务代码不能把“不会报错”误解为“结果正确”。
package main
import "fmt"
func main() {
var u uint8 = 255
u++
fmt.Println(u) // 0
var i int8 = 127
i++
fmt.Println(i) // -128
}如果要拒绝溢出,必须在运算前检查。例如对正整数加法,可以判断 b > math.MaxInt-a。常量溢出不同:编译器会在常量装不进目标类型时直接报错。
位运算把整数当作一排开关:
x & y:两边同为 1 才保留,适合测试交集;x | y:任一边为 1 就置位,适合开启标志;x ^ y:不同为 1,适合切换标志;x &^ y:把 y 中为 1 的位置从 x 清掉;^x:逐位取反;x << n、x >> n:把位模式左移或右移 n 位。package main
import "fmt"
const (
CanRead uint8 = 1 << iota
CanWrite
CanDelete
)
func main() {
permission := CanRead | CanWrite
fmt.Printf("%08b\n", permission) // 00000011
fmt.Println(permission&CanWrite !=
移位次数必须是非负整数。对无符号值右移时高位补 0;对有符号值右移时,结果等价于向负无穷取整的除以 ,在补码表示中表现为高位补符号位。把整数当位集合时,优先使用无符号类型,避免符号扩展干扰判断。

Go 不会在不同整数类型之间做隐式转换。即使 int32 与当前平台的 int 恰好同宽,也要明确写转换。
var apples int32 = 12
var oranges int16 = 3
total := int(apples) + int(oranges)窄化整数转换会保留目标宽度对应的位;整数转浮点可能丢失低位精度;浮点转整数会丢弃小数部分并向零截断。若浮点值超出目标整数范围,结果依实现而定,因此转换前应先检查范围。
package main
import "fmt"
func main() {
var x uint16 = 0x01ff
fmt.Printf("%#x\n", uint8(x)) // 0xff
fmt.Println(int(3.99)) // 3
fmt.Println(int(-3.99)) // -3
整数可以写成十进制、二进制、八进制或十六进制。前缀分别是无前缀、0b、0o、0x;数字中的下划线只用于分组,不改变数值。
package main
import "fmt"
func main() {
flags := 0b0010_0101
perm := 0o755
color := 0xff_80_40
fmt.Printf("%d %o %#x %08b\n", flags, perm, color, flags)
}同类型整数可用 ==、!=、<、<=、>、>= 比较。fmt.Printf 的 %d、%b、%o、%x 分别按十、二、八、十六进制输出;# 标志会补上进制前缀。
float32 和 float64 遵循 IEEE 754 二进制浮点格式。一个浮点值可以理解为“符号 × 有效数字 × 2 的指数”。这让它能覆盖很大的量级,却不能精确表示区间里的所有实数。
float32 大约提供 6 到 7 位十进制有效数字,float64 大约提供 15 到 16 位。除非内存、带宽或外部格式明确要求 float32,一般计算优先用 float64。
math.MaxFloat32 约为 ,math.MaxFloat64 约为 ;对应的最小正非零值可由 math.SmallestNonzeroFloat32 与 math.SmallestNonzeroFloat64 取得。这里的“最小”是最接近 0 的正数,不是最小的负数。
package main
import "fmt"
func main() {
var f float32 = 1 << 24
fmt.Println(f == f+1) // true
}在 附近,float32 相邻可表示值的间距已经达到 2,所以加 1 可能落回原值。浮点数越大,能分辨的绝对间距也越大。

十进制的 0.1 转成二进制是无限循环小数,只能舍入到最近的可表示浮点值。计算误差并不是“Go 算错了”,而是有限位宽表示连续实数的结果。
package main
import "fmt"
func main() {
x := 0.1
y := 0.2
fmt.Printf("%.20f\n", x+y) // 0.30000000000000004441
}比较计算结果时,通常要定义业务允许的误差,而不是直接写 a == b。只用固定绝对误差适合接近 0 的数;跨越多个数量级时,应结合相对误差。
package main
import (
"fmt"
"math"
)
func almostEqual(a, b, absTol, relTol float64) bool {
diff := math.Abs(a - b)
if diff <= absTol {
return true
}
scale :=
金额通常不该直接用二进制浮点数累计。常见做法是用整数保存最小货币单位,或者选择明确的十进制定点/高精度方案。
math.Inf(1)、math.Inf(-1) 分别生成正、负无穷;math.NaN() 生成 NaN。用 math.IsInf 与 math.IsNaN 检测它们。
NaN 与任何值比较都不相等,包括它自己;nan < x 和 nan > x 也都为 false。因此绝不能用 x == math.NaN() 检测 NaN。
package main
import (
"fmt"
"math"
)
func main() {
nan := math.NaN()
fmt.Println(nan == nan) // false
fmt.Println(math.IsNaN(nan)) // true
fmt.Println(math.IsInf(math.Inf(1),
函数如果可能失败,通常返回 (value, error) 或 (value, ok) 比偷偷塞一个 NaN 更清晰,因为调用方不容易忘记检查。
complex64 的实部和虚部是 float32,complex128 的两部分是 float64。内置函数 complex(real, imag) 构造复数,real、imag 取出两部分。
package main
import "fmt"
func main() {
z := complex(1.0, 2.0) // complex128
w := 3 + 4i
fmt.Println(z * w) // (-5+10i)
fmt.Println(real(z), imag(z))
fmt.Println(1i
复数支持加减乘除,也支持 == 和 !=。只有实部和虚部都相等时,两个复数才相等。大小关系没有意义,所以不能用 < 或 >。

复数 可以看成平面上的点 。乘以 相当于逆时针旋转 90°;复数乘法同时改变长度和角度。math/cmplx 提供 Abs、Phase、Sqrt、Exp 等函数。
package main
import (
"fmt"
"math/cmplx"
)
func main() {
z := 3 + 4i
fmt.Println(cmplx.Abs(z)) // 5
fmt.Println(cmplx.Sqrt(-1)) // (0+1i)
}复数适合信号处理、二维旋转、频域计算和分形。若问题本身没有“实部 + 虚部”或平面旋转结构,不必为了少写两个变量而强行使用它。
bool 只有 true 和 false。比较运算产生布尔值,! 取反,&& 表示逻辑与,|| 表示逻辑或。
Go 不会在布尔和整数之间隐式转换。不能把 0 当 false,也不能把 true 当 1。这样写条件时,意图会更明确。
count := 3
hasItems := count != 0
if hasItems {
fmt.Println("有可处理的数据")
}不要写 if ready == true,直接写 if ready;否定时写 if !ready。
&& 的左边为 false 时,右边不再执行;|| 的左边为 true 时,右边不再执行。这不仅是性能细节,还能保护后续表达式。
if s != "" && s[0] == 'G' {
fmt.Println("以 G 开头")
}如果字符串为空,s[0] 本来会越界;短路让它根本不执行。类似地,可以先检查指针不为 nil,再访问其字段。
&& 的优先级高于 ||,但条件一旦混合多组逻辑,我仍建议加括号或拆成有名字的布尔变量,减少阅读时的心算。
Go 字符串保存任意字节,不保证内容一定是 UTF-8。len(s) 返回字节数;s[i] 取第 i 个字节,结果类型是 byte。字符串不可变,不能给 s[i] 赋值。
package main
import "fmt"
func main() {
s := "Go语言"
fmt.Println(len(s)) // 8:G、o 各 1 字节,“语”“言”各 3 字节
fmt.Printf("%x\n", s[2]) // e8:“语”的第一个 UTF-8 字节
}切片表达式 s[i:j] 也按字节边界工作。如果 i 或 j 落在一个多字节编码中间,结果仍是合法的 Go 字符串值,但可能不是合法 UTF-8。
字符串拼接会生成新字符串;把字符串赋给另一个变量不会复制并修改原内容。不可变性让多个字符串安全共享底层只读数据,也让字符串可作为可靠的值参与比较。
“第几个字符”不是一个足够精确的问题。你可能要的是第几个字节、第几个 Unicode 码点,甚至第几个用户眼中的字素。Go 的 string 下标只回答第一个问题。
双引号字符串会处理 \n、\t、\、" 等转义。\xNN 写入一个指定字节;\uNNNN 与 \UNNNNNNNN 表示 Unicode 码点,并把它编码成 UTF-8。
反引号原始字符串不处理反斜杠转义,可以跨行,适合正则、SQL、模板或多行文本。它唯一不能直接包含的是反引号本身。
package main
import "fmt"
func main() {
interpreted := "第一行\n第二行"
raw := `第一行\n第二行`
fmt.Printf("%q\n", interpreted)
fmt.Printf("%q\n", raw)
}单引号写的是 rune 字面量,例如 'A'、'中'、'\n',而不是长度为 1 的字符串。
Unicode 为码点分配编号;Go 用 rune 表示码点。UTF-8 再把一个 rune 编成 1 到 4 个字节:ASCII 占 1 字节,常见汉字通常占 3 字节。
for range 遍历字符串时会解码 UTF-8。索引是当前 rune 起始位置的字节偏移,不是连续的“字符编号”。
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
s := "A中🙂"
fmt.Println(len(s)) // 8
fmt.Println(utf8.RuneCountInString(s)) // 3
for i, r := range s {
fmt.Printf("字节偏移=%d
无效 UTF-8 在解码时会产生替换字符 U+FFFD。需要先拒绝非法文本时,用 utf8.ValidString;需要手动解码时,用 utf8.DecodeRuneInString。把字符串转换为 []rune 可按码点索引,但会分配新存储,也仍不等于按用户感知字符分割。

strings 面向字符串,提供查找、替换、分割、拼接、裁剪与大小写处理。bytes 提供相似 API,但操作可变的 []byte;需要逐步构建字节内容时,可使用 bytes.Buffer。只构建文本时,strings.Builder 更直接。
[]byte(s) 得到可修改的字节切片,string(b) 再生成不可变字符串。语义上,这两种转换都不能让可变字节与字符串互相影响;编译器可以在确认安全时消除实际复制,但代码不能依赖共享底层存储。
package main
import "fmt"
func main() {
s := "Go"
b := []byte(s)
b[0] = 'N'
fmt.Println(s, string(b)) // Go No
}package main
import (
"fmt"
"strings"
)
func main() {
fields := strings.Fields(" Go 语言 ")
fmt.Println(strings.Join(fields, "-")) // Go-语言
var b strings.Builder
b.WriteString("状态")
b.
strconv 负责字符串与标量值互转。Atoi 解析 int;ParseInt、ParseUint 可指定进制和位宽;ParseFloat 可指定 32 或 64 位语义。格式化方向使用 Itoa、FormatInt、FormatFloat。
string(65) 得到的是码点 U+0041 对应的 A,不是十进制文本 65。整数转十进制字符串应使用 strconv.Itoa 或 strconv.FormatInt。
package main
import (
"fmt"
"strconv"
)
func main() {
n, err := strconv.ParseInt("7f", 16, 8)
if err != nil {
panic(err)
}
fmt.Println(n) // 127
fmt.Println(strconv.
别忘了检查解析错误。输入可能包含空格、符号、超出位宽的数或无效数字,静默忽略错误会把坏数据混入后续逻辑。
常量的值在编译期确定,底层只能是布尔、字符串或数字。字面量、命名常量、只含常量操作数的表达式,以及部分内置函数的常量结果,都可以继续成为常量。
const (
AppName = "we-learn"
RetryMax = 3
BufferLen = 1 << 10
Banner = AppName + " / Go"
)常量表达式不会发生运行时整数溢出;数值常量在语言层面拥有任意精度,编译器至少要提供规范要求的高精度表示。直到它被放进具体类型,编译器才检查目标类型能否表示该值。
const Huge = 1 << 100
const Four int8 = Huge >> 98 // 4,可以表示
// const Bad int8 = 128 // 编译错误:溢出 int8除零、越界的常量索引等错误也能在编译期被发现。常量不会产生 NaN 或无穷大;常量除数为 0 直接是编译错误。
显式写类型会得到有类型常量:
const TimeoutSeconds int64 = 30没有显式类型的字面量通常是无类型常量。它有“种类”,但暂未绑定到机器类型,包括无类型布尔、整数、rune、浮点、复数与字符串。无类型常量能在不同上下文中被精确转换,只要目标类型能表示它。
const Pi = 3.141592653589793238462643383279
var f32 float32 = Pi
var f64 float64 = Pi
var z complex128 = Pi无类型常量必须成为变量且没有其他类型线索时,会采用默认类型:
字面量写法会影响常量运算。5/9 是整数常量除法,结果为 0;5.0/9.0 是浮点常量除法,保持精确有理结果,直到转换成具体浮点类型时再舍入。
在一个 const 声明块中,iota 从 0 开始,每经过一个 ConstSpec(通常是一行常量规格)加 1。省略表达式会重复前一个非空表达式及其类型。
type State uint8
const (
StatePending State = iota // 0
StateRunning // 1
StateDone // 2
)iota 常与移位组合,为每个标志分配一个独立比特:
type Feature uint8
const (
FeatureSearch Feature = 1 << iota // 00000001
FeatureExport // 00000010
FeatureShare // 00000100
)空白标识符也占一行,显式写了别的表达式也不会暂停计数。每个新的 const 块都会重新从 0 开始。

面对一个新字段,可以依次问:
类型选择的目标不是“用最小的位数”,而是让非法状态更难出现,让边界行为在代码里看得见。
下面程序综合了无符号回绕、短路、UTF-8 与常量默认类型。先写下每一行输出,再展开答案。
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
var u uint8 = 255
u++
s := "Go中"
safe := s != "" && s[0] == 'G'
x := 3.0
请定义三个互不重叠的标志 Read、Write、Admin,再实现:
下面函数想返回前 n 个 Unicode 码点,但它按字节切片:
func firstN(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n]
}请修复它,并说明空字符串、n 小于等于 0、n 超过 rune 数时如何处理。
每个标志只占一个比特,按位或用于开启,按位与用于测试,AND NOT 用于清除。
range 的索引是 rune 起始处的字节偏移,因此用它切片不会落在合法 UTF-8 编码的中间。n 超过 rune 数时循环结束并返回原字符串。这里按码点截断;若产品要求按用户看到的字素截断,还需要更高层的 Unicode 分段逻辑。