5.Go 基本数据类型

背景与动机#

Go 的基本数据类型不只是“能装什么值”,更决定了表达式如何计算、默认值是什么、能否比较、以及不同类型之间是否允许直接混用。这个主题表面上像语法入门,实际是后面数组、切片、结构体、接口、并发乃至性能优化的基础。

学习这一节,核心要弄清 5 件事:

  1. 整型和浮点型各自的值域、精度和默认选型。
  2. bool 为什么保持极简,没有“数字即真”的隐式规则。
  3. string 在 Go 里到底是“字符序列”还是“字节序列”。
  4. Go 为什么要求大多数类型转换必须显式写出来。
  5. “转换”与“格式化/解析”不是一回事。

核心原理拆解#

1. 整型:有符号、无符号、平台相关大小要分清#

Go 的整型分为两类:

  • 有符号整型:int8int16int32int64int
  • 无符号整型:uint8uint16uint32uint64uintuintptr

还存在两个常见别名:

  • byteuint8 的别名
  • runeint32 的别名,常用于表示 Unicode 码点

固定宽度类型的语义最稳定,例如:

  • int32 永远是 32 位
  • uint64 永远是 64 位

intuint 是平台相关大小:

  • 32 位架构上通常是 32 位
  • 64 位架构上通常是 64 位

Go 规范要求整数使用二进制补码语义理解。工程上最重要的结论不是“底层怎么存”,而是:

  • 需要稳定数据宽度时,用 int32uint64 这类固定类型
  • 只做普通计数、索引、长度运算时,优先用 int

image.png
image.png

2. 浮点型:遵循 IEEE 754,但不能拿来做“精确小数”#

Go 的浮点型主要是:

  • float32
  • float64

它们遵循 IEEE 754 浮点标准。默认情况下,浮点字面量如果没有额外约束,通常会落到 float64 语义中。

浮点型适合表达:

  • 科学计算
  • 比例、平均值、连续量
  • 允许存在舍入误差的数值

不适合直接表达:

  • 金额
  • 高精度统计结果
  • 需要严格十进制精确性的业务值

原因不是 Go 特有,而是二进制浮点无法精确表示很多十进制小数,例如 0.10.2

典型现象:

0.1 + 0.2 != 0.3

在很多语言里都可能成立。Go 这里没有“帮你修正直觉”,而是保持底层数值模型的真实语义。

3. 布尔型:只有 truefalse,没有隐式 truthy/falsy#

Go 的布尔型只有一个预声明类型:

bool

它只表示两个值:

  • true
  • false

这看起来简单,但设计上很重要。Go 不允许把整数、指针、字符串隐式当成布尔值使用。也就是说,下面这种写法非法:

if 1 { }
if "hello" { }

必须写成明确判断:

if n != 0 { }
if s != "" { }

这种限制带来的价值是:

  • 条件表达式语义更清晰
  • 减少“空值即假”这类隐式规则
  • 编译器更容易在类型层面帮你发现错误

4. 字符串:本质是不可变的字节序列,不是“字符数组”#

Go 里的 string 代表的是一段只读字节序列。这个定义非常关键。

它有几个核心性质:

  • 可以为空,但长度永不为负
  • 可以通过 len(s) 获取字节长度
  • 可以按索引访问某个字节
  • 一旦创建,内容不可修改

例如:

s := "Go语言"
fmt.Println(len(s))
fmt.Println(s[0])

这里:

  • len(s) 返回的是字节数,不是“字符数”
  • s[0] 取到的是第一个字节,不是第一个 Unicode 字符

这也是很多初学者最容易误解的地方。Go 字符串常常保存 UTF-8 文本,但类型本身并不承诺“一个索引对应一个字符”。

如果你想按 Unicode 码点处理,更常见的方式是:

  • 使用 for range 遍历字符串
  • 或先转换成 []rune

字符串为什么不可变#

不可变的好处是:

  • 共享更安全
  • 作为 map key 更稳定
  • 不会因为局部修改破坏别处引用语义

代价是:

  • 频繁拼接会产生新字符串
  • 修改文本通常要借助 []byte[]rune

image.png
image.png

5. 类型转换:Go 强调显式转换,避免“看起来像对其实错了”#

Go 不鼓励不同类型自动混算。即使两个类型底层位宽一样,也往往要显式转换。

例如:

var a int32 = 10
var b int = 20
fmt.Println(int(a) + b)

这里不能直接 a + b,因为 int32int 是不同类型。

这种设计的重点是:把可能丢精度、变符号、变语义的地方显式暴露出来。

数值类型转换的核心规则#

  • 整数转更小位宽整数:高位会被截断
  • 有符号和无符号互转:按位截断/扩展后得到目标类型值,不会报“溢出异常”
  • 浮点转整数:小数部分直接截断,朝零取整
  • 整数或浮点转目标浮点型:会按目标精度舍入

例如:

fmt.Println(int(3.9)) // 3
fmt.Println(uint8(300)) // 44
fmt.Println(int8(130)) // -126

这里最重要的不是背结果,而是记住:Go 的数值转换通常“总能生成一个目标类型值”,但这个值不一定符合你的业务预期。

字符串转换不是文本解析#

这点必须单独讲清。

string(65)

结果是 "A",因为这是把整数当 Unicode 码点转换成字符串,不是把数字格式化成十进制文本。

如果你想得到 "65",应该用:

strconv.Itoa(65)

反过来,如果你有 "123" 想变成整数,也不是类型转换,而是解析:

