Go-04 基本数据类型详解
Go 的基本数据类型看似简单,却藏着大量容易踩坑的细节:int 到底几字节?为什么 0.1 + 0.2 != 0.3?byte 和 rune 有什么区别?哪些类型能用 == 直接比较?本章把这些问题一次讲透。
1. 整型家族
Go 的整型分为有符号和无符号两大类,并按位宽划分。
1.1 完整类型列表
package main
import "fmt"
func main() {
// 有符号整型(可为负)
var a int8 = -128 // 8 位:-128 ~ 127
var b int16 = 32767
var c int32 = 2147483647
var d int64 = 9223372036854775807
// 无符号整型(不能为负)
var e uint8 = 255 // 8 位:0 ~ 255
var f uint16 = 65535
var g uint32 = 4294967295
var h uint64 = 18446744073709551615
// 平台相关类型
var i int // 32 位平台为 int32,64 位平台为 int64
var j uint // 同上
var k uintptr // 足以存放一个指针的无符号整数
fmt.Println(a, b, c, d, e, f, g, h, i, j, k)
}
1.2 平台相关的 int 位数
int 和 uint 的宽度取决于目标平台,不是固定的:
- 在 64 位系统(amd64、arm64)上,
int= 64 位 - 在 32 位系统(386、arm)上,
int= 32 位
package main
import (
"fmt"
"unsafe"
)
func main() {
var i int
// 用 unsafe.Sizeof 查看当前平台 int 占用字节数
fmt.Printf("int 占用 %d 字节\n", unsafe.Sizeof(i)) // 64 位平台输出 8
var p uintptr
fmt.Printf("uintptr 占用 %d 字节\n", unsafe.Sizeof(p)) // 与指针等宽,通常 8
}
重要结论:永远不要假设
int是 32 位。需要固定宽度时(如网络协议、文件格式、跨平台序列化),显式使用int32/int64。
1.3 各类型取值范围表
| 类型 | 位宽 | 取值范围 | 说明 |
|---|---|---|---|
| int8 | 8 | -128 ~ 127 | 有符号 |
| int16 | 16 | -32768 ~ 32767 | 有符号 |
| int32 | 32 | -2^31 ~ 2^31-1 | 别名 rune |
| int64 | 64 | -2^63 ~ 2^63-1 | 有符号 |
| uint8 | 8 | 0 ~ 255 | 别名 byte |
| uint16 | 16 | 0 ~ 65535 | 无符号 |
| uint32 | 32 | 0 ~ 2^32-1 | 无符号 |
| uint64 | 64 | 0 ~ 2^64-1 | 无符号 |
| int | 平台相关 | 32 位或 64 位 | 默认整型 |
| uint | 平台相关 | 32 位或 64 位 | 无符号默认 |
| uintptr | 平台相关 | 与指针等宽 | 存指针数值 |
ASCII 图直观展示位宽关系:
8位 16位 32位 64位
int8 [########]
int16 [################]
int32 [################################]
int64 [################################################################]
有符号:最高位为符号位(0 正 1 负)
┌─符号位
│
v
[S|xxxxxxx] int8
无符号:全部位表示数值
[xxxxxxxx] uint8
2. 整数溢出与回绕
Go 的整数运算不会自动检测溢出,超出范围时会静默回绕(wrap around),这是编译期确定类型、运行期无检查的必然结果。
2.1 有符号溢出
package main
import (
"fmt"
"math"
)
func main() {
var x int8 = math.MaxInt8 // 127
x++ // 溢出,回绕到最小值
fmt.Println(x) // -128
var y int8 = math.MinInt8 // -128
y-- // 下溢,回绕到最大值
fmt.Println(y) // 127
}
原理:int8 用补码表示,127 的二进制是 0111 1111,加 1 变成 1000 0000,补码解释为 -128。
2.2 无符号回绕
package main
import "fmt"
func main() {
var u uint8 = 0
u-- // 0 减 1 回绕
fmt.Println(u) // 255
var v uint8 = 255
v++ // 255 加 1 回绕
fmt.Println(v) // 0
// 经典陷阱:无符号数相减做倒序循环会死循环
for i := uint8(3); i >= 0; i-- {
fmt.Println(i) // 3 2 1 0 255 254 ... 永不结束!
if i == 0 { // 需要额外条件跳出
break
}
}
}
2.3 如何检测溢出
Go 标准库没有内置的溢出检查运算,需要手动判断:
package main
import (
"fmt"
"math"
)
// 安全加法:检测 int 相加是否溢出
func safeAdd(a, b int) (int, bool) {
if b > 0 && a > math.MaxInt-b {
return 0, false // 正向溢出
}
if b < 0 && a < math.MinInt-b {
return 0, false // 负向溢出
}
return a + b, true
}
func main() {
sum, ok := safeAdd(math.MaxInt, 1)
fmt.Println(sum, ok) // 0 false
sum, ok = safeAdd(100, 200)
fmt.Println(sum, ok) // 300 true
}
Go 1.22+ 也可结合 math/bits 包中的加法带进位函数:
package main
import (
"fmt"
"math/bits"
)
func main() {
// bits.Add64 返回和与进位 carry
sum, carry := bits.Add64(math.MaxUint64, 1, 0)
fmt.Println(sum, carry) // 0 1,carry=1 表示发生了溢出
}
2.4 math 包中的边界常量
package main
import (
"fmt"
"math"
)
func main() {
fmt.Println(math.MaxInt) // 平台相关,64 位为 9223372036854775807
fmt.Println(math.MinInt) // -9223372036854775808
fmt.Println(math.MaxInt8) // 127
fmt.Println(math.MaxUint8) // 255
fmt.Println(math.MaxInt32) // 2147483647
fmt.Println(math.MaxInt64) // 9223372036854775807
// math.MaxUint64 是无类型常量,不能直接赋给 int
var u uint64 = math.MaxUint64
fmt.Println(u)
}
3. 浮点型
Go 提供 float32 和 float64,均遵循 IEEE 754 标准。默认浮点字面量类型为 float64。
3.1 IEEE 754 表示
浮点数由符号位、指数位、尾数位组成:
float32(单精度,32 位)
┌─┬────────┬───────────────────────┐
│S│ Exp │ Mantissa │
│1│ 8 bits │ 23 bits │
└─┴────────┴───────────────────────┘
float64(双精度,64 位)
┌─┬───────────┬──────────────────────────────────────┐
│S│ Exp │ Mantissa │
│1│ 11 bits │ 52 bits │
└─┴───────────┴──────────────────────────────────────┘
值 = (-1)^S × 1.尾数 × 2^(指数-偏移)
float32:约 6~7 位十进制有效数字float64:约 15~16 位十进制有效数字
3.2 精度丢失经典案例
package main
import "fmt"
func main() {
// 0.1 和 0.2 无法用二进制精确表示
fmt.Println(0.1 + 0.2) // 0.30000000000000004
fmt.Println(0.1+0.2 == 0.3) // false!
// float32 精度更低
var f float32 = 0.1
fmt.Printf("%.20f\n", f) // 0.10000000149011611938
}
原因:0.1 在二进制中是无限循环小数 0.0001100110011...,尾数位有限只能截断,产生舍入误差。就像十进制无法精确表示 1/3 一样。
3.3 比较浮点数的正确方式
不能用 == 直接比较浮点数,应判断差值是否在容忍误差(epsilon)内:
package main
import (
"fmt"
"math"
)
// 判断两个浮点数是否"足够接近"
func almostEqual(a, b, epsilon float64) bool {
return math.Abs(a-b) < epsilon
}
func main() {
x := 0.1 + 0.2
y := 0.3
fmt.Println(x == y) // false
fmt.Println(almostEqual(x, y, 1e-9)) // true
// 对于大数,应使用相对误差
a, b := 1e10, 1e10+1
rel := math.Abs(a-b) / math.Max(math.Abs(a), math.Abs(b))
fmt.Println(rel < 1e-9) // true
}
3.4 NaN 与 Inf
package main
import (
"fmt"
"math"
)
func main() {
inf := math.Inf(1) // 正无穷
ninf := math.Inf(-1) // 负无穷
nan := math.NaN() // Not a Number
fmt.Println(1.0/0.0 == inf) // 注意:字面量 1.0/0.0 编译报错,需变量
var zero float64
fmt.Println(1.0 / zero) // +Inf
fmt.Println(-1.0 / zero) // -Inf
fmt.Println(zero / zero) // NaN
// NaN 的诡异特性:不等于任何值,包括它自己
fmt.Println(nan == nan) // false
fmt.Println(math.IsNaN(nan)) // true,正确的判断方式
fmt.Println(math.IsInf(inf, 1)) // true
_ = ninf
}
关键点:判断 NaN 必须用
math.IsNaN(),因为nan == nan恒为false。这也导致含 NaN 的 map key、切片排序会出现异常行为。
4. 布尔与复数类型
4.1 布尔 bool
bool 只有 true 和 false 两个值,且不能与整型互相转换(与 C 语言不同):
package main
import "fmt"
func main() {
var b bool = true
// 下面这些在 Go 中都是编译错误:
// var i int = int(b) // 错误:无法转换 bool 到 int
// if 1 { } // 错误:非布尔条件
// b = 1 // 错误:类型不匹配
// 正确做法:显式判断
var n int = 0
if n != 0 {
b = true
}
fmt.Println(b)
}
4.2 复数 complex64 / complex128
package main
import "fmt"
func main() {
// complex128 由两个 float64 组成,complex64 由两个 float32 组成
var c1 complex128 = complex(3, 4) // 3 + 4i
c2 := 1 + 2i // 字面量,默认 complex128
fmt.Println(c1) // (3+4i)
fmt.Println(real(c1)) // 3,实部
fmt.Println(imag(c1)) // 4,虚部
fmt.Println(c1 + c2) // (4+6i)
fmt.Println(c1 * c2) // (-5+10i)
var c3 complex64 = complex(float32(1), float32(1))
fmt.Println(c3)
}
复数在科学计算、信号处理领域有用,日常业务开发几乎用不到,了解即可。
5. byte 与 rune
这是 Go 字符串处理的核心概念,也是面试高频考点。
5.1 本质:它们是别名
// Go 源码中的定义
// type byte = uint8 // byte 是 uint8 的别名
// type rune = int32 // rune 是 int32 的别名
byte=uint8,表示一个字节(0~255)rune=int32,表示一个Unicode 码点(一个字符)
5.2 Unicode 码点与 UTF-8 编码
Go 源码文件是 UTF-8 编码,字符串底层是 UTF-8 字节序列。一个中文字符通常占 3 个字节:
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
s := "Go语言"
// len 返回字节数,不是字符数!
fmt.Println(len(s)) // 8(Go=2字节 + 语言各3字节)
fmt.Println(utf8.RuneCountInString(s)) // 4(G o 语 言)
// 转成 []byte 看底层字节
fmt.Println([]byte(s)) // [71 111 232 175 173 232 168 128]
// 转成 []rune 看字符
fmt.Println([]rune(s)) // [71 111 35821 35328]
}
UTF-8 编码规则(变长):
码点范围 字节数 编码格式
U+0000 ~ U+007F 1 0xxxxxxx (ASCII 兼容)
U+0080 ~ U+07FF 2 110xxxxx 10xxxxxx
U+0800 ~ U+FFFF 3 1110xxxx 10xxxxxx 10xxxxxx (多数中文在此)
U+10000 ~ U+10FFFF 4 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
5.3 遍历字符串的坑
package main
import "fmt"
func main() {
s := "a语b"
// 坑:用下标索引得到的是【字节】,中文会被拆散
fmt.Println("=== 按字节索引 ===")
for i := 0; i < len(s); i++ {
fmt.Printf("%d: %d\n", i, s[i]) // s[i] 是 byte,中文字符输出乱码字节
}
// 正确:用 range 遍历,得到的是【rune】和字节起始下标
fmt.Println("=== range 遍历 ===")
for i, r := range s {
// i 是该字符首字节的下标,r 是 rune
fmt.Printf("下标 %d: 字符 %c (码点 %d)\n", i, r, r)
}
// 输出:
// 下标 0: 字符 a (码点 97)
// 下标 1: 字符 语 (码点 35821)
// 下标 4: 字符 b (码点 98) ← 下标从 1 跳到 4,因为"语"占 3 字节
}
记忆口诀:
for i := range s按字节步进但按字符取值,s[i]永远是字节。处理中文务必用range或转[]rune。
5.4 单引号是 rune,双引号是 string
package main
import "fmt"
func main() {
var r rune = '中' // 单引号 = 字符字面量,类型是 rune(int32)
var s string = "中" // 双引号 = 字符串
var b byte = 'A' // ASCII 字符可以放进 byte
fmt.Printf("%c %d\n", r, r) // 中 20013
fmt.Println(s)
fmt.Printf("%c\n", b) // A
}
6. 类型转换
Go 是强类型语言,不同类型间不会自动转换,必须显式转换(一个例外是无类型常量)。
6.1 显式转换语法
package main
import "fmt"
func main() {
var i int = 100
var f float64 = float64(i) // int -> float64
var u uint = uint(f) // float64 -> uint(截断小数)
var i8 int8 = int8(i) // int -> int8
fmt.Println(f, u, i8) // 100 100 100
// Go 不允许隐式转换,下面会编译报错:
// var x float64 = i // 错误:cannot use i (int) as float64
}
6.2 数值转换的截断
package main
import "fmt"
func main() {
// float 转 int 直接截断小数部分(向零取整),不是四舍五入
fmt.Println(int(3.99)) // 3
fmt.Println(int(-3.99)) // -3
// 需要四舍五入用 math.Round
// fmt.Println(int(math.Round(3.99))) // 4
}
6.3 不同整型转换的溢出
大范围类型转小范围类型时,高位被直接丢弃:
package main
import "fmt"
func main() {
var big int32 = 300
var small int8 = int8(big) // 300 超出 int8 范围
fmt.Println(small) // 44(300 % 256 = 44,实际是取低 8 位)
var neg int32 = -1
var u uint8 = uint8(neg) // -1 的低 8 位全为 1
fmt.Println(u) // 255
}
6.4 strconv 字符串转换
数值和字符串之间的转换不能用 int(s),必须用 strconv 包:
package main
import (
"fmt"
"strconv"
)
func main() {
// 字符串 -> 数值
i, err := strconv.Atoi("123") // string -> int
fmt.Println(i, err) // 123 <nil>
f, _ := strconv.ParseFloat("3.14", 64) // string -> float64
fmt.Println(f) // 3.14
b, _ := strconv.ParseBool("true") // string -> bool
fmt.Println(b) // true
// 指定进制解析:ParseInt(字符串, 进制, 位宽)
n, _ := strconv.ParseInt("ff", 16, 32) // 十六进制
fmt.Println(n) // 255
// 数值 -> 字符串
s1 := strconv.Itoa(456) // int -> string
s2 := strconv.FormatFloat(3.14, 'f', 2, 64) // float -> string,保留 2 位
fmt.Println(s1, s2) // 456 3.14
// 注意:string(65) 不是 "65",而是 rune 65 对应的字符 "A"!
fmt.Println(string(rune(65))) // A(Go 1.15+ 对 string(int) 会告警)
}
陷阱:
string(97)得到的是"a"(码点转字符),不是"97"。数字转字符串一律用strconv.Itoa。
7. 类型别名 vs 类型定义
这是两个容易混淆但本质不同的概念。
7.1 语法区别
// 类型别名(Type Alias):用 = 号,完全等价,同一个类型
type ByteAlias = byte // ByteAlias 就是 byte,就是 uint8
// 类型定义(Defined Type):无 = 号,创建全新类型
type MyInt int // MyInt 是一个新类型,底层类型是 int
7.2 别名:完全等价
byte 和 rune 就是别名,编译器视其与 uint8/int32 完全相同:
package main
import "fmt"
type Celsius = float64 // 别名
func main() {
var c Celsius = 36.5
var f float64 = c // 无需转换,因为它们是同一个类型
fmt.Println(f) // 36.5
// byte 和 uint8 可以自由互赋值
var x byte = 10
var y uint8 = x
fmt.Println(y)
}
7.3 类型定义:全新类型
package main
import "fmt"
type MyInt int
func main() {
var a MyInt = 5
var b int = 10
// a + b 编译错误!MyInt 和 int 是不同类型
// fmt.Println(a + b)
// 必须显式转换
fmt.Println(int(a) + b) // 15
}
7.4 方法集的区别
这是最关键的差异:类型定义可以拥有独立的方法,别名不行。
package main
import "fmt"
type Celsius float64 // 类型定义
// 为新类型定义方法(别名做不到,因为别名不能给原类型加方法集之外的方法)
func (c Celsius) ToFahrenheit() float64 {
return float64(c)*9/5 + 32
}
func main() {
temp := Celsius(100)
fmt.Println(temp.ToFahrenheit()) // 212
}
对比总结:
| 特性 | 类型别名 type A = B |
类型定义 type A B |
|---|---|---|
| 是否新类型 | 否,与 B 完全等价 | 是,全新类型 |
| 能否互相赋值 | 可以(同一类型) | 需显式转换 |
| 能否定义新方法 | 不能 | 能 |
| 典型用途 | 兼容重构、简化名称 | 领域建模、加行为 |
| 例子 | byte、rune |
time.Duration |
8. 类型可比较性
== 和 != 并非对所有类型都可用。理解可比较性对写 map key、用作接口值至关重要。
8.1 可比较的类型
package main
import "fmt"
func main() {
// 基本类型都可比较
fmt.Println(1 == 1) // 布尔、整型、浮点、字符串
fmt.Println("a" == "a")
// 指针:比较是否指向同一地址
x, y := 1, 1
fmt.Println(&x == &y) // false
fmt.Println(&x == &x) // true
// channel:比较是否同一个 channel
ch := make(chan int)
fmt.Println(ch == ch) // true
}
8.2 数组可比较(元素类型可比较时)
package main
import "fmt"
func main() {
a := [3]int{1, 2, 3}
b := [3]int{1, 2, 3}
fmt.Println(a == b) // true,逐元素比较
c := [3]int{1, 2, 4}
fmt.Println(a == c) // false
// 元素不可比较的数组也不可比较,如 [3][]int 不能用 ==
}
8.3 结构体可比较(所有字段可比较时)
package main
import "fmt"
type Point struct {
X, Y int
}
type Bad struct {
Data []int // 含 slice 字段,整个结构体不可比较
}
func main() {
p1 := Point{1, 2}
p2 := Point{1, 2}
fmt.Println(p1 == p2) // true,逐字段比较
// b1 := Bad{[]int{1}}
// b2 := Bad{[]int{1}}
// fmt.Println(b1 == b2) // 编译错误:struct containing []int cannot be compared
}
可比较性 × 类型定义/别名(呼应 §7):即使两个结构体字段完全相同,只要是不同的 defined type 就不能直接 ==;但可以先强制转换再比较(要求字段顺序、类型、名称完全一致):
type T2 struct{ Name string; Age int }
type T22 T2 // 类型定义:全新类型
type T222 = T2 // 类型别名:完全等价
type T3 struct{ Name string; Age int } // 字段与 T2 相同,但是不同类型
func main() {
var t2 T2
fmt.Println(t2 == T222{}) // true:别名是同一类型
// fmt.Println(t2 == T22{}) // 编译错误:T2 与 T22 是不同类型
// fmt.Println(t2 == T3{}) // 编译错误:字段相同也不行
fmt.Println(t2 == T2(T3{})) // OK:先把 T3 强转成 T2 再比较
}
8.4 不可比较的类型:slice、map、func
package main
func main() {
// 这三类只能和 nil 比较,不能互相比较
var s []int
_ = s == nil // 允许,仅能与 nil 比
// var s1, s2 []int
// _ = s1 == s2 // 编译错误:slice can only be compared to nil
var m map[string]int
_ = m == nil // 允许
var f func()
_ = f == nil // 允许
}
原因:slice、map、func 都是引用类型,逐元素比较语义模糊且代价高,Go 干脆禁止。需要深度比较用 reflect.DeepEqual。
8.5 接口比较陷阱
接口比较需要动态类型和动态值都相等,且若动态类型不可比较会 panic:
package main
import "fmt"
func main() {
var a interface{} = 1
var b interface{} = 1
fmt.Println(a == b) // true,类型 int 且值都为 1
var c interface{} = 1
var d interface{} = int64(1)
fmt.Println(c == d) // false!动态类型不同(int vs int64)
// 陷阱:接口装了 slice,比较时运行时 panic
var e interface{} = []int{1}
var f interface{} = []int{1}
defer func() {
if r := recover(); r != nil {
fmt.Println("panic:", r) // panic: comparing uncomparable type []int
}
}()
fmt.Println(e == f) // 运行时 panic
}
8.6 可比较性速查表
| 类型 | 可比较 | 备注 |
|---|---|---|
| bool/数值/string | 是 | 按值比较 |
| 指针 | 是 | 比较地址 |
| channel | 是 | 比较是否同一个 |
| 数组 | 条件 | 元素类型可比较时 |
| 结构体 | 条件 | 所有字段可比较时 |
| 接口 | 条件 | 动态类型不可比较时运行时 panic |
| slice | 否 | 只能与 nil 比 |
| map | 否 | 只能与 nil 比 |
| func | 否 | 只能与 nil 比 |
9. 优化与最佳实践
9.1 选择合适的整型宽度
// 默认场景:直接用 int,它是 CPU 原生字长,效率最高
count := 0
// 大量数据时用最小够用的类型,节省内存
type Pixel struct {
R, G, B, A uint8 // 4 字节,而非 4 个 int(32 字节)
}
// 网络协议 / 文件格式:用固定宽度类型保证跨平台一致
type Header struct {
Version uint16
Length uint32
}
对比:一个 100 万元素的切片,[]uint8 占约 1MB,[]int64 占约 8MB。
9.2 金额禁用浮点
浮点精度问题在财务场景是灾难,应使用整数(以最小单位计)或 decimal 库:
package main
import "fmt"
func main() {
// 反例:浮点累加金额
var total float64
for i := 0; i < 10; i++ {
total += 0.1
}
fmt.Println(total) // 0.9999999999999999,不是 1!
// 正例1:用整数存"分",避免小数
var cents int64 = 0
for i := 0; i < 10; i++ {
cents += 10 // 0.1 元 = 10 分
}
fmt.Printf("%.2f 元\n", float64(cents)/100) // 1.00 元
// 正例2:生产环境用 github.com/shopspring/decimal
}
9.3 位运算做标志位
用整型的每一位表示一个开关,节省空间且操作高效:
package main
import "fmt"
// 用 iota 配合位移定义标志位
const (
FlagRead = 1 << iota // 1 (0001)
FlagWrite // 2 (0010)
FlagExecute // 4 (0100)
FlagDelete // 8 (1000)
)
func main() {
var perm int
perm |= FlagRead | FlagWrite // 置位:开启读写
fmt.Printf("%04b\n", perm) // 0011
// 检查某位是否开启
fmt.Println(perm&FlagRead != 0) // true
fmt.Println(perm&FlagExecute != 0) // false
perm &^= FlagWrite // AND NOT:清除写权限
fmt.Printf("%04b\n", perm) // 0001
perm ^= FlagRead // XOR:翻转读权限
fmt.Printf("%04b\n", perm) // 0000
}
常用位运算符:
| 运算符 | 含义 | 用途 |
|---|---|---|
& |
按位与 | 检查/清除某位 |
| |
按位或 | 置位 |
^ |
按位异或/取反 | 翻转位 |
&^ |
按位清除 (AND NOT) | 清除指定位 |
<< |
左移 | 乘 2^n、定义标志 |
>> |
右移 | 除 2^n |
10. 高频面试题
Q1:int 在 Go 中占几个字节?
答:取决于平台。64 位系统上是 8 字节(64 位),32 位系统上是 4 字节(32 位)。Go 规范只保证 int 至少 32 位。需要固定宽度必须用 int32/int64。可用 unsafe.Sizeof 验证。
Q2:byte 和 rune 有什么区别?
答:byte 是 uint8 的别名,代表一个字节(0~255);rune 是 int32 的别名,代表一个 Unicode 码点(一个字符)。字符串底层是 UTF-8 字节序列,len(s) 返回字节数,[]rune(s) 才能按字符处理。一个中文在 UTF-8 下通常占 3 个字节(即 3 个 byte,但只是 1 个 rune)。
Q3:浮点数为什么不精确?0.1+0.2 为什么不等于 0.3?
答:浮点数遵循 IEEE 754,用二进制表示。0.1、0.2 在二进制中是无限循环小数,尾数位有限只能截断舍入,累积误差导致 0.1+0.2 = 0.30000000000000004。比较浮点数要用误差范围 math.Abs(a-b) < epsilon,金额计算应用整数或 decimal。
Q4:Go 中哪些类型可以直接用 == 比较?
答:布尔、数值、字符串、指针、channel 恒可比较;数组在元素可比较时可比较;结构体在所有字段可比较时可比较;接口在动态类型可比较时可比较(否则运行时 panic)。slice、map、func 不可比较,只能与 nil 比。深度比较用 reflect.DeepEqual。
Q5:int 类型转换会溢出吗?大类型转小类型会怎样?
答:会。Go 整数运算不检测溢出,超范围静默回绕(补码规则)。大类型转小类型时保留低位、丢弃高位,如 int8(300) 得 44、uint8(-1) 得 255。需要安全转换要先做范围检查,或用 math/bits 辅助检测进位。
Q6:bool 能和整型互相转换吗?
答:不能。Go 中 bool 与整型是完全独立的类型,int(true) 和 if 1 都是编译错误(区别于 C)。必须显式写条件判断,如 if n != 0。
Q7:string(65) 得到什么?
答:得到 "A",不是 "65"。因为 string(int) 会把整数当作 Unicode 码点转成对应字符(65 是 ‘A’ 的码点)。Go 1.15+ 对此会给出 vet 告警。数字转字符串应使用 strconv.Itoa(65) 得到 "65"。
Q8:类型别名和类型定义有什么区别?
答:别名 type A = B 用等号,A 与 B 完全等价、可自由互赋值、不能加新方法(如 byte、rune);类型定义 type A B 无等号,创建全新类型,赋值需显式转换,可以拥有独立的方法集(如 time.Duration)。
小结
本章系统梳理了 Go 的基本数据类型,核心要点如下:
- 整型:
int/uint位宽平台相关(64 位系统为 8 字节),跨平台场景用固定宽度类型;各类型有明确取值范围。 - 溢出:整数运算不检测溢出,超范围静默回绕,需手动检查或借助
math/bits;math.MaxInt等常量提供边界值。 - 浮点:遵循 IEEE 754,天然不精确,
0.1+0.2 != 0.3;比较用 epsilon 误差,NaN 用math.IsNaN判断,金额禁用浮点。 - 布尔/复数:
bool不能与整型转换;复数了解即可。 - byte 与 rune:
byte=uint8(字节),rune=int32(码点);字符串是 UTF-8 字节流,遍历中文用range或转[]rune。 - 类型转换:必须显式,数值转换会截断/溢出,字符串转换用
strconv而非string(int)。 - 别名 vs 定义:别名完全等价无新方法,类型定义是新类型可带方法集。
- 可比较性:基本类型/指针/channel 可比较,数组/结构体条件可比较,slice/map/func 不可比较,接口比较可能 panic。
- 最佳实践:合理选整型宽度省内存,金额用整数或 decimal,标志位用位运算。
掌握这些类型细节,能帮你避开 Go 开发中最常见的一类隐蔽 bug。下一章我们将进入数组与切片,拆解切片的底层结构(array/len/cap)与扩容机制。
xingliuhua