数据类型与零值
问题
Go 有哪些数据类型?零值机制是什么?类型转换有什么规则?
答案
基本类型
Go 有以下基本数据类型:
| 分类 | 类型 | 大小 | 零值 | 说明 |
|---|---|---|---|---|
| 布尔 | bool | 1 字节 | false | |
| 整数 | int8/int16/int32/int64 | 1/2/4/8 字节 | 0 | 有符号整数 |
uint8/uint16/uint32/uint64 | 1/2/4/8 字节 | 0 | 无符号整数 | |
int/uint | 4 或 8 字节 | 0 | 平台相关(32/64位) | |
uintptr | 4 或 8 字节 | 0 | 存放指针的整数类型 | |
| 浮点 | float32/float64 | 4/8 字节 | 0.0 | IEEE 754 |
| 复数 | complex64/complex128 | 8/16 字节 | 0+0i | |
| 字符串 | string | 16 字节(header) | "" | 不可变字节序列 |
| 别名 | byte(= uint8)、rune(= int32) | 1/4 字节 | 0 | byte 处理 ASCII,rune 处理 Unicode |
复合类型
// 数组 —— 固定长度,值类型
var arr [3]int // [0, 0, 0]
// 结构体 —— 字段集合,值类型
type Point struct {
X, Y float64
}
var p Point // {0, 0}
引用类型
var sl []int // nil(nil slice)
var m map[string]int // nil(nil map,不能写入!)
var ch chan int // nil
var fn func() // nil
var ptr *int // nil
var itf interface{} // nil
nil map 可以读(返回零值),但不能写入——写入会 panic。必须先 make 初始化:
var m map[string]int
_ = m["key"] // OK,返回 0
// m["key"] = 1 // panic: assignment to entry in nil map
m = make(map[string]int)
m["key"] = 1 // OK
零值机制
Go 的零值(Zero Value)保证所有变量声明后都有确定的初始值,不存在"未初始化"的情况:
var b bool // false
var i int // 0
var f float64 // 0.0
var s string // ""
var p *int // nil
var sl []int // nil
Go 标准库大量利用零值可用性设计。例如 sync.Mutex 零值就是未锁定的有效互斥锁,bytes.Buffer 零值就是空缓冲区,无需调用构造函数:
var mu sync.Mutex // 直接使用,无需 NewMutex()
mu.Lock()
defer mu.Unlock()
var buf bytes.Buffer // 直接使用,无需 NewBuffer()
buf.WriteString("hello")
类型转换
Go 不支持隐式类型转换,必须显式转换:
var i int = 42
var f float64 = float64(i) // int → float64
var u uint = uint(f) // float64 → uint
// 字符串与字节切片互转
s := "hello"
b := []byte(s) // string → []byte(拷贝)
s2 := string(b) // []byte → string(拷贝)
// 字符串与 rune 切片互转(处理中文等多字节字符)
r := []rune("你好") // [20320, 22909]
s3 := string(r) // "你好"
// int → string(注意!不是数字转字符串)
s4 := string(65) // "A"(Unicode 码点 65)
s5 := strconv.Itoa(65) // "65"(数字转字符串用 strconv)
string(intValue) 是把整数当 Unicode 码点转为字符,不是数字转字符串!数字转字符串要用 strconv.Itoa() 或 fmt.Sprintf()。
类型别名与类型定义
// 类型定义(创建新类型)
type UserID int64 // UserID 和 int64 是不同类型
var id UserID = 100
// var n int64 = id // 编译错误!需要显式转换
// 类型别名(只是别名,完全相同)
type Byte = uint8 // byte 的官方定义方式
type Rune = int32 // rune 的官方定义方式
var b byte = 1
var u uint8 = b // OK,byte 和 uint8 是同一类型
| 特性 | 类型定义 type A B | 类型别名 type A = B |
|---|---|---|
| 是否新类型 | ✅ 新类型 | ❌ 完全相同 |
| 能否互相赋值 | ❌ 需要显式转换 | ✅ 直接赋值 |
| 能否定义方法 | ✅ 可以 | ❌ 不能(除非在同一包内) |
| 使用场景 | 领域建模、增强语义 | 渐进式重构、跨包兼容 |
常见面试问题
Q1: Go 中 int 和 int64 有什么区别?
答案:
int 在 32 位系统上是 32 位,在 64 位系统上是 64 位,大小取决于平台。int64 在任何平台上都是 64 位。int 和 int64 是不同类型,不能直接互相赋值,需要显式转换。
在实际开发中,通常使用 int 即可。只有在序列化协议、跨平台一致性等场景下才需要使用固定大小的 int64。
Q2: Go 的零值有什么好处?
答案:
- 消除未初始化变量:不会出现 Java 中的
NullPointerException或 C 中的随机值 - 零值可用设计:
sync.Mutex、bytes.Buffer等类型的零值就是可用状态,无需构造函数 - 简化代码:减少
if xxx != nil的判断(nil slice 可以append,nil map 可以读) - 安全性:指针零值为 nil,访问 nil 指针会触发明确的 panic,而不是访问随机内存
Q3: byte 和 rune 有什么区别?什么时候用哪个?
答案:
byte是uint8的别名,表示一个字节(0-255),用于处理 ASCII 字符和原始字节数据rune是int32的别名,表示一个 Unicode 码点,用于处理多字节字符(如中文、emoji)
s := "你好Go"
fmt.Println(len(s)) // 8(字节数:中文3字节×2 + Go 2字节)
fmt.Println(len([]rune(s))) // 4(字符数)
for i, b := range []byte(s) { // 遍历字节
fmt.Printf("%d: %x\n", i, b)
}
for i, r := range s { // range 字符串默认遍历 rune
fmt.Printf("%d: %c\n", i, r)
}
处理文本内容(尤其是多语言)用 rune,处理二进制数据或网络协议用 byte。
Q4: 为什么 Go 不支持隐式类型转换?
答案:
Go 的设计哲学是显式优于隐式。隐式类型转换虽然方便,但容易出现精度丢失、语义错误等隐蔽 Bug(如 C 语言中 int 与 unsigned int 混合运算的经典问题)。Go 通过强制显式转换,让开发者明确知道发生了什么,减少意外行为。
Q5: new 和 make 的区别?
答案:
| 维度 | new(T) | make(T, args) |
|---|---|---|
| 适用类型 | 任意类型 | 仅 slice、map、channel |
| 返回值 | *T(指针) | T(值) |
| 做了什么 | 分配内存,填零值 | 分配 + 初始化内部数据结构 |
| 常见替代 | &T{} 更常用 | 无替代 |
p := new(int) // *int,指向 0
s := make([]int, 5) // []int,长度 5,已初始化
m := make(map[string]int) // map[string]int,已初始化可写入
实际开发中 new 很少使用,通常用 &T{} 替代。make 是 slice/map/channel 初始化的唯一方式。
Q6: Go 有枚举类型吗?如何实现枚举?
答案:
Go 没有 enum 关键字,通过 const + iota 实现枚举:
type Weekday int
const (
Sunday Weekday = iota // 0
Monday // 1
Tuesday // 2
Wednesday // 3
Thursday // 4
Friday // 5
Saturday // 6
)
// 还可以实现 String() 方法用于打印
func (d Weekday) String() string {
names := [...]string{"Sunday", "Monday", "Tuesday",
"Wednesday", "Thursday", "Friday", "Saturday"}
if d < Sunday || d > Saturday {
return "Unknown"
}
return names[d]
}