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 到 127
  • int16: 16位,范围 -32,768 到 32,767
  • int32: 32位,范围 -2,147,483,648 到 2,147,483,647
  • int64: 64位,范围 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807
  • int: 平台相关。在 32 位系统上是 int32,在 64 位系统上是 int64。通常直接使用 int 即可,除非有特定的大小需求或跨平台兼容性考虑。

无符号整型:

  • uint8: 8位,范围 0 到 255 (也是 byte 类型的底层类型)
  • uint16: 16位,范围 0 到 65,535
  • uint32: 32位,范围 0 到 4,294,967,295
  • uint64: 64位,范围 0 到 18,446,744,073,709,551,615
  • uint: 平台相关。在 32 位系统上是 uint32,在 64 位系统上是 uint64
  • uintptr: 无符号整型,大小足以存储指针的位模式。主要用于底层编程。

选择原则: 根据你的数据范围需求选择最小的能满足要求的类型,可以节省内存。但通常为了简化代码,直接使用 intuint 也很常见。

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.FormatIntstrconv.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.ParseIntstrconv.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: 使用短变量声明 :=,声明并初始化 v2999,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)

当需要的整数值超出了 int64uint64 的范围时,可以使用 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 位被划分为三部分:

float32 存储结构

  1. 符号位 (Sign): 1 位。0 代表正数,1 代表负数。
  2. 指数位 (Exponent): 8 位。用于存储科学计数法中的指数。它使用偏移表示法(biased representation),实际指数 = 存储值 - 偏移量(对于 float32,偏移量是 127)。这允许表示非常大和非常小的数。
  3. 尾数位 (Fraction / Mantissa): 23 位。存储有效数字(科学计数法中小数点后的部分)。它基于一个隐含的规则:规格化的二进制数总是在小数点前有一个'1'(这个'1'不存储,节省了一位),所以实际精度是 24 位。

转换过程大致如下:

  1. 将十进制浮点数转换为二进制表示(整数部分和小数部分分别转换,小数部分使用“乘2取整法”)。
  2. 将二进制浮点数表示为科学计数法形式:1.xxxx... * 2^exponent
  3. 提取符号、计算偏移后的指数、提取尾数(小数点后的部分),并将它们填入对应的位。

float64 类似,但有 1 位符号位、11 位指数位(偏移量 1023)和 52 位尾数位,提供更大的范围和更高的精度。

关键点: 由于尾数位是有限的,如果二进制表示是无限循环的(如十进制 0.1),就只能存储其近似值,这就是精度问题的根源。

2.3 高精度小数 (decimal)

