2.2 基本数据类型与复合数据类型¶
各位同学,今天我们来系统学习Go语言的数据类型体系。数据类型是编程语言的基石,理解Go语言数据类型的设计理念和底层实现,不仅能帮助我们写出更高效的代码,更能让我们领会Go语言"简单、明确、高效"的设计哲学。
学习目标¶
- 掌握数值类型的精度与范围特性,能根据场景选择合适类型
- 理解字符串类型的底层实现机制,解释其不可变性本质
- 熟悉布尔类型的正确使用场景,避免常见类型转换错误
- 掌握复合类型的内存布局与使用技巧,优化代码性能
一、数值类型¶
Go语言提供了丰富的数值类型,每种类型都有明确的精度和范围,这体现了Go语言"明确性"的设计原则——让开发者对内存占用和数值范围有清晰的预期。
1.1 整数类型¶
Go的整数类型分为有符号和无符号两大类,每种类型都有固定的位数,这意味着它们的内存占用和数值范围是确定的。
package main
import (
"fmt"
"math"
"unsafe"
)
func main() {
// 有符号整数
var i8 int8 = 127
var i16 int16 = 32767
var i32 int32 = math.MaxInt32
var i64 int64 = math.MaxInt64
// 无符号整数
var u8 uint8 = 255
var u16 uint16 = 65535
// 打印各类型的范围和内存大小
fmt.Printf("int8 范围: %d 到 %d, 大小: %d 字节\n",
math.MinInt8, math.MaxInt8, unsafe.Sizeof(i8))
fmt.Printf("int16 范围: %d 到 %d, 大小: %d 字节\n",
math.MinInt16, math.MaxInt16, unsafe.Sizeof(i16))
fmt.Printf("int32 范围: %d 到 %d, 大小: %d 字节\n",
math.MinInt32, math.MaxInt32, unsafe.Sizeof(i32))
fmt.Printf("int64 范围: %d 到 %d, 大小: %d 字节\n",
math.MinInt64, math.MaxInt64, unsafe.Sizeof(i64))
fmt.Printf("uint8 范围: 0 到 %d, 大小: %d 字节\n",
math.MaxUint8, unsafe.Sizeof(u8))
fmt.Printf("uint16 范围: 0 到 %d, 大小: %d 字节\n",
math.MaxUint16, unsafe.Sizeof(u16))
// 特殊整数类型示例
var ptr uintptr = 0x12345678
var b byte = 65 // byte是uint8的别名
var r rune = 'A' // rune是int32的别名,用于表示Unicode码点
fmt.Printf("\nbyte类型值: %d (字符表示: %c)\n", b, b)
fmt.Printf("rune类型值: %d (字符表示: %c)\n", r, r)
fmt.Printf("uintptr类型值: 0x%x\n", ptr)
}
1.2 浮点类型¶
Go语言提供两种浮点类型,分别对应IEEE 754标准的单精度和双精度浮点数:
package main
import (
"fmt"
"math"
"unsafe"
)
func main() {
var f32 float32 = math.MaxFloat32
var f64 float64 = math.MaxFloat64
fmt.Printf("float32 最大值: %e, 精度: 约6位小数, 大小: %d 字节\n",
f32, unsafe.Sizeof(f32))
fmt.Printf("float64 最大值: %e, 精度: 约15位小数, 大小: %d 字节\n",
f64, unsafe.Sizeof(f64))
// 浮点数精度问题示例
var a float32 = 1.0 / 3.0
var b float64 = 1.0 / 3.0
fmt.Printf("float32 1/3 = %.20f\n", a)
fmt.Printf("float64 1/3 = %.20f\n", b)
// 浮点数比较的正确方式(避免直接使用==)
x, y := 0.1+0.2, 0.3
fmt.Printf("0.1+0.2 = %v, 0.3 = %v\n", x, y)
fmt.Printf("直接比较: 0.1+0.2 == 0.3 ? %t\n", x == y)
// 使用容差比较
epsilon := 1e-9
equal := math.Abs(x-y) < epsilon
fmt.Printf("容差比较: 0.1+0.2 ≈ 0.3 ? %t\n", equal)
// 金融计算建议:使用整数(以分、厘为单位)避免精度问题
priceInCents := 999 // 表示9.99元
quantity := 3
totalInCents := priceInCents * quantity // 2997分 = 29.97元
fmt.Printf("商品总价: %.2f 元\n", float64(totalInCents)/100)
}
1.3 复数类型¶
Go语言原生支持复数类型,这在科学计算和工程领域非常有用:
package main
import (
"fmt"
"math/cmplx"
)
func main() {
// 复数创建方式
var c1 complex64 = complex(1, 2) // 1+2i,使用complex函数
var c2 complex128 = 3 + 4i // 直接使用字面量
c3 := complex(5.5, -6.6) // 类型推断为complex128(默认)
fmt.Printf("c1 = %v (类型: %T)\n", c1, c1)
fmt.Printf("c2 = %v (类型: %T)\n", c2, c2)
fmt.Printf("c3 = %v (类型: %T)\n", c3, c3)
// 复数组件提取
fmt.Printf("c2 实部: %f, 虚部: %f\n", real(c2), imag(c2))
// 复数运算
sum := c1 + complex64(c2)
product := c1 * complex64(c2)
fmt.Printf("c1 + c2 = %v\n", sum)
fmt.Printf("c1 * c2 = %v\n", product)
// 复数函数库使用
fmt.Printf("c2 的模: %f\n", cmplx.Abs(c2)) // 计算模长
fmt.Printf("c2 的共轭: %v\n", cmplx.Conj(c2)) // 计算共轭复数
fmt.Printf("e^πi = %v\n", cmplx.Exp(cmplx.Pi*i)) // 欧拉公式
}
1.4 数值类型使用指南¶
- 类型选择原则:在满足需求的前提下,选择最小的类型以节省内存
- 跨平台一致性:如果需要在不同平台保持一致的数值范围,应使用明确位数的类型(如int32)而非int
- 浮点数注意:避免用float32存储精确值,金融计算优先使用整数+缩放因子
- 类型转换:Go不支持隐式类型转换,必须使用显式转换(如int32(x))
- 比较方式:浮点数比较应使用容差法,而非直接使用==运算符
二、字符串类型¶
字符串是Go语言中处理文本数据的核心类型,理解其底层实现对于高效使用至关重要。
2.1 底层实现机制¶
Go语言中的字符串本质是一个只读的字节序列,其底层结构包含两个部分:指向字节数组的指针和长度信息。
package main
import (
"fmt"
"reflect"
"unsafe"
)
func main() {
s := "Hello, 世界"
// 字符串的字符数与字节数
fmt.Printf("字符串: %s\n", s)
fmt.Printf("字符数: %d\n", len([]rune(s))) // 8个字符
fmt.Printf("字节数: %d\n", len(s)) // 12个字节(英文1字节,中文3字节)
// 查看UTF-8编码的字节表示
fmt.Printf("UTF-8 字节: ")
for i := 0; i < len(s); i++ {
fmt.Printf("%x ", s[i])
}
fmt.Println()
// 通过unsafe包查看字符串内部结构
strHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))
fmt.Printf("字符串数据指针: %#x, 长度: %d\n",
strHeader.Data, strHeader.Len)
// 验证字符串的不可变性
fmt.Println("\n尝试修改字符串:")
// s[0] = 'h' // 编译错误:cannot assign to s[0] (strings are immutable)
// 要修改字符串需先转换为字节切片
bytes := []byte(s)
bytes[0] = 'h' // 现在可以修改了
modified := string(bytes)
fmt.Printf("原字符串: %s\n", s) // 保持不变
fmt.Printf("修改后字符串: %s\n", modified)
}
2.2 字符串操作技巧¶
Go标准库的strings包提供了丰富的字符串操作函数,掌握这些函数能极大提高处理字符串的效率。
package main
import (
"fmt"
"strings"
"unicode/utf8"
)
func main() {
s1 := "Hello"
s2 := "World"
// 字符串连接
result := s1 + " " + s2
fmt.Println("连接结果:", result)
// 高效字符串拼接(大量操作时使用)
var builder strings.Builder
builder.WriteString(s1)
builder.WriteString(" ")
builder.WriteString(s2)
efficientResult := builder.String()
fmt.Println("高效拼接结果:", efficientResult)
// 字符串切片(注意:按字节操作)
fmt.Println("前5字节:", result[:5])
// 正确处理UTF-8多字节字符
chinese := "你好世界"
fmt.Println("\n中文字符串:", chinese)
fmt.Println("字节长度:", len(chinese)) // 12字节(每个汉字3字节)
fmt.Println("字符长度:", utf8.RuneCountInString(chinese))
// 按字符遍历(使用for range)
fmt.Print("按字符遍历: ")
for _, r := range chinese {
fmt.Printf("%c ", r)
}
fmt.Println()
// 常用字符串处理函数
fmt.Println("\n字符串包含检查:", strings.Contains(result, "Hello"))
fmt.Println("字符串前缀检查:", strings.HasPrefix(result, "He"))
fmt.Println("字符串后缀检查:", strings.HasSuffix(result, "ld"))
fmt.Println("转大写:", strings.ToUpper(result))
fmt.Println("转小写:", strings.ToLower(result))
fmt.Println("替换操作:", strings.ReplaceAll(result, "l", "L"))
fmt.Println("分割操作:", strings.Split(result, " "))
fmt.Println("修剪操作:", strings.Trim(" Hello ", " "))
}
2.3 字符串使用注意事项¶
- 不可变性:字符串一旦创建就不能修改,任何修改操作都会创建新字符串
- UTF-8编码:len()返回字节数而非字符数,遍历多字节字符需用for range
- 内存效率:子字符串不会复制底层数据,仅创建新的字符串头,共享原字节数组
- 拼接性能:大量字符串拼接时,使用strings.Builder比+运算符更高效
- 空字符串:空字符串("")与nil不同,它有长度0但指针非nil
三、布尔类型¶
布尔类型是表示逻辑值的简单类型,在Go语言中有着明确的使用规范和限制。
package main
import (
"fmt"
"strings"
"strconv"
)
func main() {
// 布尔值声明与初始化
var isActive bool = true
var isFinished bool = false
var defaultBool bool // 零值为false
fmt.Printf("isActive: %t, isFinished: %t\n", isActive, isFinished)
fmt.Printf("布尔零值: %t\n", defaultBool)
// 逻辑运算
a, b := true, false
fmt.Printf("\na && b: %t (逻辑与)\n", a && b)
fmt.Printf("a || b: %t (逻辑或)\n", a || b)
fmt.Printf("!a: %t (逻辑非)\n", !a)
// 短路特性演示
fmt.Println("\n短路特性测试:")
fmt.Println("true && unknown:", true && unknown()) // 不会执行unknown()
fmt.Println("false && unknown:", false && unknown()) // 会执行unknown()
// 实际应用场景:数据验证
email := "test@example.com"
isValid := checkEmailValidity(email)
fmt.Printf("\n邮箱 %q 是否有效: %t\n", email, isValid)
// 布尔值与字符串的转换
boolStr := strconv.FormatBool(true)
fmt.Println("布尔转字符串:", boolStr, "(类型:", fmt.Sprintf("%T", boolStr), ")")
parsedBool, err := strconv.ParseBool("true")
if err == nil {
fmt.Println("字符串转布尔:", parsedBool, "(类型:", fmt.Sprintf("%T", parsedBool), ")")
}
// 注意:Go不允许布尔值与其他类型的转换
// var num int = 1
// var b bool = bool(num) // 编译错误:cannot convert num (type int) to type bool
}
// 用于演示短路特性的函数
func unknown() bool {
fmt.Println("执行了unknown()函数")
return true
}
// 邮箱验证函数(简化版)
func checkEmailValidity(email string) bool {
return strings.Contains(email, "@") &&
strings.Contains(email, ".") &&
strings.Index(email, "@") < strings.LastIndex(email, ".")
}
3.1 布尔类型使用场景¶
- 条件判断:作为if、switch语句的条件表达式
- 循环控制:控制for循环的执行与否
- 状态标记:表示某个操作是否完成、某个条件是否满足
- 逻辑运算:组合多个条件进行复杂判断
- 函数返回值:表示操作是否成功(常与错误返回结合使用)
3.2 布尔类型使用规范¶
- 命名约定:布尔变量/函数建议以"Is"、"Has"、"Can"、"Should"等前缀命名,如isValid、hasPermission
- 类型安全:Go不允许布尔值与整数或其他类型相互转换
- 短路特性:利用&&和||的短路特性优化条件判断,避免不必要的计算
- 避免冗余:不要写
if flag == true这样的冗余代码,直接使用if flag
四、复合数据类型¶
复合数据类型是由基本类型组合而成的数据结构,掌握它们的内存布局有助于写出更高效的代码。
4.1 数组(Array)¶
数组是具有固定长度且元素类型相同的序列,其长度是类型的一部分。
package main
import (
"fmt"
"unsafe"
)
func main() {
// 数组的多种声明和初始化方式
var arr1 [5]int // 声明长度为5的int数组,元素默认值为0
arr2 := [3]string{"a", "b", "c"} // 声明并初始化
arr3 := [...]int{1, 2, 3, 4, 5} // 编译器自动推断长度为5
arr4 := [5]int{2: 10, 4: 20} // 指定索引位置初始化
fmt.Printf("arr1: %v, 长度: %d\n", arr1, len(arr1))
fmt.Printf("arr2: %v, 长度: %d\n", arr2, len(arr2))
fmt.Printf("arr3: %v, 长度: %d\n", arr3, len(arr3))
fmt.Printf("arr4: %v, 长度: %d\n", arr4, len(arr4))
// 数组是值类型(赋值会复制整个数组)
copyArr := arr3
copyArr[0] = 100
fmt.Printf("\n原数组arr3: %v\n", arr3) // [1 2 3 4 5](未被修改)
fmt.Printf("复制数组copyArr: %v\n", copyArr) // [100 2 3 4 5]
// 数组元素在内存中连续存储
fmt.Println("\n数组元素地址(展示连续性):")
for i := 0; i < len(arr3); i++ {
fmt.Printf("arr3[%d] = %d, 地址: %p\n", i, arr3[i], &arr3[i])
}
// 数组长度是类型的一部分
var intArr3 [3]int
var intArr5 [5]int
// intArr3 = intArr5 // 编译错误:cannot assign [5]int to [3]int
// 多维数组
matrix := [2][3]int{
{1, 2, 3},
{4, 5, 6},
}
fmt.Printf("\n二维数组: %v\n", matrix)
fmt.Printf("访问元素matrix[1][2]: %d\n", matrix[1][2])
// 数组大小计算
fmt.Printf("\narr3数组占用内存: %d 字节\n", unsafe.Sizeof(arr3))
}
4.2 切片(Slice)¶
切片是对数组的动态视图,提供了灵活的序列操作能力,是Go语言中最常用的数据结构之一。
package main
import "fmt"
func main() {
// 切片的创建方式
slice1 := []int{1, 2, 3, 4, 5} // 直接初始化
slice2 := make([]string, 3, 5) // 使用make创建,长度3,容量5
slice3 := make([]float64, 4) // 省略容量,容量等于长度
var slice4 []bool // 零值切片(nil)
fmt.Printf("slice1: %v, 长度: %d, 容量: %d\n", slice1, len(slice1), cap(slice1))
fmt.Printf("slice2: %v, 长度: %d, 容量: %d\n", slice2, len(slice2), cap(slice2))
fmt.Printf("slice3: %v, 长度: %d, 容量: %d\n", slice3, len(slice3), cap(slice3))
fmt.Printf("slice4: %v, 长度: %d, 容量: %d, 是否为nil: %t\n",
slice4, len(slice4), cap(slice4), slice4 == nil)
// 基于数组创建切片
arr := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
slice5 := arr[2:5] // 包含arr[2], arr[3], arr[4]
slice6 := arr[5:] // 从arr[5]到末尾
slice7 := arr[:3] // 从开头到arr[2]
fmt.Printf("\n原数组arr: %v\n", arr)
fmt.Printf("slice5: %v, 长度: %d, 容量: %d\n", slice5, len(slice5), cap(slice5))
fmt.Printf("slice6: %v, 长度: %d, 容量: %d\n", slice6, len(slice6), cap(slice6))
// 切片共享底层数组(修改会相互影响)
slice5[0] = 200
fmt.Printf("修改slice5后,原数组arr: %v\n", arr)
fmt.Printf("修改slice5后,slice6: %v\n", slice6)
// 切片追加操作
slice8 := []int{10, 20}
fmt.Printf("\n初始slice8: %v, 容量: %d\n", slice8, cap(slice8))
slice8 = append(slice8, 30)
fmt.Printf("追加1个元素后: %v, 容量: %d\n", slice8, cap(slice8))
slice8 = append(slice8, 40, 50, 60)
fmt.Printf("追加3个元素后: %v, 容量: %d\n", slice8, cap(slice8))
// 切片拷贝
source := []int{1, 2, 3}
destination := make([]int, len(source))
copyCount := copy(destination, source)
fmt.Printf("\n拷贝结果: source=%v, destination=%v, 拷贝个数=%d\n",
source, destination, copyCount)
}
4.3 结构体(Struct)¶
结构体是将零个或多个任意类型的字段组合在一起的复合类型,用于表示实体的属性集合。
package main
import (
"fmt"
"unsafe"
)
// 定义一个结构体类型
type Person struct {
Name string
Age int
Male bool
}
// 嵌套结构体
type Address struct {
City string
Street string
ZipCode string
}
type Employee struct {
Person // 匿名字段(嵌入)
ID int
Address // 嵌入结构体
Salary float64
}
// 空结构体(不占用内存)
type EmptyStruct struct{}
func main() {
// 结构体初始化
p1 := Person{"Alice", 30, true}
p2 := Person{Name: "Bob", Age: 25} // 部分字段初始化
var p3 Person // 零值初始化
p3.Name = "Charlie"
p3.Age = 35
fmt.Printf("p1: %v\n", p1)
fmt.Printf("p2: %v\n", p2)
fmt.Printf("p3: %v\n", p3)
// 访问结构体字段
fmt.Printf("\np1的名字: %s, 年龄: %d\n", p1.Name, p1.Age)
// 嵌套结构体使用
e := Employee{
Person: Person{
Name: "David",
Age: 40,
Male: true,
},
ID: 1001,
Salary: 5000.0,
Address: Address{
City: "Beijing",
Street: "Main St",
ZipCode: "100000",
},
}
fmt.Printf("\nEmployee e: %v\n", e)
fmt.Printf("员工姓名: %s, 所在城市: %s\n", e.Name, e.City) // 直接访问嵌入字段
// 结构体的内存布局
fmt.Println("\n结构体字段地址(展示内存布局):")
fmt.Printf("p1.Name 地址: %p\n", &p1.Name)
fmt.Printf("p1.Age 地址: %p\n", &p1.Age)
fmt.Printf("p1.Male 地址: %p\n", &p1.Male)
// 结构体大小(受内存对齐影响)
fmt.Printf("\nPerson结构体大小: %d 字节\n", unsafe.Sizeof(p1))
// 空结构体特性
var es EmptyStruct
var es2 EmptyStruct
fmt.Printf("空结构体大小: %d 字节\n", unsafe.Sizeof(es))
fmt.Printf("两个空结构体的地址是否相同: %v\n", &es == &es2)
}
4.4 映射(Map)¶
映射是一种无序的键值对集合,提供了快速的查找能力,类似于其他语言中的字典或哈希表。
package main
import "fmt"
func main() {
// 映射的创建方式
map1 := map[string]int{
"apple": 5,
"banana": 3,
"orange": 7,
}
map2 := make(map[int]string) // 使用make创建,默认容量
map3 := make(map[string]float64, 10) // 指定初始容量
var map4 map[string]bool // 零值映射(nil)
fmt.Printf("map1: %v, 长度: %d\n", map1, len(map1))
fmt.Printf("map2: %v, 长度: %d\n", map2, len(map2))
fmt.Printf("map4是否为nil: %t\n", map4 == nil)
// 映射的基本操作
fmt.Println("\n映射基本操作:")
// 添加/修改元素
map2[1] = "one"
map2[2] = "two"
map1["grape"] = 4 // 添加新元素
map1["apple"] = 6 // 修改已有元素
fmt.Printf("添加元素后map2: %v\n", map2)
fmt.Printf("修改元素后map1: %v\n", map1)
// 获取元素
appleCount := map1["apple"]
fmt.Printf("苹果数量: %d\n", appleCount)
// 检查键是否存在
orangeCount, exists := map1["orange"]
if exists {
fmt.Printf("橙子存在,数量: %d\n", orangeCount)
} else {
fmt.Println("橙子不存在")
}
// 删除元素
delete(map1, "banana")
fmt.Printf("删除香蕉后map1: %v\n", map1)
// 映射是引用类型(赋值不会复制数据)
map1Copy := map1
map1Copy["orange"] = 10
fmt.Printf("\n修改复制的map后,原map1: %v\n", map1)
// 映射的遍历
fmt.Println("\n遍历map1:")
for fruit, count := range map1 {
fmt.Printf("%s: %d\n", fruit, count)
}
// 注意:映射的遍历顺序是不确定的
// 若需要有序遍历,需先获取键列表并排序
}
总结¶
本节我们全面学习了Go语言的数据类型系统,包括:
-
数值类型:有着明确的精度和范围,选择合适的类型能提高内存效率并避免溢出问题。特别要注意浮点数的精度限制,金融计算中建议使用整数+缩放因子的方式。
-
字符串类型:本质是只读的字节切片,默认采用UTF-8编码。其不可变性带来了安全性,但也需要注意修改操作会创建新字符串。大量字符串拼接应使用strings.Builder提高效率。
-
布尔类型:只有true和false两个值,不能与其他类型转换。主要用于条件判断和状态标记,命名时应使用"Is"、"Has"等前缀提高可读性。
- 复合类型:
- 数组:固定长度,值类型,元素连续存储
- 切片:动态视图,引用类型,包含指针、长度和容量三个字段
- 结构体:字段的集合,支持嵌套,存在内存对齐现象
- 映射:键值对集合,引用类型,底层为哈希表实现
理解这些数据类型的特性和内存布局,是写出高效、安全的Go代码的基础。在实际开发中,应根据具体场景选择合适的数据类型,并充分利用其特性优化程序性能。