strconv.Atoi("123")

string[]byte[]rune 转换的语义不同#

  • []byte(s):拿到字符串的原始字节副本
  • string(bs):把字节切片重新构造成字符串
  • []rune(s):按 UTF-8 解码后得到 Unicode 码点序列

这三者不是同一件事,尤其不能把“字节数”和“字符数”混为一谈。

最小可运行代码示例#

package main
import (
"fmt"
"strconv"
)
func main() {
var age int = 18
var pi float64 = 3.14
var ok bool = true
var text string = "Go语言"
fmt.Println(age, pi, ok, text)
var n int32 = 10
var m int = 20
fmt.Println(int(n) + m)
fmt.Println(len(text)) // 字节长度
fmt.Println([]rune(text)) // 按 rune 解码
fmt.Println(string(65)) // "A"
fmt.Println(strconv.Itoa(65)) // "65"
fmt.Println(int(3.9)) // 3
}

这个例子覆盖了:

  • 整型、浮点型、布尔型、字符串的声明
  • 不同整型之间的显式转换
  • string 的字节长度与 rune 视角
  • “类型转换”和“格式化为字符串”的区别

常见陷阱与错误示例#

1. 误以为 int 永远是 64 位#

错误理解:

var id int

然后把它当成稳定的 64 位存储类型使用。int 只保证“与平台字长一致”,不保证跨平台固定 64 位。涉及协议、文件格式、数据库字段边界时,应用 int64uint32 这类固定宽度类型。

2. 浮点数直接做精确相等比较#

错误示例:

if 0.1+0.2 == 0.3 {
// 期望进入
}

这类判断在浮点场景里不可靠。更稳妥的做法是比较误差是否在允许范围内。

3. 把字符串索引当成“取第几个字符”#

错误示例:

s := "你"
fmt.Println(s[0])

这里拿到的是第一个字节,不是完整字符。处理 Unicode 文本时,应该用 for range[]rune

4. 把 string(123) 当成 "123"#

错误示例:

fmt.Println(string(123))

这不是数字转文本,而是把 123 当成 Unicode 码点转换。数字文本化要用 strconv.ItoaFormatInt;文本解析要用 strconv.AtoiParseInt

5. 误以为类型转换会自动做安全检查#

错误示例:

var x uint8 = uint8(300)

这不会报错,但结果不是 300,而是截断后的值。Go 的显式转换让风险可见,但不替你做业务语义兜底。

性能影响或设计取舍#

1. 默认用 int 是 Go 的工程取舍,不是“最省内存”#

int 通常是最顺手的默认整型,因为它和很多内建函数、索引、长度运算天然配合。但它不是跨平台协议类型,也不是最省空间的类型。需要稳定布局时,应选固定宽度整型。

2. float64 常比 float32 更常见,因为默认精度更稳#

float32 节省内存,但精度更低。大多数通用业务代码里,float64 更安全、更符合默认浮点字面量语义;只有在大规模数组、图形计算、明确内存敏感场景下,float32 才更有吸引力。

3. 字符串不可变,换来安全和共享,代价是修改要拐弯#

字符串拼接、截取、转换都很方便,但如果你频繁编辑文本,直接堆叠字符串往往不划算。大量构造文本时,更适合用 strings.Builder 或先操作 []byte

4. string[]byte[]rune 转换通常有成本#

这类转换常常意味着分配和拷贝,尤其在热路径里要谨慎。设计上这是 Go 在“语义清晰”和“低层零成本共享”之间的取舍:默认安全清楚,性能敏感时再主动优化。

5. 显式转换牺牲一点简洁性,换来类型边界清晰#

Go 不允许很多隐式数值混算,看起来啰嗦一点,但它把精度变化、符号变化、宽度变化的风险直接暴露到代码表面,长期维护收益很高。

面试高频问题#

1. intint32 有什么区别#

int32 是固定 32 位有符号整数;int 是平台相关大小,通常跟机器字长一致。两者即使在某个平台上位宽一样,也仍然是不同类型,不能直接混用。

2. 为什么 Go 的字符串说是字节序列,不是字符数组#

因为 string 的语言定义就是不可变字节序列,len 返回字节数,索引得到的是字节。UTF-8 文本只是常见用法,不改变它的底层语义。

3. string(65)strconv.Itoa(65) 有什么区别#

string(65) 是把整数当 Unicode 码点转换成字符串,结果是 "A"strconv.Itoa(65) 是把整数格式化成十进制文本,结果是 "65"

4. 浮点数为什么不适合表示金额#

因为二进制浮点无法精确表示很多十进制小数,连续计算后容易产生舍入误差。金额这类值通常需要定点整数或专门的十进制方案。

5. Go 为什么要求很多类型转换必须显式写出来#

因为不同类型即使底层表示接近,语义也可能不同。显式转换能把精度损失、符号变化、截断风险暴露出来,减少隐式错误。

6. []byte(s)[]rune(s) 的区别是什么#

[]byte(s) 取的是字符串底层字节序列;[]rune(s) 是把字符串按 UTF-8 解码后得到 Unicode 码点序列。前者适合原始字节处理,后者适合按字符语义处理文本。

一句话总结#

Go 的基本数据类型可以概括为:整型关注位宽和符号,浮点型关注精度与舍入,布尔型坚持显式真假,字符串本质上是不可变字节序列,而类型转换强调显式和语义清晰,绝不是“顺手改个外观”那么简单。

文章目录

文章目录