当需要精确的小数运算时(尤其是在金融等领域),不应使用 float32float64 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) 只有两个可能的值:truefalse。它主要用于逻辑判断和控制流程(如 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 类型: runeint32 的别名,用于表示一个 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道)

  1. Go语言中int占多少字节?

    • 取决于操作系统架构。在 32 位系统上占 4 字节 (32位),在 64 位系统上占 8 字节 (64位)。
  2. 整型中有符号和无符号是什么意思?

    • 有符号整型 (signed, 如 int, int8...) 可以表示负数、零和正数。其最高位用作符号位 (0为正, 1为负)。
    • 无符号整型 (unsigned, 如 uint, uint8...) 只能表示零和正数。所有位都用于表示数值大小。
  3. 整型可以表示的最大范围是多少?超出怎么办?

    • 最大范围取决于具体的类型(如 int8 是 127, uint8 是 255, int64 约 9.22x10^18)。
    • 如果数值超出了所选类型的范围,会发生溢出 (overflow)。对于无符号类型,通常是回绕 (wrap around)(例如 uint8 的 255 + 1 变成 0)。对于有符号类型,溢出行为可能导致数值和符号都变得不符合预期(如 int8 的 127 + 1 变成 -128)。如果需要处理可能超出 int64/uint64 的值,应使用 math/big 包。
  4. 什么是nil?

    • nil 是 Go 语言中指针、接口、切片、字典、通道和函数类型的零值。它表示这些类型的变量没有指向任何有效的实例或内存地址。
  5. 十进制是以整型方式存在,其他进制则是以字符串的形式存在?如何实现进制之间的转换?

    • 在代码中直接写的数字(如 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 限制结果的位数。
  6. 简述如下代码的意义

    var v1 int      // 声明一个 int 型变量 v1,初始化为零值 0。
    var v2 *int     // 声明一个指向 int 的指针变量 v2,初始化为零值 nil。
    var v3 = new(int) // 使用 new 分配一个 int 的内存空间,初始化为 0,并将指向该空间的指针 (*int) 赋值给 v3。
    
  7. 浮点型为什么有时无法精确表示小数?

    • 因为计算机内部使用二进制 (IEEE 754 标准) 存储浮点数。很多十进制小数(如 0.1)在二进制下是无限循环小数,无法用有限的位数精确表示,只能存储一个近似值。运算过程中这些微小的误差可能累积,导致结果不精确。
  8. 如何使用第三方包 decimal?

    1. 安装: 在项目目录下运行 go get github.com/shopspring/decimal
    2. 导入: 在代码中 import "github.com/shopspring/decimal"
    3. 创建: 使用 decimal.NewFromString("value") (推荐) 或 decimal.NewFromFloat(floatValue) 创建 decimal.Decimal 对象。
    4. 运算: 调用对象的方法进行加 (Add)、减 (Sub)、乘 (Mul)、除 (Div) 等运算。这些方法通常返回新的 decimal.Decimal 对象。
    5. 输出/转换: 使用 .String() 方法获取其精确的字符串表示。
  9. 简述 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 定义了“数字(码点)->字节序列”的转换规则。
  10. 判断:Go语言中的字符串是utf-8编码的字节序列。

    • 正确。 Go 语言规范推荐并默认按 UTF-8 处理字符串。string 类型的底层是字节序列,这些字节通常(但不强制)代表 UTF-8 编码的文本。
  11. 什么是rune?

    • rune 是 Go 语言中 int32 类型的别名。它专门用于表示一个 Unicode 码点。当需要按字符(而不是字节)处理字符串时,通常会涉及 rune 类型。
  12. 判断:字符串是否可变?

    • 不可变 (Immutable)。 Go 中的字符串一旦创建,其内容(字节序列)就不能被修改。任何看起来像修改字符串的操作(如替换、拼接)实际上都是创建了一个新的字符串。
  13. 列举你记得的字符串的常见功能? (至少 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 遍历。
  14. 字符串和 “字节集合” 、“rune集合” 如何实现相互转换?

    • 字符串 转 字节集合 ([]byte): byteSlice := []byte(myString)
    • 字节集合 转 字符串: myString := string(byteSlice)
    • 字符串 转 Rune 集合 ([]rune): runeSlice := []rune(myString)
    • Rune 集合 转 字符串: myString := string(runeSlice)
  15. 字符串如何实现高效的拼接?

    • 对于少量、固定的拼接,使用 + 操作符尚可。
    • 对于多次或在循环中拼接大量字符串:
      • 推荐: 使用 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 通常更好。
  16. 简述数组的存储原理。

    • 数组的元素在内存中是连续存储的,没有间隙。
    • 数组变量存储的是整个数组的数据(或者说,第一个元素的地址隐式代表了整个数组的起始位置)。
    • 访问元素 arr[i] 时,计算机会根据数组的起始地址、元素类型的大小和索引 i,直接计算出第 i 个元素的内存地址(起始地址 + i * 元素大小),然后读取或写入该地址的数据。
    • 因为内存连续且大小固定,数组的随机访问(通过索引)效率非常高 (O(1))。
  17. 根据要求写代码

    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)
    }
    
  18. 看代码写输出结果

    var nestData [3][2]int
    fmt.Println(nestData)
    
    • 输出结果: [[0 0] [0 0] [0 0]]
    • 原因:声明了一个 3x2 的 int 二维数组。由于没有显式初始化,所有元素都被初始化为其类型的零值,对于 int 来说就是 0
  19. 请声明一个有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)
    }
    
  20. 循环如下数组并使用字符串格式化输出如下内容:

    // 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)
         }
    }
    
  21. 补充代码实现用户登录

    // 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("用户名或密码错误!")
        }
    }
    
posted on 2025-04-06 17:39  Leo_Yide  阅读(98)  评论(0)    收藏  举报