Go 语言核心数据类型详解(一):整型、浮点型、布尔型、字符串与数组
Go 语言的基础学习之旅。如果说写程序如同写作文,那么数据类型就是我们构建“文章”(程序)的“词汇”和“素材”。不同的数据类型用来表示和处理现实世界中不同种类的信息。
Go 语言内置了丰富的数据类型,常见的有:
- 基础类型:
- 整型 (Integer): 用于表示整数。
- 浮点型 (Floating-point): 用于表示小数(可能不精确)。
- 布尔型 (Boolean): 用于表示逻辑真 (
true) 或假 (false)。 - 字符串 (String): 用于表示文本信息。
- 复合类型:
- 数组 (Array): 用于表示固定长度的同类型元素序列。
- 切片 (Slice): 动态长度的同类型元素序列(更常用)。
- 字典 (Map): 用于表示键值对集合。
- 结构体 (Struct): 用于组合不同类型的字段来自定义数据结构。
- 指针 (Pointer): 用于存储变量的内存地址。
- 接口 (Interface): 定义行为契约,用于实现多态和泛型编程。
- 通道 (Channel): 用于 Goroutine 间的通信。
- ...等等
关于“值类型”与“引用类型”的讨论:
在 Go 的语境中,严格来说所有类型赋值和传参都是“按值传递”的。但像切片、字典、通道、指针、函数这些类型,它们的值本身包含(或就是)指向底层数据结构的指针。因此,当复制这些类型的值时,复制的是指针(或包含指针的结构),使得多个变量可能共享同一份底层数据,表现出类似“引用传递”的行为。数组和结构体(不含指针字段时)则是典型的“值类型”,赋值时会创建完整的数据副本。更深入的讨论可以参考:Go 语言中关于“引用类型”术语的说明
今天,我们将重点深入学习以下几种基础且常用的数据类型:整型、浮点型、布尔型、字符串和数组。
1. 整型 (Integer)
整型用于表示没有小数部分的数字。Go 提供了多种整型,主要分为有符号 (signed) 和 无符号 (unsigned) 两大类。有符号整型可以表示负数、零和正数,而无符号整型只能表示零和正数。
1.1 类型与范围
有符号整型:
int8: 8位,范围 -128 到 127int16: 16位,范围 -32,768 到 32,767int32: 32位,范围 -2,147,483,648 到 2,147,483,647int64: 64位,范围 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807int: 平台相关。在 32 位系统上是int32,在 64 位系统上是int64。通常直接使用int即可,除非有特定的大小需求或跨平台兼容性考虑。
无符号整型:
uint8: 8位,范围 0 到 255 (也是byte类型的底层类型)uint16: 16位,范围 0 到 65,535uint32: 32位,范围 0 到 4,294,967,295uint64: 64位,范围 0 到 18,446,744,073,709,551,615uint: 平台相关。在 32 位系统上是uint32,在 64 位系统上是uint64。uintptr: 无符号整型,大小足以存储指针的位模式。主要用于底层编程。
选择原则: 根据你的数据范围需求选择最小的能满足要求的类型,可以节省内存。但通常为了简化代码,直接使用 int 或 uint 也很常见。
1.2 整型间转换
Go 语言是强类型语言,不同类型的整型变量之间不能直接进行运算或赋值,必须进行显式类型转换。
package main
import (
"fmt"
"reflect"
)
func main() {
var v1 int8 = 10
var v2 int16 = 18
// 必须将 v1 转换为 int16 类型才能与 v2 相加
v3 := int16(v1) + v2
fmt.Println(v3, reflect.TypeOf(v3)) // 输出: 28 int16
// 高位转向低位转换 - 可能丢失数据(溢出)
var v4 int16 = 130 // 130 超出了 int8 的最大值 127
v5 := int8(v4) // 发生溢出
fmt.Println(v5) // 输出: -126 (二进制表示被截断后解释为 int8)
}
注意:
- 低位类型向高位类型转换通常是安全的。
- 高位类型向低位类型转换需要特别小心,如果原始值超出了目标类型的表示范围,会发生截断 (truncation),导致结果不符合预期(数值可能改变,甚至符号也可能改变)。
1.3 整型与字符串转换
使用 strconv 包进行转换。
package main
import (
"fmt"
"reflect"
"strconv"
)
func main() {
// 1. 整型转换为字符串类型
v1 := 19
resultStr := strconv.Itoa(v1) // Itoa 是 FormatInt(i, 10) 的缩写
fmt.Println(resultStr, reflect.TypeOf(resultStr)) // 输出: "19" string
var v2 int8 = 17
// 需要先将 int8 转为 int (Itoa 要求 int 类型)
dataStr := strconv.Itoa(int(v2))
fmt.Println(dataStr, reflect.TypeOf(dataStr)) // 输出: "17" string
// 通用方法 FormatInt (适用于所有整型,需先转为 int64 或 uint64)
var v3 int16 = 100
strFromInt64 := strconv.FormatInt(int64(v3), 10) // 10 表示目标是十进制
fmt.Println(strFromInt64) // 输出: "100"
// 2. 字符串转换为整型
v4Str := "666"
// Atoi 是 ParseInt(s, 10, 0) 的缩写,返回 int 类型
resultInt, err := strconv.Atoi(v4Str)
if err == nil {
fmt.Println("转换成功:", resultInt, reflect.TypeOf(resultInt)) // 输出: 转换成功: 666 int
} else {
fmt.Println("转换失败:", err)
}
v5Str := "not_a_number"
resultInt2, err2 := strconv.Atoi(v5Str)
if err2 != nil {
fmt.Println("转换失败:", err2) // 输出: 转换失败: strconv.Atoi: parsing "not_a_number": invalid syntax
fmt.Println("转换失败时的默认值:", resultInt2) // 输出: 转换失败时的默认值: 0
}
}
注意: 字符串转整型 (Atoi, ParseInt, ParseUint) 可能会失败(如果字符串格式无效),因此必须检查返回的 error。
1.4 进制转换
在 Go 代码中,我们通常直接书写十进制整数。其他进制(二进制、八进制、十六进制)主要在需要表示特定位模式或与字符串交互时用到。
- 十进制 (整型) 转 其他进制 (字符串): 使用
strconv.FormatInt或strconv.FormatUint。
package main
import (
"fmt"
"strconv"
)
func main() {
v1 := 99 // 十进制整数
// 转二进制字符串
binStr := strconv.FormatInt(int64(v1), 2)
fmt.Printf("%d 的二进制是: %s\n", v1, binStr) // 输出: 99 的二进制是: 1100011
// 转八进制字符串
octStr := strconv.FormatInt(int64(v1), 8)
fmt.Printf("%d 的八进制是: %s\n", v1, octStr) // 输出: 99 的八进制是: 143
// 转十六进制字符串
hexStr := strconv.FormatInt(int64(v1), 16)
fmt.Printf("%d 的十六进制是: %s\n", v1, hexStr) // 输出: 99 的十六进制是: 63
}
- 其他进制 (字符串) 转 十进制 (整型): 使用
strconv.ParseInt或strconv.ParseUint。
package main
import (
"fmt"
"reflect"
"strconv"
)
func main() {
// data: 要转换的字符串
// base: 源字符串的进制 (2-36, 0表示自动检测前缀: 0b/0B 二进制, 0 八进制, 0x/0X 十六进制, 否则十进制)
// bitSize: 结果的期望位大小 (0=int, 8=int8, 16=int16, 32=int32, 64=int64). 约束结果范围。
data := "1100011" // 二进制字符串
resultInt64, err := strconv.ParseInt(data, 2, 64) // 解析为 int64
if err == nil {
fmt.Println(resultInt64, reflect.TypeOf(resultInt64)) // 输出: 99 int64
} else {
fmt.Println("转换失败:", err)
}
hexData := "63" // 十六进制字符串
resultInt64Hex, _ := strconv.ParseInt(hexData, 16, 64)
fmt.Println(resultInt64Hex) // 输出: 99
// Atoi 本质上是 ParseInt(s, 10, 0)
// v, err := Atoi("123") 等价于 v, err := ParseInt("123", 10, 0)
}
练习题解答思路:
- 十进制 14 转 16 进制字符串:
strconv.FormatInt(14, 16)-> "e" - 二进制 "10011" 转 10 进制整型:
strconv.ParseInt("10011", 2, 0)-> 19 (int) - 二进制 "10011" 转 16 进制字符串: 先转为十进制
19,再转为十六进制字符串strconv.FormatInt(19, 16)-> "13"
1.5 常见数学运算 (math 包)
math 包提供了基本的数学函数。注意,这些函数通常操作 float64 类型。
package main
import (
"fmt"
"math"
)
func main() {
fmt.Println(math.Abs(-19.5)) // 取绝对值: 19.5
fmt.Println(math.Floor(3.14)) // 向下取整: 3
fmt.Println(math.Ceil(3.14)) // 向上取整: 4
fmt.Println(math.Round(3.49)) // 四舍五入到最近整数: 3
fmt.Println(math.Round(3.5)) // 四舍五入到最近整数: 4
fmt.Println(math.Round(3.3478*100) / 100) // 保留两位小数 (四舍五入): 3.35
fmt.Println(math.Mod(11.0, 3.0)) // 取浮点数余数: 2
// 整数取余使用 % 操作符: fmt.Println(11 % 3) // 2
fmt.Println(math.Pow(2, 5)) // 计算x的y次方 (2^5): 32
fmt.Println(math.Pow10(3)) // 计算10的n次方 (10^3): 1000
fmt.Println(math.Max(1.5, 2.8)) // 取两者中的最大值: 2.8
fmt.Println(math.Min(1.5, 2.8)) // 取两者中的最小值: 1.5
}
1.6 指针、nil 与 new
-
声明变量:
var v1 int: 声明一个int型变量v1,并初始化为其零值 (zero value),对于int来说就是0。v2 := 999: 使用短变量声明:=,声明并初始化v2为999,Go 会自动推断其类型为int。
-
指针 (Pointer):
- 指针是一种存储内存地址的变量。
var v3 *int: 声明一个指向int类型的指针v3。其零值是nil。此时v3不指向任何有效的内存地址。&操作符用于获取变量的内存地址。例如p := &v1,则p就是一个指向v1的*int指针。*操作符用于解引用 (dereference) 指针,即访问指针所指向地址处存储的值。例如fmt.Println(*p)会输出v1的值。
-
nil:nil是 Go 中某些类型的零值标识符,包括:指针、切片、字典、接口、通道和函数类型。- 它表示这些类型的变量未初始化或不指向任何有效的实例/内存。
var v100 *int声明后v100的值就是nil。
-
new关键字:new(T)是一个内置函数,用于分配内存来存储一个T类型的值。- 它会将分配的内存初始化为
T类型的零值。 - 最重要的是,
new(T)返回的是一个指向该新分配内存的指针 (*T)。 v4 := new(int): 分配一个int的内存空间,初始化为 0,并把指向这块内存的指针 (*int) 赋值给v4。
package main
import "fmt"
func main() {
var v1 int // v1 = 0
v2 := 999 // v2 = 999, 类型为 int
var v3 *int // v3 = nil
v4 := new(int) // v4 是一个 *int 指针, 指向一个值为 0 的内存地址
fmt.Println("v1:", v1)
fmt.Println("v2:", v2)
fmt.Println("v3:", v3) // 输出: v3: <nil>
fmt.Println("v4:", v4) // 输出: v4: 0xc000016088 (内存地址, 每次运行可能不同)
fmt.Println("*v4:", *v4) // 输出: *v4: 0 (解引用 v4 得到其指向的值)
// 修改指针指向的值
*v4 = 100
fmt.Println("*v4 after modification:", *v4) // 输出: *v4 after modification: 100
// & 获取地址
p := &v2
fmt.Printf("Address of v2: %p, type of p: %T, value of p: %v, value pointed by p: %d\n", p, p, p, *p)
}
-
为什么需要指针?
- 效率: 允许函数修改调用者作用域中的变量(通过传递指针),避免大型数据结构的值拷贝开销。
- 共享数据: 多个指针可以指向同一块内存,实现数据共享。
- 表示可选值或缺失状态: 指针可以是
nil,可以用来表示一个值不存在。
-
重要:
int和*int是两种不同的类型,不能直接相互赋值或比较(除非与nil比较)。
1.7 超大整型 (math/big)
当需要的整数值超出了 int64 或 uint64 的范围时,可以使用 math/big 包中的 Int 类型。它提供了任意精度的整数算术运算。
package main
import (
"fmt"
"math/big"
)
func main() {
// 推荐方式: 使用 new 或 big.NewInt 创建指针类型
v1 := new(big.Int)
v1.SetString("92233720368547758089223372036854775808", 10) // 从十进制字符串设置值
v2 := big.NewInt(99) // 从 int64 创建
// 进行运算 (方法通常作用于指针接收者,并修改其值)
result := new(big.Int) // 创建一个用于存储结果的对象
// 加法: result = v1 + v2
result.Add(v1, v2)
fmt.Println("v1 + v2 =", result.String()) // 使用 String() 获取字符串表示
// 减法: result = v1 - v2
result.Sub(v1, v2)
fmt.Println("v1 - v2 =", result) // 默认打印也是 String()
// 乘法: result = v1 * v2
result.Mul(v1, v2)
fmt.Println("v1 * v2 =", result)
// 除法 (整数除法,只得到商): result = v1 / v2
result.Div(v1, v2)
fmt.Println("v1 / v2 (商) =", result)
// 同时获取商和余数: v1 = quotient * v2 + remainder
quotient := new(big.Int)
remainder := new(big.Int)
quotient.DivMod(v1, v2, remainder) // quotient 接收商, remainder 接收余数
fmt.Printf("%s / %s = 商 %s, 余数 %s\n", v1, v2, quotient, remainder)
// 易错点:方法接收者是指针类型
// var n1 big.Int // 错误示范: n1 是值类型
// n1.SetString("123", 10)
// var n2 big.Int
// n2.SetString("456", 10)
// var res big.Int
// res.Add(&n1, &n2) // Add 需要 *big.Int 参数, 需要传递地址
// fmt.Println(res.String())
}
使用 big.Int 建议:
- 始终使用指针 (
*big.Int)。通过new(big.Int)或big.NewInt()创建。 - 运算方法(如
Add,Sub等)通常会修改调用者(第一个参数,有时也叫接收者,但在big包中通常是结果存储参数),并需要传入指针。仔细阅读文档确定每个方法的行为。 - 使用
String()方法获取其十进制字符串表示。
2. 浮点型 (Floating-point)
浮点型用于表示可能带有小数部分的数字,例如 3.14, -0.01。Go 提供了两种浮点类型:
float32: 32位(4字节),IEEE 754 单精度标准。精度约 6-7 位十进制数。float64: 64位(8字节),IEEE 754 双精度标准。精度约 15 位十进制数。默认的小数类型是float64。
package main
import "fmt"
func main() {
var v1 float32 = 3.14
v2 := 99.9 // 默认推断为 float64
// 不同浮点类型运算也需要转换
v3 := float64(v1) + v2
fmt.Println(v1, v2, v3) // 输出: 3.14 99.9 103.04000010490418
}
2.1 精度问题
标准的浮点类型(float32, float64)在计算机中是以二进制形式存储的,它们无法精确表示所有的十进制小数。这会导致一些看似奇怪的计算结果。
package main
import "fmt"
func main() {
v4 := 0.1
v5 := 0.2
result := v4 + v5
fmt.Println("0.1 + 0.2 =", result) // 可能输出: 0.1 + 0.2 = 0.30000000000000004
v6 := 0.3
v7 := 0.2
data := v6 + v7
fmt.Println("0.3 + 0.2 =", data) // 可能输出: 0.3 + 0.2 = 0.5 (有时结果恰好精确)
}
原因简述: 就像十进制无法精确表示 1/3 (0.333...) 一样,二进制也无法精确表示某些十进制小数(如 0.1)。存储时会取一个最接近的二进制近似值,累积运算后误差可能显现。
2.2 浮点数底层存储原理 (IEEE 754 概览)
以 float32 为例,它使用 32 位来存储一个浮点数,这 32 位被划分为三部分:

- 符号位 (Sign): 1 位。0 代表正数,1 代表负数。
- 指数位 (Exponent): 8 位。用于存储科学计数法中的指数。它使用偏移表示法(biased representation),实际指数 = 存储值 - 偏移量(对于 float32,偏移量是 127)。这允许表示非常大和非常小的数。
- 尾数位 (Fraction / Mantissa): 23 位。存储有效数字(科学计数法中小数点后的部分)。它基于一个隐含的规则:规格化的二进制数总是在小数点前有一个'1'(这个'1'不存储,节省了一位),所以实际精度是 24 位。
转换过程大致如下:
- 将十进制浮点数转换为二进制表示(整数部分和小数部分分别转换,小数部分使用“乘2取整法”)。
- 将二进制浮点数表示为科学计数法形式:
1.xxxx... * 2^exponent。 - 提取符号、计算偏移后的指数、提取尾数(小数点后的部分),并将它们填入对应的位。
float64 类似,但有 1 位符号位、11 位指数位(偏移量 1023)和 52 位尾数位,提供更大的范围和更高的精度。
关键点: 由于尾数位是有限的,如果二进制表示是无限循环的(如十进制 0.1),就只能存储其近似值,这就是精度问题的根源。
2.3 高精度小数 (decimal)
当需要精确的小数运算时(尤其是在金融等领域),不应使用 float32 或 float64。 Go 标准库没有内置精确的十进制类型,但可以使用第三方库,其中 github.com/shopspring/decimal 是一个广泛使用的选择。
第一步:安装
在你的项目目录下打开终端,运行:
go get github.com/shopspring/decimal
第二步:使用
package main
import (
"fmt"
"github.com/shopspring/decimal" // 导入包
)
func main() {
// 推荐从字符串创建,避免 float 自身的精度问题
d1, _ := decimal.NewFromString("0.1")
d2, _ := decimal.NewFromString("0.2")
// 也可以从 float 创建,但要意识到 float 可能已不精确
f1 := 0.0000000000019
df1 := decimal.NewFromFloat(f1)
df2 := decimal.NewFromFloat(0.29)
// 运算返回新的 decimal 对象,原对象不变
addResult := d1.Add(d2) // 0.1 + 0.2
fmt.Println("d1 + d2 =", addResult.String()) // 输出: d1 + d2 = 0.3 (精确!)
subResult := df1.Sub(df2) // df1 - df2
mulResult := df1.Mul(df2) // df1 * df2
divResult := df1.Div(df2) // df1 / df2 (注意除不尽的情况,默认精度可能很高)
fmt.Println("df1 - df2 =", subResult)
fmt.Println("df1 * df2 =", mulResult)
fmt.Println("df1 / df2 =", divResult)
// 精度控制和舍入
price, _ := decimal.NewFromString("3.467")
// Round(places) 四舍五入到指定小数位数
rounded := price.Round(2)
fmt.Println("Rounded to 2 places:", rounded) // 输出: Rounded to 2 places: 3.47
// Truncate(places) 直接截断到指定小数位数
truncated := price.Truncate(1)
fmt.Println("Truncated to 1 place:", truncated) // 输出: Truncated to 1 place: 3.4
}
3. 布尔类型 (Boolean)
布尔类型 (bool) 只有两个可能的值:true 和 false。它主要用于逻辑判断和控制流程(如 if 语句、for 循环条件等)。其零值是 false。
package main
import (
"fmt"
"strconv"
)
func main() {
var isReady bool // 零值为 false
fmt.Println("Initial isReady:", isReady)
isValid := true
fmt.Println("isValid:", isValid)
// 布尔运算
fmt.Println("true && false =", true && false) // 逻辑与: false
fmt.Println("true || false =", true || false) // 逻辑或: true
fmt.Println("!true =", !true) // 逻辑非: false
// 字符串与布尔值转换 (使用 strconv)
// ParseBool 接受: "1", "t", "T", "true", "TRUE", "True" (为 true)
// "0", "f", "F", "false", "FALSE", "False" (为 false)
// 其他输入会返回错误
boolVal1, err1 := strconv.ParseBool("True")
fmt.Println(boolVal1, err1) // 输出: true <nil>
boolVal2, err2 := strconv.ParseBool("no")
fmt.Println(boolVal2, err2) // 输出: false strconv.ParseBool: parsing "no": invalid syntax
// FormatBool 将布尔值转为字符串 "true" 或 "false"
strVal := strconv.FormatBool(false)
fmt.Println(strVal) // 输出: "false"
}
4. 字符串 (String)
字符串 (string) 类型用于表示文本数据。在 Go 中,字符串是不可变的 (immutable) 字节序列,通常(但不强制)包含 UTF-8 编码的文本。
package main
import "fmt"
func main() {
var name string = "alex" // 声明并初始化
fmt.Println(name)
title := "生活要想过得去,头上总得带点绿" // 使用短声明
fmt.Println(title)
// 字符串是不可变的
// title[0] = '人' // 编译错误: cannot assign to title[0]
}
4.1 字符串底层存储与 UTF-8
- 字节序列: Go 字符串的底层是一个只读的字节切片 (
[]byte)。len(str)返回的是字节数量。 - UTF-8 编码: Go 源代码文件默认使用 UTF-8 编码,字符串字面量也按 UTF-8 处理。UTF-8 是一种变长编码,ASCII 字符占 1 字节,常用汉字通常占 3 字节,一些特殊字符或表情符号可能占 4 字节。
rune类型:rune是int32的别名,用于表示一个 Unicode 码点。当需要按字符(而不是字节)处理字符串时,通常会用到rune。
package main
import (
"fmt"
"strconv"
"unicode/utf8"
)
func main() {
name := "张翼德" // UTF-8 编码: "张"占3字节, "翼"占3字节, "德"占3字节
// 1. 访问字节 (使用索引)
fmt.Println("Byte 0:", name[0], "Binary:", strconv.FormatInt(int64(name[0]), 2)) // "张"的第一个字节 (230)
fmt.Println("Byte 1:", name[1], "Binary:", strconv.FormatInt(int64(name[1]), 2)) // "张"的第二个字节 (173)
fmt.Println("Byte 2:", name[2], "Binary:", strconv.FormatInt(int64(name[2]), 2)) // "张"的第三个字节 (166)
// ...以此类推
// 2. 获取长度
fmt.Println("字节长度 (len):", len(name)) // 输出: 9 (3*3)
fmt.Println("字符长度 (RuneCountInString):", utf8.RuneCountInString(name)) // 输出: 3
// 3. 字符串 <-> 字节切片 (`[]byte`)
byteSlice := []byte(name)
fmt.Println("Byte Slice:", byteSlice) // 输出: [230 173 166 230 178 155 233 189 144]
strFromBytes := string(byteSlice)
fmt.Println("String from Bytes:", strFromBytes)
// 4. 字符串 <-> Rune 切片 (`[]rune`)
runeSlice := []rune(name) // 将字符串解码为 Unicode 码点序列
fmt.Println("Rune Slice:", runeSlice) // 输出: [27494 27803 40784] (十进制码点)
fmt.Printf("Runes (Hex): [%x %x %x]\n", runeSlice[0], runeSlice[1], runeSlice[2]) // 输出: [6b66 6c9b 9f50] (十六进制码点)
strFromRunes := string(runeSlice)
fmt.Println("String from Runes:", strFromRunes)
}
4.2 字符串常见功能 (strings 包)
strings 包提供了大量用于字符串操作的函数。
package main
import (
"bytes"
"fmt"
"strings" // 导入 strings 包
"strconv"
)
func main() {
s := " Hello, 世界! Hello! "
chinese := "张翼德是帅哥"
// 获取长度 (字节 vs 字符 - 已在 4.1 演示)
// 前缀/后缀检查
fmt.Println("HasPrefix ' H':", strings.HasPrefix(s, " H")) // true
fmt.Println("HasSuffix '! ':", strings.HasSuffix(s, "! ")) // true
fmt.Println("HasPrefix '张':", strings.HasPrefix(chinese, "张")) // true
fmt.Println("HasSuffix '哥':", strings.HasSuffix(chinese, "哥")) // true
// 包含检查
fmt.Println("Contains '世界':", strings.Contains(s, "世界")) // true
fmt.Println("Contains '帅哥':", strings.Contains(chinese, "帅哥")) // true
fmt.Println("ContainsAny 'abc':", strings.ContainsAny(s, "abc")) // false (s 中不含 a, b, c 中任何一个)
fmt.Println("ContainsRune '界':", strings.ContainsRune(s, '界')) // true (检查是否包含某个 rune)
// 查找子串位置 (返回第一个匹配的起始索引, -1 表示未找到)
fmt.Println("Index 'l':", strings.Index(s, "l")) // 3 (第一个 'l')
fmt.Println("LastIndex 'l':", strings.LastIndex(s, "l")) // 15 (最后一个 'l')
fmt.Println("Index '沛':", strings.Index(chinese, "沛")) // 3 (沛 的起始字节索引)
// 大小写转换 (只对 ASCII 字符有效)
fmt.Println("ToUpper:", strings.ToUpper("Hello Go")) // "HELLO GO"
fmt.Println("ToLower:", strings.ToLower("Hello Go")) // "hello go"
// 去除空白或指定字符 (Trim*, TrimSpace)
fmt.Println("TrimSpace:", "'"+strings.TrimSpace(s)+"'") // "'Hello, 世界! Hello!'" (去除两端空白)
fmt.Println("TrimPrefix ' H':", strings.TrimPrefix(s, " H")) // "ello, 世界! Hello! "
fmt.Println("TrimSuffix '! ':", strings.TrimSuffix(s, "! ")) // " Hello, 世界! Hello"
fmt.Println("Trim ' H!':", strings.Trim(s, " H!")) // "ello, 世界" (去除两端 H, !, 空格 的任意组合)
// 替换
// Replace(s, old, new, n) n > 0 替换n次, n < 0 替换所有
fmt.Println("Replace 'l'->'L' (n=2):", strings.Replace(s, "l", "L", 2)) // " HeLLo, 世界! Hello! "
fmt.Println("ReplaceAll 'l'->'L':", strings.ReplaceAll(s, "l", "L")) // " HeLLo, 世界! HeLLo! "
// 分割 (Split, SplitN, SplitAfter, Fields)
parts := strings.Split(chinese, "是")
fmt.Printf("Split by '是': %q\n", parts) // ["张懿德" "帅哥"]
fields := strings.Fields(" first second third ")
fmt.Printf("Fields: %q\n", fields) // ["first" "second" "third"] (按空白分割)
// 拼接 (Join) - 高效方式
dataList := []string{"我爱", "北京天安门"}
joined := strings.Join(dataList, "...")
fmt.Println("Joined:", joined) // "我爱...北京天安门"
// 高效拼接 (大量字符串时)
// Go 1.10+ 推荐使用 strings.Builder
var builder strings.Builder
builder.WriteString("你好,")
builder.WriteString(" Go!")
builder.WriteString(" 效率高!")
efficientStr := builder.String()
fmt.Println("Builder:", efficientStr)
// Go 1.10 之前使用 bytes.Buffer
var buffer bytes.Buffer
buffer.WriteString("Buffer ")
buffer.WriteString("is ")
buffer.WriteString("older.")
bufferStr := buffer.String()
fmt.Println("Buffer:", bufferStr)
// 字符串与数字转换 (已在整型部分演示 Atoi/Itoa, ParseInt/FormatInt)
numStr := "12345"
numInt, _ := strconv.Atoi(numStr)
fmt.Println("Atoi:", numInt)
intVal := 6789
strNum := strconv.Itoa(intVal)
fmt.Println("Itoa:", strNum)
// 字符串与 Rune 转换 (单个字符/码点)
firstChar, size := utf8.DecodeRuneInString(chinese) // 解码第一个 rune 及其字节大小
fmt.Printf("First rune: %c, size: %d bytes\n", firstChar, size) // First rune: 武, size: 3 bytes
runeA := 'A' // rune 字面量用单引号
strA := string(runeA)
fmt.Println("String from rune 'A':", strA) // String from rune 'A': A
strWu := string(27494) // 从 Unicode 码点创建字符串
fmt.Println("String from codepoint 27494:", strWu) // String from codepoint 27494: 武
}
4.3 索引、切片与循环
- 索引
str[i]: 获取字符串在索引i处的字节 (byte)。对于多字节字符,这只会得到字符的一部分。 - 切片
str[i:j]: 获取从索引i(包含) 到j(不包含) 的字节组成的子字符串。需要注意切片的边界必须落在 UTF-8 字符的边界上,否则可能产生无效的 UTF-8 序列。 for i := 0; i < len(str); i++: 按字节遍历字符串。for index, runeValue := range str: 推荐的方式,按 rune (字符) 遍历字符串。index是每个 rune 的起始字节索引,runeValue是该 rune 的 Unicode 码点。
package main
import "fmt"
func main() {
name := "武沛齐"
// 1. 索引获取字节 (不推荐用于访问字符)
byte0 := name[0]
fmt.Println("Byte at index 0:", byte0) // 230 ('张'的第一个字节)
// 2. 切片获取子串 (按字节)
firstCharBytes := name[0:3] // '张' 占 3 个字节
fmt.Println("Slice [0:3]:", firstCharBytes) // "张"
// 3. for 循环遍历字节
fmt.Println("Looping by bytes:")
for i := 0; i < len(name); i++ {
fmt.Printf(" Index %d, Byte %d (%#x)\n", i, name[i], name[i])
}
// 4. for range 循环遍历 Rune (推荐)
fmt.Println("\nLooping by runes (for range):")
for index, runeValue := range name {
fmt.Printf(" Byte Index %d, Rune %c (Codepoint %U)\n", index, runeValue, runeValue)
}
// 输出:
// Byte Index 0, Rune 张 (Codepoint U+6B66)
// Byte Index 3, Rune 懿 (Codepoint U+6C9B)
// Byte Index 6, Rune 德 (Codepoint U+9F50)
// 5. 如果需要按字符索引访问 (效率较低, 因为需要先转换)
runeSlice := []rune(name)
fmt.Println("\nAccessing by rune index:")
fmt.Printf(" Rune at index 1: %c\n", runeSlice[1]) // 懿 (注意索引是 0-based)
}
5. 数组 (Array)
数组是具有固定长度且包含相同类型元素的序列。数组的长度是其类型的一部分。
5.1 声明与初始化
package main
import "fmt"
func main() {
// 方式一:先声明再赋值 (声明时已分配内存,元素初始化为零值)
var numbers [3]int // 声明长度为 3 的 int 数组, numbers = [0 0 0]
numbers[0] = 999
numbers[1] = 666
numbers[2] = 333
fmt.Println("Numbers:", numbers) // Output: Numbers: [999 666 333]
// 方式二:声明同时初始化
var names = [2]string{"张翼德", "alex"} // 类型 [2]string
fmt.Println("Names:", names) // Output: Names: [张翼德 alex]
// 方式三:使用索引初始化 (未指定的元素为零值)
var ages = [3]int{0: 87, 2: 99} // ages = [87 0 99]
fmt.Println("Ages:", ages) // Output: Ages: [87 0 99]
// 方式四:使用 ... 自动推断长度
var cities = [...]string{"北京", "上海", "广州"} // cities 类型为 [3]string
fmt.Println("Cities:", cities, "Length:", len(cities)) // Output: Cities: [北京 上海 广州] Length: 3
var data = [...]int{0: 1, 5: 10} // 长度为 6 (最大索引+1), data = [1 0 0 0 0 10]
fmt.Println("Data:", data, "Length:", len(data)) // Output: Data: [1 0 0 0 0 10] Length: 6
// 数组指针
var ptrToArray *[3]int // 声明一个指向 [3]int 数组的指针, ptrToArray = nil
fmt.Println("ptrToArray:", ptrToArray) // Output: ptrToArray: <nil>
// 使用 new 创建数组并获取指针
ptrCreated := new([3]int) // ptrCreated 是 *[3]int 类型, 指向 [0 0 0]
fmt.Println("ptrCreated:", ptrCreated, "*ptrCreated:", *ptrCreated)
// Output: ptrCreated: &[0 0 0] *ptrCreated: [0 0 0]
ptrCreated[0] = 111 // 可以直接通过指针修改元素
fmt.Println("After modification:", *ptrCreated) // Output: After modification: [111 0 0]
}
5.2 数组内存管理
- 连续内存: 数组的元素在内存中是连续存储的。
- 地址: 数组变量本身的地址就是其第一个元素的地址。后续元素的地址根据元素类型的大小依次递增。
- 字符串元素: 如果数组元素是字符串,数组本身存储的是字符串头(一个包含指向底层字节数据的指针和长度的结构),字符串的实际字节内容可能存储在内存的其他地方。
package main
import "fmt"
func main() {
// 示例 1: int8 (1字节)
nums8 := [3]int8{11, 22, 33}
fmt.Printf("int8 数组地址: %p\n", &nums8)
fmt.Printf(" Element 0 地址: %p\n", &nums8[0])
fmt.Printf(" Element 1 地址: %p\n", &nums8[1]) // 地址+1
fmt.Printf(" Element 2 地址: %p\n", &nums8[2]) // 地址+2
fmt.Println("---")
// 示例 2: int32 (4字节)
nums32 := [3]int32{11, 22, 33}
fmt.Printf("int32 数组地址: %p\n", &nums32)
fmt.Printf(" Element 0 地址: %p\n", &nums32[0])
fmt.Printf(" Element 1 地址: %p\n", &nums32[1]) // 地址+4
fmt.Printf(" Element 2 地址: %p\n", &nums32[2]) // 地址+8
fmt.Println("---")
// 示例 3: string (在 64 位系统上通常占 16 字节 - 指针 8 + 长度 8)
names := [2]string{"张翼德", "alex"}
fmt.Printf("string 数组地址: %p\n", &names)
fmt.Printf(" Element 0 地址: %p\n", &names[0])
fmt.Printf(" Element 1 地址: %p\n", &names[1]) // 地址 + (string 头大小)
}
(注意: 输出的内存地址每次运行都可能不同,但地址间的差值反映了元素大小)
5.3 可变性与拷贝行为
- 元素可变: 数组的元素可以通过索引进行修改。
- 长度类型不可变: 数组一旦声明,其长度和元素类型就固定了,不能改变。
- 值类型: 数组在 Go 中是值类型。这意味着当将一个数组赋值给另一个变量,或者将数组作为参数传递给函数时,会创建该数组的一个完整副本。修改副本不会影响原始数组。
package main
import "fmt"
func main() {
names1 := [2]string{"张翼德", "alex"}
names2 := names1 // 赋值操作,names2 是 names1 的一个副本
fmt.Println("Before modification:")
fmt.Println(" names1:", names1) // [张翼德 alex]
fmt.Println(" names2:", names2) // [张翼德 alex]
names1[1] = "苑昊" // 修改 names1
fmt.Println("\nAfter modification:")
fmt.Println(" names1:", names1) // [张翼德 苑昊]
fmt.Println(" names2:", names2) // [张翼德 alex] (names2 不受影响)
// 字符串本身是不可变的,但数组中的字符串元素可以被替换为另一个字符串
// names1[0][0] = '王' // 编译错误,字符串不可变
}
5.4 长度、索引、切片与循环
与字符串类似,但操作对象是数组元素。
package main
import "fmt"
func main() {
nums := [3]int32{11, 22, 33}
// 1. 长度
fmt.Println("Length:", len(nums)) // 3
// 2. 索引 (访问和修改)
fmt.Println("Element at index 1:", nums[1]) // 22
nums[1] = 222
fmt.Println("Modified nums:", nums) // [11 222 33]
// 3. 切片 (从数组创建切片 slice)
// 切片是对底层数组的一个视图,更灵活,后续会详细讲
// numSlice := nums[0:2] // 创建一个包含 nums[0], nums[1] 的切片
// fmt.Println("Slice [0:2]:", numSlice) // [11 222]
// fmt.Printf("Slice type: %T\n", numSlice) // []int32 (注意类型是切片,不是数组)
// 4. for 循环遍历
fmt.Println("\nLooping with classic for:")
for i := 0; i < len(nums); i++ {
fmt.Printf(" Index %d, Value %d\n", i, nums[i])
}
// 5. for range 循环遍历 (推荐)
fmt.Println("\nLooping with for range:")
for index, value := range nums {
fmt.Printf(" Index %d, Value %d\n", index, value)
}
// 只关心索引
fmt.Println("\nLooping for index only:")
for index := range nums {
fmt.Println(" Index", index)
}
// 只关心值
fmt.Println("\nLooping for value only:")
for _, value := range nums {
fmt.Println(" Value", value)
}
}
5.5 数组嵌套 (多维数组)
可以创建数组的数组,形成多维数组。
package main
import "fmt"
func main() {
// 声明一个 2x3 的二维整数数组
var matrix [2][3]int // [[0 0 0] [0 0 0]]
// 赋值
matrix[0] = [3]int{1, 2, 3}
matrix[1][1] = 5
fmt.Println("Matrix:", matrix) // [[1 2 3] [0 5 0]]
// 声明并初始化
grid := [2][3]int{
{11, 12, 13}, // 第一行
{21, 22, 23}, // 第二行
}
fmt.Println("Grid:", grid) // [[11 12 13] [21 22 23]]
// 访问元素
fmt.Println("Grid[1][2]:", grid[1][2]) // 23
// 遍历二维数组
fmt.Println("\nIterating grid:")
for i := 0; i < len(grid); i++ { // 遍历行
for j := 0; j < len(grid[i]); j++ { // 遍历列
fmt.Printf(" grid[%d][%d] = %d\n", i, j, grid[i][j])
}
}
// 使用 for range 遍历
fmt.Println("\nIterating grid with range:")
for rowIndex, row := range grid {
for colIndex, value := range row {
fmt.Printf(" grid[%d][%d] = %d\n", rowIndex, colIndex, value)
}
}
}
作业题 (21道)
-
Go语言中int占多少字节?
- 取决于操作系统架构。在 32 位系统上占 4 字节 (32位),在 64 位系统上占 8 字节 (64位)。
-
整型中有符号和无符号是什么意思?
- 有符号整型 (signed, 如
int,int8...) 可以表示负数、零和正数。其最高位用作符号位 (0为正, 1为负)。 - 无符号整型 (unsigned, 如
uint,uint8...) 只能表示零和正数。所有位都用于表示数值大小。
- 有符号整型 (signed, 如
-
整型可以表示的最大范围是多少?超出怎么办?
- 最大范围取决于具体的类型(如
int8是 127,uint8是 255,int64约 9.22x10^18)。 - 如果数值超出了所选类型的范围,会发生溢出 (overflow)。对于无符号类型,通常是回绕 (wrap around)(例如
uint8的 255 + 1 变成 0)。对于有符号类型,溢出行为可能导致数值和符号都变得不符合预期(如int8的 127 + 1 变成 -128)。如果需要处理可能超出int64/uint64的值,应使用math/big包。
- 最大范围取决于具体的类型(如
-
什么是nil?
nil是 Go 语言中指针、接口、切片、字典、通道和函数类型的零值。它表示这些类型的变量没有指向任何有效的实例或内存地址。
-
十进制是以整型方式存在,其他进制则是以字符串的形式存在?如何实现进制之间的转换?
- 在代码中直接写的数字(如
99)默认为十进制整型。 - 十进制(整型) 转 其他进制(字符串): 使用
strconv.FormatInt(value, base)或strconv.FormatUint(value, base),其中base是目标进制 (2, 8, 16)。 - 其他进制(字符串) 转 十进制(整型): 使用
strconv.ParseInt(str, base, bitSize)或strconv.ParseUint(str, base, bitSize),其中base是源字符串的进制 (2, 8, 16 或 0 自动检测),bitSize限制结果的位数。
- 在代码中直接写的数字(如
-
简述如下代码的意义
var v1 int // 声明一个 int 型变量 v1,初始化为零值 0。 var v2 *int // 声明一个指向 int 的指针变量 v2,初始化为零值 nil。 var v3 = new(int) // 使用 new 分配一个 int 的内存空间,初始化为 0,并将指向该空间的指针 (*int) 赋值给 v3。 -
浮点型为什么有时无法精确表示小数?
- 因为计算机内部使用二进制 (IEEE 754 标准) 存储浮点数。很多十进制小数(如 0.1)在二进制下是无限循环小数,无法用有限的位数精确表示,只能存储一个近似值。运算过程中这些微小的误差可能累积,导致结果不精确。
-
如何使用第三方包 decimal?
- 安装: 在项目目录下运行
go get github.com/shopspring/decimal。 - 导入: 在代码中
import "github.com/shopspring/decimal"。 - 创建: 使用
decimal.NewFromString("value")(推荐) 或decimal.NewFromFloat(floatValue)创建decimal.Decimal对象。 - 运算: 调用对象的方法进行加 (
Add)、减 (Sub)、乘 (Mul)、除 (Div) 等运算。这些方法通常返回新的decimal.Decimal对象。 - 输出/转换: 使用
.String()方法获取其精确的字符串表示。
- 安装: 在项目目录下运行
-
简述 ascii、unicode、utf-8的关系。
- ASCII: 最早的美国标准字符集,用 7 位 (后扩展到 8 位) 表示 128 (或 256) 个字符,主要包含英文字母、数字、标点和控制符。范围有限。
- Unicode: 一个国际标准,旨在为世界上所有字符(包括各种语言、符号、表情等)分配一个唯一的数字标识符,称为码点 (Code Point)。它是一个字符集 (Character Set)。
- UTF-8: Unicode 字符集的一种编码方案 (Encoding Scheme)。它将 Unicode 码点转换为可变长度的字节序列进行存储和传输。UTF-8 的优点是兼容 ASCII(ASCII 字符只占 1 字节),且对常用字符(包括 CJK)有较好的空间效率。它是目前最广泛使用的编码方式。
- 关系:Unicode 定义了“字符->数字(码点)”的映射,UTF-8 定义了“数字(码点)->字节序列”的转换规则。
-
判断:Go语言中的字符串是utf-8编码的字节序列。
- 正确。 Go 语言规范推荐并默认按 UTF-8 处理字符串。
string类型的底层是字节序列,这些字节通常(但不强制)代表 UTF-8 编码的文本。
- 正确。 Go 语言规范推荐并默认按 UTF-8 处理字符串。
-
什么是rune?
rune是 Go 语言中int32类型的别名。它专门用于表示一个 Unicode 码点。当需要按字符(而不是字节)处理字符串时,通常会涉及rune类型。
-
判断:字符串是否可变?
- 不可变 (Immutable)。 Go 中的字符串一旦创建,其内容(字节序列)就不能被修改。任何看起来像修改字符串的操作(如替换、拼接)实际上都是创建了一个新的字符串。
-
列举你记得的字符串的常见功能? (至少 5 个)
len(str): 获取字节长度。utf8.RuneCountInString(str): 获取字符(rune)数量。strings.HasPrefix(s, prefix): 检查前缀。strings.HasSuffix(s, suffix): 检查后缀。strings.Contains(s, substr): 检查是否包含子串。strings.Index(s, substr): 查找子串首次出现位置。strings.ReplaceAll(s, old, new): 替换所有子串。strings.Split(s, sep): 按分隔符分割字符串。strings.Join(elems []string, sep string): 拼接字符串切片。strings.ToLower(s)/strings.ToUpper(s): 大小写转换。strings.TrimSpace(s): 去除两端空白。[]byte(str)/string(byteSlice): 字符串与字节切片互转。[]rune(str)/string(runeSlice): 字符串与 rune 切片互转。for index, runeValue := range str: 按 rune 遍历。
-
字符串和 “字节集合” 、“rune集合” 如何实现相互转换?
- 字符串 转 字节集合 (
[]byte):byteSlice := []byte(myString) - 字节集合 转 字符串:
myString := string(byteSlice) - 字符串 转 Rune 集合 (
[]rune):runeSlice := []rune(myString) - Rune 集合 转 字符串:
myString := string(runeSlice)
- 字符串 转 字节集合 (
-
字符串如何实现高效的拼接?
- 对于少量、固定的拼接,使用
+操作符尚可。 - 对于多次或在循环中拼接大量字符串:
- 推荐: 使用
strings.Builder。它内部优化了内存分配,效率最高。var builder strings.Builder builder.WriteString("part1") builder.WriteString("part2") result := builder.String() - 使用
strings.Join(slice, ""):如果已经有一个字符串切片,这是非常好的选择。 - 使用
bytes.Buffer:在 Go 1.10 之前是推荐的高效方式,现在strings.Builder通常更好。
- 推荐: 使用
- 对于少量、固定的拼接,使用
-
简述数组的存储原理。
- 数组的元素在内存中是连续存储的,没有间隙。
- 数组变量存储的是整个数组的数据(或者说,第一个元素的地址隐式代表了整个数组的起始位置)。
- 访问元素
arr[i]时,计算机会根据数组的起始地址、元素类型的大小和索引i,直接计算出第i个元素的内存地址(起始地址 + i * 元素大小),然后读取或写入该地址的数据。 - 因为内存连续且大小固定,数组的随机访问(通过索引)效率非常高 (O(1))。
-
根据要求写代码
package main import "fmt" func main() { names := [3]string{"Alex", "超级无敌小jj", "傻儿子"} // a. 请根据索引获取“傻儿子” son := names[2] fmt.Println("a:", son) // b. 请根据索引获取“Alex” (假设大小写敏感) alex := names[0] fmt.Println("b:", alex) // c. 请根据索引获取“超级无敌小jj” jj := names[1] fmt.Println("c:", jj) // d. 请将name数组的最后一个元素修改为 “大烧饼” names[2] = "大烧饼" fmt.Println("d: Modified array:", names) } -
看代码写输出结果
var nestData [3][2]int fmt.Println(nestData)- 输出结果:
[[0 0] [0 0] [0 0]] - 原因:声明了一个 3x2 的 int 二维数组。由于没有显式初始化,所有元素都被初始化为其类型的零值,对于
int来说就是0。
- 输出结果:
-
请声明一个有3个元素的数组,元素的类型是有两个元素的数组,并在数组中初始化值。例如:
// [ // ["alex","qwe123"], // ["eric","admin11"], // ["tony","pp1111"] // ] package main import "fmt" func main() { // 元素类型是 [2]string // 数组本身有 3 个元素 userCredentials := [3][2]string{ {"alex", "qwe123"}, {"eric", "admin11"}, {"tony", "pp1111"}, } fmt.Println(userCredentials) } -
循环如下数组并使用字符串格式化输出如下内容:
// dataList := [ // ["alex","qwe123"], // ["eric","admin11"], // ["tony","pp1111"] // ] // // 请补充代码... // // 最终实现输出: // 我是alex,我的密码是qwe123。 // 我是eric,我的密码是admin11。 // 我是tony,我的密码是pp1111。 package main import "fmt" func main() { dataList := [3][2]string{ {"alex", "qwe123"}, {"eric", "admin11"}, {"tony", "pp1111"}, } for _, userData := range dataList { username := userData[0] password := userData[1] fmt.Printf("我是%s,我的密码是%s。\n", username, password) } } -
补充代码实现用户登录
// userList表示有三个用户,每个用户有用户名和密码,例如:用户名是alex,密码是qwe213 // userList := [ // ["alex","qwe123"], // ["eric","admin11"], // ["tony","pp1111"] // ] // // 需求:提示让用户输入用户名和密码,然后再userList中校验用户名和密码是否正确。 package main import "fmt" func main() { userList := [3][2]string{ {"alex", "qwe123"}, {"eric", "admin11"}, {"tony", "pp1111"}, } var inputUsername string var inputPassword string fmt.Print("请输入用户名: ") fmt.Scanln(&inputUsername) // Scanln 读取一行输入 fmt.Print("请输入密码: ") fmt.Scanln(&inputPassword) loginSuccess := false // 标记是否登录成功 for _, userData := range userList { storedUsername := userData[0] storedPassword := userData[1] if inputUsername == storedUsername && inputPassword == storedPassword { loginSuccess = true break // 找到匹配用户,跳出循环 } } if loginSuccess { fmt.Println("登录成功!") } else { fmt.Println("用户名或密码错误!") } }
浙公网安备 33010602011771号