目录

Go-04 基本数据类型详解

Go 的基本数据类型看似简单,却藏着大量容易踩坑的细节:int 到底几字节?为什么 0.1 + 0.2 != 0.3byterune 有什么区别?哪些类型能用 == 直接比较?本章把这些问题一次讲透。


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 位数

intuint 的宽度取决于目标平台,不是固定的:

  • 在 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 提供 float32float64,均遵循 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 只有 truefalse 两个值,且不能与整型互相转换(与 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 别名:完全等价

byterune 就是别名,编译器视其与 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 完全等价 是,全新类型
能否互相赋值 可以(同一类型) 需显式转换
能否定义新方法 不能
典型用途 兼容重构、简化名称 领域建模、加行为
例子 byterune 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 有什么区别?

答:byteuint8 的别名,代表一个字节(0~255);runeint32 的别名,代表一个 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 完全等价、可自由互赋值、不能加新方法(如 byterune);类型定义 type A B 无等号,创建全新类型,赋值需显式转换,可以拥有独立的方法集(如 time.Duration)。


小结

本章系统梳理了 Go 的基本数据类型,核心要点如下:

  1. 整型int/uint 位宽平台相关(64 位系统为 8 字节),跨平台场景用固定宽度类型;各类型有明确取值范围。
  2. 溢出:整数运算不检测溢出,超范围静默回绕,需手动检查或借助 math/bitsmath.MaxInt 等常量提供边界值。
  3. 浮点:遵循 IEEE 754,天然不精确,0.1+0.2 != 0.3;比较用 epsilon 误差,NaN 用 math.IsNaN 判断,金额禁用浮点。
  4. 布尔/复数bool 不能与整型转换;复数了解即可。
  5. byte 与 runebyte=uint8(字节),rune=int32(码点);字符串是 UTF-8 字节流,遍历中文用 range 或转 []rune
  6. 类型转换:必须显式,数值转换会截断/溢出,字符串转换用 strconv 而非 string(int)
  7. 别名 vs 定义:别名完全等价无新方法,类型定义是新类型可带方法集。
  8. 可比较性:基本类型/指针/channel 可比较,数组/结构体条件可比较,slice/map/func 不可比较,接口比较可能 panic。
  9. 最佳实践:合理选整型宽度省内存,金额用整数或 decimal,标志位用位运算。

掌握这些类型细节,能帮你避开 Go 开发中最常见的一类隐蔽 bug。下一章我们将进入数组与切片,拆解切片的底层结构(array/len/cap)与扩容机制。