三、Go语言语法进阶

0 阅读54分钟

一,函数

1.1 函数的基本结构

函数声明

Go 语言中函数的通用格式:

func 函数名(参数列表) (返回值列表) {
    // 函数体
}

示例

func add(a int, b int) int {
    return a + b
}

关键点

  • func 是声明函数的关键字。
  • 函数名遵循驼峰命名法(例如:calculateSum)。
  • 参数和返回值需要明确类型(Go 是静态类型语言),如果不需要返回值就不用写。

1.2 参数

形式参数(形参)

  • 定义函数时声明的参数。

  • 可以指定参数类型,多个相同类型的参数可简写:

    func multiply(x, y int) int { // x 和 y 都是 int 类型
        return x * y
    }
    

实际参数(实参)

  • 调用函数时传递的具体值。

    result := multiply(3, 4) // 3 和 4 是实参
    

不定参数(Variadic Parameters)

  • ... 表示接受任意数量的参数,本质上是一个切片。

    func sum(numbers ...int) int {
        total := 0
        for _, num := range numbers {
            total += num
        }
        return total
    }
    
    // 调用
    fmt.Println(sum(1, 2, 3)) // 输出 6
    

1.3 返回值

单返回值

  • func isEven(n int) bool {
        return n % 2 == 0
    }
    

多返回值(Go 的特色)

  • 常用于返回结果和错误信息。

    func divide(a, b float64) (float64, error) {
        if b == 0 {
            return 0, errors.New("division by zero")
        }
        return a / b, nil
    }
    
    // 调用
    result, err := divide(10, 2)
    if err != nil {
        log.Fatal(err)
    }
    

命名返回值(Named Return Values)

  • 返回值可以在函数签名中命名,直接作为变量使用。

  • return 语句可省略返回值变量名。

    func rectangleProps(length, width float64) (area, perimeter float64) {
    	area = length * width
    	perimeter = 2 * (length + width)
    	return // 等价于 return area, perimeter
    }
    
    func main() {
    	// 调用
    	area, perimeter := rectangleProps(10, 20)
    	fmt.Println(area, perimeter)
    
    }
    

1.4 函数调用

基本调用

res := add(2, 3) // 返回 5

忽略返回值

  • _ 忽略不需要的返回值:

    _, err := someFunction()
    

1.5 高阶函数特性

1.5.1 函数作为参数

package main

import "fmt"

func applyOperation(a, b int, operation func(int, int) int) int {
	return operation(a, b)
}

func main() {
	// 调用
	result := applyOperation(5, 3, func(x, y int) int { return x * y })
	fmt.Println(result)
}

在java中这个函数式接口的有相同效果

1.5.2 函数作为返回值

func createMultiplier(factor int) func(int) int {
	return func(x int) int {
		return x * factor
	}
}
func main() {
	// 使用
	double := createMultiplier(2)
	fmt.Println(double(5)) // 输出 10
}

1.5.3 匿名函数(Lambda)

没有名字的函数,直接定义并使用。

func main() {
    square := func(x int) int {
        return x * x
    }
    fmt.Println(square(4)) // 16
}

1.5.4 闭包(Closure)

闭包的定义

  • 闭包是一个 函数值(Function Value),它引用了其函数体之外的变量。这个函数可以访问这些变量,并“记住”这些变量的状态,即使外部函数已经执行完毕。闭包的核心是:函数 + 它引用的外部变量环境
  • 用生活场景类比:闭包就像一个随身携带背包的函数,背包里装的是它需要用到的外部变量。
  • 闭包的本质是函数能够访问并记住其词法作用域外的变量

闭包的特性

  1. 捕获外部变量:闭包函数可以访问定义它的作用域中的变量。
  2. 变量生命周期延长:被闭包引用的变量不会随外部函数执行结束而销毁,而是会一直存在,直到闭包不再被使用。

代码示例

  • 最简单的闭包

    package main
    
    import "fmt"
    
    func main() {
        
        // 定义一个闭包
        adder := func() func(int) int {
            sum := 0               // 外部变量 sum
            return func(x int) int {
                sum += x            // 闭包捕获了 sum
                return sum
            }
        }
    
        // 创建闭包实例
        myAdder := adder()
    
        // 调用闭包
        fmt.Println(myAdder(1))  // 1
        fmt.Println(myAdder(2))  // 3
        fmt.Println(myAdder(3))  // 6
    }
    

解释

  • adder 是一个返回闭包函数的外部函数。
  • 闭包内部引用了外部变量 sum
  • 每次调用 myAdder(x),闭包会修改并“记住” sum 的值。
  • 关键点sum 变量的生命周期被延长,直到 myAdder 不再被使用。

核心突破:作用域的延伸

func normalFunc() {
    x := 10 // 局部变量
    // 函数结束时 x 被销毁
}

func closureFunc() func() {
    y := 20 // 被闭包捕获的变量
    return func() {
        y++ // 神奇之处:函数返回后还能修改 y!
        fmt.Println(y)
    }
}

闭包的其他写法:理解本质是捕获了外部作用域的变量并对其修改

package main

func main() {
	y := 0
	closed := func() int {
		y++
		return y
	}
	println(y)
	i1 := closed()
	println(y)
	i2 := closed()
	println(y)
	i3 := closed()
	println(y)
	i4 := closed()
	println(y)
	println(i1, i2, i3, i4)
}

1.5.5 回调函数

回调函数是一个作为参数传递给其他函数的函数,接收方函数会在适当的时候调用它。

语法

// 回调函数类型
type Callback func(int, string)

// 接受回调的函数
func ProcessData(data int, cb Callback) {
    // 处理数据...
    result := fmt.Sprintf("Processed: %d", data)
    // 调用回调
    cb(data, result)
}

func main() {
    // 定义回调实现
    myCallback := func(id int, res string) {
        fmt.Printf("Callback received: ID=%d, Result=%s\n", id, res)
    }
    
    ProcessData(42, myCallback)
}

1.6 函数的值传递

Golang支持两种将参数传递给函数的方式,即按值传递或按值调用以及按引用传递或按引用传递。默认情况下,Golang使用按值调用的方式将参数传递给函数。

1.6.1 按值调用

在此参数传递中,实际参数的值将复制到函数的形式参数中,并且两种类型的参数将存储在不同的存储位置中。因此,在函数内部进行的任何更改都不会反映在调用者的实际参数中。

**示例1:**在下面的程序中,可以看到Z的值不能通过Modify函数修改

package main

import "fmt"

// 函数修改值
func modify(Z int) {
    Z = 70
}

func main() {

    var Z int = 10

    fmt.Printf("函数调用前,Z的值为 = %d", Z)

    //按值调用
    modify(Z)

    fmt.Printf("\n函数调用后,Z的值为 = %d", Z)
}

输出:

函数调用前,Z的值为 = 10
函数调用后,Z的值为 = 10

1.6.2 按引用调用

在这里,将使Pointers(指针)的概念

  • 引用运算符*用于访问地址中的值
  • 地址运算符用于获取任何数据类型的变量的地址

实际参数和形式参数都指向相同的位置,因此在函数内部所做的任何更改实际上都会反映在调用者的实际参数中

示例:在函数调用中,我们传递变量的地址,并使用引用运算符修改值。因此,在函数即*Modify之后,将找到更新后的值。

package main

import "fmt"

// 修改值的函数
func modifydata(Z *int) {
    *Z = 70
}
func main() {
    var Zz int = 10
    fmt.Printf("函数调用前,Zz的值为 = %d", Zz)

    //通过引用调用传递变量Z地址
    modifydata(&Zz)

    fmt.Printf("\n函数调用后,Zz的值为 = %d", Zz)
}

输出

函数调用前,Zz的值为 = 10
函数调用后,Zz的值为 = 70

1.7 Defer关键字

defer 是 Go 语言中一个非常实用的关键字,用于延迟执行某些操作。它的核心逻辑是 “延迟执行,但确保执行”,尤其在处理资源释放(如关闭文件、解锁、数据库连接回收)和错误处理时非常有用。

基本行为

  • defer 会将函数或语句的执行推迟到 当前函数返回之前
  • 多个 defer 会按 后进先出(LIFO) 的顺序执行。

示例:

package main

import "fmt"

func f1() {
	fmt.Println("f1")
}
func f2() {
	fmt.Println("f2")
}
func f3() {
	fmt.Println("f3")
}
func main() {
	defer f1()
	defer f2()
	defer f3()
}
//输出f3,f2,f1

1.8 main和init函数

main函数

在Go语言中,main包是一个特殊的软件包,与可执行程序一起使用,并且该package包含*main()函数。main()函数是一种特殊类型的函数,它是可执行程序的入口点。它不带任何参数也不返回任何内容。由于可以自动调用main()函数,因此无需显式调用main()函数,并且每个可执行程序必须包含一个package main和main()*函数。

示例

//主包的声明
package main

//导入包
import (
    "fmt"

func main() {
    fmt.PrintLn("hello World")
}

init函数

  • init()函数就像main函数一样**,不带任何参数也不返回任何东西。** 每个包中都存在此函数,并且在初始化包时将调用此函数。
  • 该函数是隐式声明的,因此不能从任何地方引用它,并且可以在同一程序中创建多个init()函数,并且它们将按照创建顺序执行
  • 可以在程序中的任何位置创建init()函数,并且它们以词汇文件名顺序(字母顺序)调用。
  • 允许在init()函数中放置语句,但始终记住要在main()函数调用之前执行init()函数,因此它不依赖于main()函数。
  • init()函数的主要目的是初始化无法在全局上下文中初始化的全局变量。
package main 
  
import "fmt"
  
//多个init()函数 
func init() { 
    fmt.Println("init 1") 
} 
  
func init() { 
    fmt.Println("init 2") 
} 
  
func main() { 
    fmt.Println("main") 
}

输出:

init 1
init 2
main

二,结构体

Golang中的结构(struct)是一种用户定义的类型,允许将可能不同类型的项分组/组合成单个类型。任何现实世界中拥有一组属性/字段的实体都可以表示为结构。这个概念通常与面向对象编程中的类进行比较。它可以被称为不支持继承但支持组合的轻量级类。

例如,一个地址具有name,street,city,state,Pincode。如下所示,将这三个属性组合为一个结构Address是有意义的。

2.1 声明结构

举例:

 type Address struct {
      name string 
      street string
      city string
      state string
      Pincode int
}

在上面,type关键字引入了一个新类型。其后是类型的名称(Address)和关键字*struct,*以说明我们正在定义结构。该结构包含花括号内各个字段的列表。每个字段都有一个名称和类型。

**注意:**我们还可以通过组合相同类型的各个字段来使它们紧凑,如下例所示:

type Address struct {
    name, street, city, state string
    Pincode int
}

**定义结构:**定义一个结构体

var a Address

上面的代码创建一个Address类型的变量,默认情况下将其设置为零。对于结构,零表示所有字段均设置为其对应的零值。因此,字段name,street,city,state都设置为“”,而Pincode设置为0。

2.2 初始化结构体

字面量初始化

p1 := Person{Name: "Alice", Age: 30}       // 指定字段名,k:v结构,p1是 *Person类型
p2 := Person{"Bob", 25}                   // 按字段顺序(需全部赋值),p2是 *Person类型

new 函数:返回指针

p3 := new(Person)  // p3 是 *Person 类型
p3.Name = "Charlie"

取地址初始化

p4 := &Person{Name: "Dave", Age: 40}      // 直接返回指针

关键区别总结

特性p1,p2(值类型)p4(指针+显式初始化)p3(指针+零值初始化)
类型Person*Person*Person
字段初始化显式赋值显式赋值零值(需后续赋值)
内存行为直接存储结构体数据存储指向初始化数据的指针存储指向零值数据的指针
复制开销复制整个结构体(可能昂贵)仅复制指针(高效)仅复制指针(高效)
修改影响范围仅影响当前副本影响所有指向该实例的指针影响所有指向该实例的指针

2.3 访问与修改字段

通过 . 操作符访问或修改字段:

fmt.Println(p1.Name)  // 输出: Alice
p1.Age = 31           // 修改字段值

2.4 结构体的比较

2.4.1 == 比较

如果所有字段都是可比较类型(如基本类型),则结构体可比较。

  • 包含不可比较字段(如切片、map)时,编译报错:
p1 := Person{"Alice", 30}
p2 := Person{"Alice", 30}
fmt.Println(p1 == p2)  // 输出: true

2.4.2 DeepEqual()比较

reflect.DeepEqual() 是Go标准库reflect包提供的函数,用于递归地比较两个值的“深度相等性”。它可以处理复杂类型(如结构体、切片、映射、指针等),并检查它们的实际内容是否一致,而非仅仅比较地址或浅层值。

适用场景:

  • 结构体包含切片、映射、函数等不可直接比较的字段。
  • 需要比较嵌套结构体或复杂数据结构的实际内容。

举例

假设有一个包含切片字段的结构体:

type Data struct {
    ID    int
    Tags  []string
}

d1 := Data{ID: 1, Tags: []string{"a", "b"}}
d2 := Data{ID: 1, Tags: []string{"a", "b"}}

// 直接使用 == 会编译报错(因为 Tags 是切片,不可比较)
// fmt.Println(d1 == d2)  // 错误!

// 使用 DeepEqual 进行比较
import "reflect"
fmt.Println(reflect.DeepEqual(d1, d2))  // 输出: true

DeepEqual()的注意事项

  1. 性能开销

    DeepEqual()依赖反射(reflect包),在递归比较大型数据结构时可能产生显著性能损耗。在性能敏感场景需谨慎使用。

  2. 未导出字段(私有字段)

 `DeepEqual()`默认不比较**未导出字段**(首字母小写的字段),即使它们的值相同,结果仍可能返回`false`
 ```go
 type Example struct {
     publicField  int
     privateField int  // 未导出字段
 }
 
 e1 := Example{1, 2}
 e2 := Example{1, 2}
 fmt.Println(reflect.DeepEqual(e1, e2))  // 输出: true(未导出字段被忽略?)
 // 注意:实际上未导出字段会被比较,但可能因反射权限导致不一致,需谨慎依赖。
 ```

3. 函数类型字段

 若结构体包含函数类型字段,`DeepEqual()`会认为两个函数只有在**同为nil**或**指向同一函数地址**时才相等:

 ```go
 type FuncHolder struct {
     F func()
 }
 
 f1 := FuncHolder{F: func() {}}
 f2 := FuncHolder{F: func() {}}
 fmt.Println(reflect.DeepEqual(f1, f2))  // 输出: false
 ```

4. 空接口与类型一致性

 比较空接口(`interface{}`)时,`DeepEqual()`会检查动态类型和值是否一致:

 ```go
 var a interface{} = []int{1, 2}
 var b interface{} = []int{1, 2}
 fmt.Println(reflect.DeepEqual(a, b))  // 输出: true
 ```

2.5 嵌套结构体

Go语言中的嵌套结构体是一种通过组合实现复杂数据结构的常用方式,允许一个结构体包含另一个结构体作为其字段。这种设计遵循组合优于继承的原则,提供灵活的数据建模能力。

定义与初始化

// 嵌套结构体
package main

import "fmt"

// 创建结构体
type Author struct {
	name   string
	branch string
	year   int
}

// 创建嵌套结构体
//写法1:具体名嵌套
type HR struct {
	id int
	//字段结构
	details Author
}
//写法2:匿名嵌套
type HR2 struct{
    id int
    Author
}

func main() {

	// 初始化结构体字段
	result := HR{
		id:      10,
		details: Author{"Sona", "ECE", 2013},
	}

	//打印输出值
	fmt.Println("\n作者的详细信息")
	fmt.Println(result)
}
//结果:作者的详细信息
//	   {10 {Sona ECE 2013}}

具体名嵌套vs匿名嵌套

特性具名嵌套 (HR)匿名嵌套 (HR2)
字段定义details AuthorAuthor
访问内嵌字段hr.details.Namehr2.Name
字段冲突天然避免(需显式路径)可能发生(需类型名限定)
方法提升有(可直接调用)
设计意图组合(强调包含关系)模拟继承(行为复用)

2.6 匿名结构

在Go语言中,允许创建匿名结构。匿名结构是不包含名称的结构。当要创建一次性可用结构时,它很有用。

语法

// 定义并初始化匿名结构体
variable := struct {
    Field1 Type1
    Field2 Type2
    // ...
}{
    Field1: Value1,
    Field2: Value2,
}

// 示例
person := struct {
    Name string
    Age  int
}{
    Name: "Alice",
    Age:  30,
}

核心特点

  1. 无类型名称 没有通过 type 关键字声明,直接通过结构体字面量定义。

  2. 即定义即用 定义和初始化在同一个表达式中完成。

  3. 作用域受限 只能在当前作用域使用,无法在其他地方复用。

  4. 类型唯一性 即使两个匿名结构体字段完全相同,也被视为不同类型:

    a := struct{ X int }{1}
    b := struct{ X int }{2}
    // a = b // 编译错误:类型不匹配
    

与命名结构体的对比

特性匿名结构体命名结构体
类型名称有(通过 type 声明)
可复用性仅当前作用域整个包/项目
类型标识结构相同也被视为不同类型类型名称决定类型
适合场景一次性使用、局部数据封装重要数据结构、API边界定义
可序列化支持(同命名结构体)支持
方法定义❌ 不能定义方法✅ 可以定义方法

2.7 匿名字段

在Go结构中,允许创建匿名字段。匿名字段是那些不包含任何名称的字段,你只需要提到字段的类型,然后Go就会自动使用该类型作为字段的名称。

语法

type struct_name struct{
    int
    bool
    float64
}

重要事项:

  • 在结构中,不允许创建两个或多个相同类型的字段,如下所示:

    type student struct{
    	int
    	int
    }
    

    如果尝试这样做,则编译器将抛出错误。

  • 允许将匿名字段与命名字段组合,如下所示:

    type student struct{
     	name int
    	price int
    	string
    }
    
  • 示例

    package main 
      
    import "fmt"
      
    //创建一个结构匿名字段 
    type student struct { 
        int
        string 
        float64 
    } 
      
    // Main function 
    func main() { 
      
        // 将值分配给匿名,学生结构的字段
        value := student{123, "Bud", 8900.23} 
      
        fmt.Println("入学人数 : ", value.int) 
        fmt.Println("学生姓名 : ", value.string) 
        fmt.Println("套餐价格 : ", value.float64) 
        // 输出:入学人数 : 123 学生姓名 : Bud 套餐价格 : 8900.23
    }
    

2.8 结构体的比较

  1. 逐字段递归比较

    结构体比较时会递归比较每个字段:

    type Address struct {
        City, Zip string
    }
    
    type Employee struct {
        ID      int
        Address Address
    }
    
    e1 := Employee{1, Address{"Paris", "75000"}}
    e2 := Employee{1, Address{"Paris", "75000"}}
    e3 := Employee{1, Address{"Lyon", "69000"}}
    
    fmt.Println(e1 == e2) // true
    fmt.Println(e1 == e3) // false
    
  2. 指针比较

    指针比较的是内存地址,而不是指向的值:

    type Node struct {
        Value int
        Next  *Node
    }
    
    n1 := &Node{1, nil}
    n2 := &Node{1, nil}
    n3 := n1
    
    fmt.Println(n1 == n2) // false - 不同地址
    fmt.Println(n1 == n3) // true - 相同地址
    
  3. 空结构体的特殊性

    空结构体始终相等

    type Empty struct{}
    var e1, e2 Empty
    fmt.Println(e1 == e2) // true
    

2.9 函数作为字段

函数类型声明

  • 语法:

    // 声明函数类型
    type HandlerFunc func(int, string) error
    
    // 在结构体中使用函数类型
    type Processor struct {
        Name    string
        Process HandlerFunc  // 函数作为字段
    }
    
  • 举例

    //作为Go结构中的字段
    package main 
      
    import "fmt"
      
    // Finalsalary函数类型
    type Finalsalary func(int, int) int
      
    //创建结构
    type Author struct { 
        name      string 
        language  string 
        Marticles int
        Pay       int
      
        //函数作为字段
        salary Finalsalary 
    } 
      
    func main() { 
      
        // 初始化字段结构
        result := Author{ 
            name:      "Sonia", 
            language:  "Java", 
            Marticles: 120, 
            Pay:       500, 
            salary: func(Ma int, pay int) int { 
                return Ma * pay 
            }, 
        } 
      
        fmt.Println("作者姓名: ", result.name) 
        fmt.Println("语言: ", result.language) 
        fmt.Println("五月份发表的文章总数: ", result.Marticles) 
        fmt.Println("每篇报酬: ", result.Pay) 
        fmt.Println("总工资: ", result.salary(result.Marticles, result.Pay)) 
    }
    

直接定义匿名函数类型

  • 语法

    type Server struct {
        // 直接使用匿名函数类型
        OnConnect func(net.Conn) 
        OnError   func(error)
    }
    
  • 举例

    //使用匿名函数作为Go结构中的一个字段
    package main 
      
    import "fmt"
      
    //创建结构
    type Author struct { 
        name      string
        language  string
        Tarticles int
        Particles int
        Pending   func(int, int) int
    } 
      
    func main() { 
      
        //初始化结构字段
        result := Author{ 
            name:      "Sonia", 
            language:  "Java", 
            Tarticles: 340, 
            Particles: 259, 
            Pending: func(Ta int, Pa int) int { 
                return Ta - Pa 
            }, 
        } 
      
        fmt.Println("作者姓名: ", result.name) 
        fmt.Println("语言: ", result.language) 
        fmt.Println("文章总数: ", result.Tarticles) 
          
        fmt.Println("发表文章总数: ", result.Particles) 
        fmt.Println("待处理文章: ", result.Pending(result.Tarticles, result.Particles)) 
    }
    

三,方法

在Go语言中,方法(Method)是一种与特定类型(Type)关联的函数,允许为自定义类型(包括结构体和非结构体类型)添加行为。方法的本质是在函数签名前增加一个接收者(Receiver),定义了该函数作用于哪种类型上。

在接收者参数的帮助下,该方法可以访问接收者的属性

  • 在这里,接收方可以是结构类型或非结构类型。
  • 在代码中创建方法时,接收者和接收者类型必须出现在同一个包中。而且不允许创建一个方法,其中的接收者类型已经在另一个包中定义,包括像int、string等内建类型。

3.1 结构类型接收者

语法:

func(接收者 接收者类型) 方法名(参数列表)(返回值类型){
    // Code
}

示例

package main 
  
import "fmt"
  
//Author 结构体
type author struct { 
    name      string 
    branch    string 
    particles int
    salary    int
} 
  
//接收者的方法 
func (a author) show() { 
  
    fmt.Println("Author's Name: ", a.name) 
    fmt.Println("Branch Name: ", a.branch) 
    fmt.Println("Published articles: ", a.particles) 
    fmt.Println("Salary: ", a.salary) 
} 
  
func main() { 
  
    //初始化值
    //Author结构体
    res := author{ 
        name:      "Sona", 
        branch:    "CSE", 
        particles: 203, 
        salary:    34000, 
    } 
  
    //调用方法
    res.show() 
}

3.2 非结构类型接收者

在Go语言中,只要类型和方法定义存在于同一包中,就可以使用非结构类型接收器创建方法。如果它们存在于int,string等不同的包中,则编译器将抛出错误,因为它们是在不同的包中定义的。

举例

package main 
  
import "fmt"
  
//类型定义
type data int

//定义一个方法
//非结构类型的接收器 
func (d1 data) multiply(d2 data) data { 
    return d1 * d2 
} 
  
/* 
//如果您尝试运行此代码,

//然后编译器将抛出错误 
func(d1 int)multiply(d2 int)int{ 
return d1 * d2 
} 
*/
  
func main() { 
    value1 := data(23) 
    value2 := data(20) 
    res := value1.multiply(value2) 
    fmt.Println("最终结果: ", res) 
}

即如果要用非结构类型的接收者就需要用type 封装一下

3.3 接收者类型为指针

在Go语言中,允许使用指针接收器创建方法。在指针接收器的帮助下,如果方法中所做的更改将反映在调用方中。

语法:

func (p *Type) method_name(...Type) Type {
    // Code
}

举例

package main 
  
import "fmt"
  
// Author 结构体
type author struct { 
    name      string 
    branch    string 
    particles int
} 
  
//方法,使用author类型的接收者
func (a *author) show(abranch string) { 
    (*a).branch = abranch 
} 
  
// Main function 
func main() { 
  
    //初始化author结构体
    res := author{ 
        name:   "Sona", 
        branch: "CSE", 
    } 
  
    fmt.Println("Author's name: ", res.name) 
    fmt.Println("Branch Name(Before): ", res.branch) 
  
    //创建一个指针
    p := &res 
  
    //调用show方法
    p.show("ECE") 
    fmt.Println("Author's name: ", res.name) 
    fmt.Println("Branch Name(After): ", res.branch) 
}
// 输出
//	Author's name:  Sona
//	Branch Name(Before):  CSE
//	Author's name:  Sona
//	Branch Name(After):  ECE

3.4 接受者类型为值

举例

package main 
  
import "fmt"
  
// Author 结构体
type author struct { 
    name   string 
    branch string 
} 
  
//带有值的方法
//作者类型的接收者 
func (a author) show() { 
    a.branch = "Gourav"
} 
  

func main() { 
  
     //初始化值
     //作者结构体
    res := author{ 
        name:   "Sona", 
        branch: "CSE", 
    } 
  
    fmt.Println("Branch Name-before: ", res.branch) 
     //调用show方法
     //带有指针的(值方法)
    res.show() 
    fmt.Println("Branch Name-after: ", res.branch) 
}
//结果
// Branch Name-before: CSE
//	Branch Name-after:CSE

值接收者 vs 指针接收者

类型语法行为是否修改原值
值接收者(t Type)操作接收者的副本❌ 否
指针接收者(t *Type)操作接收者指向的内存(原始对象)✅ 是

3.5 方法继承和重写

通过结构体嵌入实现方法继承:

type Animal struct {
    name string
}

func (a Animal) Speak() {
    fmt.Println(a.name, "makes a sound")
}

type Dog struct {
    Animal // 嵌入Animal(继承其字段和方法)
}

// 重写Speak方法
func (d Dog) Speak() {
    fmt.Println(d.name, "barks!")
}

func main() {
    d := Dog{Animal{"Buddy"}}
    d.Speak() // 输出: Buddy barks!
}

3.6 同名方法

在Go语言中,允许在同一包中创建两个或多个具有相同名称的方法,但是这些方法的接收者必须具有不同的类型。该功能在Go函数中不可用。

package main 
  
import "fmt"
  
//创建结构体
type student struct { 
    name   string 
    branch string 
} 
  
type teacher struct { 
    language string 
    marks    int
} 
  
//名称相同的方法,但有不同类型的接收器
func (s student) show() { 
  
    fmt.Println("学生姓名:", s.name) 
    fmt.Println("Branch: ", s.branch) 
} 
  
func (t teacher) show() { 
  
    fmt.Println("Language:", t.language) 
    fmt.Println("Student Marks: ", t.marks) 
} 
  
func main() { 
  
    // 初始化结构体的值
    val1 := student{"Rohit", "EEE"} 
  
    val2 := teacher{"Java", 50} 
  
    //调用方法
    val1.show() 
    val2.show() 
}

即,方法可以同名,但是接收器不能相同

四,数组

4.1 介绍

在 Go 语言中,数组(Array) 是一种固定长度、存储相同类型元素的数据结构。其核心特性是长度在编译时确定且不可变

基本特性

  • 类型定义:数组类型包含元素类型和长度(如 [3]int[5]int 是不同类型)。
  • 内存连续:元素在内存中连续存储,访问高效。
  • 值类型:赋值或传参时复制整个数组(而非引用),修改副本不影响原数组。
  • 零值初始化:声明未初始化时,元素自动初始化为对应类型的零值(如 int0string"")。

4.2 声明与初始化

声明语法

var arr [3]int        // 声明长度为3的int数组,元素全为0
var strs [2]string    // 声明长度为2的string数组,元素全为""go

初始化方式

// 方式1:字面量初始化
a := [3]int{1, 2, 3}   // [1 2 3]

// 方式2:部分初始化(未指定元素为零值)
b := [5]int{1, 2}       // [1 2 0 0 0]

// 方式3:索引初始化
c := [4]int{0: 10, 3: 40}  // [10 0 0 40]

// 方式4:自动长度推导(用...)
d := [...]int{6, 7, 8}     // 长度自动设为3

4.3 操作数组

访问与修改

arr := [3]int{10, 20, 30}
fmt.Println(arr[0])   // 输出: 10
arr[1] = 50           // 修改后: [10 50 30]

遍历数组

// 方式1:for循环
for i := 0; i < len(arr); i++ {
    fmt.Println(arr[i])
}

// 方式2:range关键字
for index, value := range arr {
    fmt.Printf("索引:%d, 值:%d\n", index, value)
}

数组长度

length := len(arr)  // 获取数组长度(编译时常量)

4.4 多维数组

// 声明二维数组(2行3列)
var matrix [2][3]int

// 初始化
matrix = [2][3]int{
    {1, 2, 3},
    {4, 5, 6},
}

// 访问元素
fmt.Println(matrix[1][2])  // 输出: 6

4.5 注意事项

  1. 长度固定 数组长度是类型的一部分,无法动态扩展。

    var a [3]int
    a = [4]int{}  // 编译错误:类型不匹配
    
  2. 值复制行为 赋值或函数传参时复制整个数组:

    x := [2]int{1, 2}
    y := x        // 复制整个数组
    y[0] = 99     // 修改副本
    fmt.Println(x) // [1 2](原数组不变)
    
  3. 数组的比较

    在数组中,如果数组的元素类型是可比较的,则数组类型也是可比较的。因此,我们可以使用==运算符直接比较两个数组

    //如何比较两个数组
    package main
    
    import "fmt"
    
    func main() {
    
        arr1 := [3]int{9, 7, 6}
        arr2 := [...]int{9, 7, 6}
        arr3 := [3]int{9, 5, 3}
    
        //使用==运算符比较数组
        fmt.Println(arr1 == arr2)	//true
        fmt.Println(arr2 == arr3)	//false
        fmt.Println(arr1 == arr3)	//false
    
        //这将给出和错误,因为
        // arr1和arr4的类型不匹配
        /*
           arr4:= [4]int{9,7,6}
           fmt.Println(arr1==arr4)
        */
    }
    

4.6 数组复制

在 Go 语言中,数组是值类型,这意味着当数组被赋值给新变量、作为参数传递给函数或从函数返回时,都会创建整个数组的完整副本

值复制行为

original := [3]int{10, 20, 30}
copy := original // 创建完整副本

copy[0] = 100
fmt.Println("Original:", original) // [10 20 30]
fmt.Println("Copy:", copy)       // [100 20 30]

函数传参时的复制

func modifyArray(arr [3]int) {
    arr[1] = 999 // 修改副本
    fmt.Println("Inside function:", arr)
}

func main() {
    data := [3]int{1, 2, 3}
    modifyArray(data)//[1 999 3]
    fmt.Println("After function:", data) // [1 2 3] - 原数组不变
}

复制机制细节

  • 完全复制:整个数组内容被复制到新内存地址
  • 独立内存:副本与原数组互不影响
  • 深度复制:元素值被复制(包括结构体等值类型)
  • 编译时确定大小:复制大小在编译时已知

如果我们不需要数组复制,可以使用指针实现

  • 使用指针

    original := [3]int{10, 20, 30}
    copy := &original // 使用指针
    
    copy[0] = 100
    fmt.Println("Original:", original) // [100 20 30]	此时值可以修改
    fmt.Println("Copy:", copy)       // [100 20 30]
    
  • 函数传参时的指针

    func modifyViaPointer(arr *[3]int) {//传入指针
        // 修改原数组元素
        arr[0] = 100
        arr[2] *= 2
    }
    
    func main() {
        data := [3]int{1, 2, 3}
        modifyViaPointer(&data)//传入地址
        fmt.Println(data) // [100 2 6]
    }
    

五,切片Slice

5.1 介绍

在Go语言中,切片比数组更强大,灵活,方便,并且是轻量级的数据结构。slice是一个可变长度序列,用于存储相同类型的元素,不允许在同一slice中存储不同类型的元素。就像具有索引值和长度的数组一样,但是切片的大小可以调整,切片不像数组那样处于固定大小。在内部,切片和数组相互连接,切片是对基础数组的引用。允许在切片中存储重复元素。切片中的第一个索引位置始终为0,而最后一个索引位置将为(切片的长度– 1)

切片的核心概念

  • 底层数组: 每个切片本质上都是对一个底层数组的连续片段的引用。
  • 动态窗口: 你可以把切片想象成一个可移动、可伸缩的“窗口”,透过这个窗口你只能看到底层数组的一部分(或全部)。
  • 描述符: 切片本身并不存储数据,它只是存储了描述它所引用的底层数组片段所需的信息:
    • 指针(Pointer): 指向底层数组中切片开始位置的元素。
    • 长度(Length): 切片中当前包含的元素个数(len(s) 获取)。
    • 容量(Capacity): 从切片开始位置到底层数组结束位置的元素个数(cap(s) 获取)。容量代表了切片在不重新分配底层数组的情况下,最大可以增长到的长度。
  • 引用语义: 切片是引用类型。当你将一个切片赋值给另一个变量时,它们共享同一个底层数组。修改其中一个切片的元素会影响所有共享该底层数组的切片。

5.2 长度和容量

特性长度(len)容量(cap)
定义切片当前实际包含的元素数量切片底层数组最大可容纳的元素数量
访问方式len(slice)cap(slice)
可变性随元素增减变化(通过append等)创建时确定,只能通过重新分配改变
索引范围[0, len-1][0, cap-1](但超出len部分不可直接访问)
用途表示当前数据规模表示内存分配空间上限

举例

arr := [5]int{10, 20, 30, 40, 50}  // 底层数组
slice := arr[1:3]                  // 从索引1切到3(不包含3)

fmt.Println(slice)    // [20 30]
fmt.Println(len(slice)) // 长度 = 2 (20和30)
fmt.Println(cap(slice)) // 容量 = 4 (从索引1开始到数组末尾:20,30,40,50)

黄金法则:容量决定了切片能看到多少底层数组,而长度决定了你能安全使用多少元素。

  • 容量就是底层数组的长度,长度就是切片的长度

5.3 切片的创建方式

从数组或切片创建(切片表达式): 这是最常见的方式。

arr := [5]int{1, 2, 3, 4, 5}
s1 := arr[1:4] // s1 = [2, 3, 4], len=3, cap=4 (从arr[1]开始到底层数组末尾有4个元素)
s2 := arr[:3]  // s2 = [1, 2, 3], len=3, cap=5 (等同于 arr[0:3])
s3 := arr[2:]  // s3 = [3, 4, 5], len=3, cap=3 (等同于 arr[2:5])
s4 := s1[1:3] // s4 = [3, 4], len=2, cap=3 (基于s1切片再次切片)
  • [low:high]:创建一个包含从索引 lowhigh-1 的元素的切片。low 默认为0,high 默认为源数组/切片的长度。
  • [low:high:max]完整切片表达式。创建一个切片,长度为 high-low,容量为 max-low。这用于限制新切片的容量,防止它访问超出预期范围的底层数组元素。

使用字面量: 类似于数组字面量,但不指定长度。

s := []int{1, 2, 3, 4} // 创建一个底层数组为 [1,2,3,4] 的切片, len=4, cap=4

使用 make 函数: 明确指定类型、初始长度和容量(容量可选)。

s1 := make([]int, 5)      // len=5, cap=5, 所有元素初始化为0值
s2 := make([]int, 3, 10)   // len=3, cap=10, 所有元素初始化为0值
  • 当你知道切片最终需要的大致容量时,使用 make 并指定一个足够大的 cap 可以避免后续频繁扩容,提高性能。

  • 从零值 nil 切片的零值是 nil,长度为0,容量为0。nil 切片在功能上通常等同于长度为0的空切片([]int{}),但 nil 切片在序列化为JSON等场景下可能有区别。

5.4 切片的操作

5.4.1 基础操作

访问元素: 和数组一样,通过索引 s[i]0 <= i < len(s))。

修改元素: 直接赋值 s[i] = value。这会修改底层数组对应位置的值。

获取长度和容量: len(s), cap(s)

遍历:

// 使用 for range
for index, value := range s {
    fmt.Println(index, value)
}
// 仅索引
for index := range s {
    fmt.Println(index, s[index])
}
// 仅值 (使用 _ 忽略索引)
for _, value := range s {
    fmt.Println(value)
}
// 传统 for 循环
for i := 0; i < len(s); i++ {
    fmt.Println(i, s[i])
}

5.4.2 追加元素

追加元素 (append): 这是改变切片长度的主要方式。

s := []int{1, 2, 3}
s = append(s, 4)        // s = [1, 2, 3, 4], len=4, cap=?
s = append(s, 5, 6, 7)  // s = [1, 2, 3, 4, 5, 6, 7]
anotherSlice := []int{8, 9}
s = append(s, anotherSlice...) // ... 表示解包切片,相当于 append(s, 8, 9)
  • append 会检查切片是否有足够的容量(cap)容纳新元素:
    • 如果容量足够,直接在底层数组尾部添加元素,返回的切片指向同一个底层数组(长度增加)。
    • 如果容量不足,append 会创建一个新的、更大的底层数组(通常是原来容量的2倍,但Go版本和大小策略可能有细微调整),将原切片元素复制到新数组,再添加新元素。返回的切片指向这个新数组。
  • 必须接收 append 的返回值! 因为 append 可能返回一个指向新底层数组的新切片。append(s, elem) 不会直接修改 s,而是返回修改后的切片。常见的错误是 append(s, elem) 而不赋值回 s

5.4.3 切片复制

复制切片 (copy): 将源切片 (src) 的元素复制到目标切片 (dst)。

src := []int{1, 2, 3, 4}
dst := make([]int, 2) // len=2, cap=2
n := copy(dst, src)  // n = 2, dst = [1, 2], src 不变
// 也可以复制部分
copy(dst, src[1:3])  // dst = [2, 3]
  • 复制的元素个数是 len(dst)len(src) 中的较小值。
  • copy 会处理源和目标切片重叠的情况(正确复制)。
  • 使用 copy 可以显式地断开新切片与原底层数组的关联,避免意外的修改。

5.4.4 重新切片

切片(重新切片): 使用切片表达式在现有切片上创建新的切片视图(如前所述)。新切片共享原切片的底层数组(或其一部分)。

s := []int{0, 1, 2, 3, 4, 5}
newS := s[1:4] // newS = [1, 2, 3], len=3, cap=5 (底层数组从s[1]开始)
newS[0] = 99   // 修改 newS 会影响 s: s[1] 也变成了 99
newS2 := newS[:cap(newS)] // newS2 = [99, 2, 3, 4, 5], len=5, cap=5 (扩展长度到容量上限)

5.4.5 切片比较

  • null比较

    在切片中,只能使用**==运算符检查给定切片是否存在。如果尝试在==**运算符的帮助下比较两个切片,则会抛出错误,如下例所示:

    //判断切片是否为零
    package main 
      
    import "fmt"
      
    func main() { 
      
        //创建切片
        s1 := []int{12, 34, 56} 
        var s2 []int
      
            //如果你尝试运行这个注释
            //代码编译器将给出一个错误
        /*s3:= []int{23, 45, 66} 
          fmt.Println(s1==s3) 
        */
      
        //检查给定的片是否为nil
        fmt.Println(s1 == nil) 	//false
        fmt.Println(s2 == nil) 	//true
    }
    
  • bytes数组比较

    在Go切片中,可以使用Compare()函数将两个字节类型的切片彼此进行比较。此函数返回一个整数值,该整数值表示这些切片相等或不相等,并且这些值是:

    • 如果结果为0,则slice_1 == slice_2。
    • 如果结果为-1,则slice_1 <slice_2。
    • 如果结果为+1,则slice_1> slice_2。

    该函数在bytes包下定义,因此,必须在程序中导入bytes包才能访问Compare函数。

    //比较两个字节切片
    package main
    
    import (
        "bytes"
        "fmt"
    )
    
    func main() {
    
        //使用简写声明创建和初始化字节片
    
        slice_1 := []byte{'G', 'E', 'E', 'K', 'S'}
        slice_2 := []byte{'G', 'E', 'e', 'K', 'S'}
    
        //比较切片
    
        //使用Compare函数
        res := bytes.Compare(slice_1, slice_2)
    
        if res == 0 {
            fmt.Println("!..切片相等..!")
        } else {
            fmt.Println("!..切片不相等..!")
        }
        //输出:!..切片不相等..!
    }
    
  • DeepEqual比较

    通用但性能较差

    import "reflect"
    
    s1 := []int{1,2,3}
    s2 := []int{1,2,3}
    s3 := []int{1,2,4}
    
    fmt.Println(reflect.DeepEqual(s1, s2)) // true
    fmt.Println(reflect.DeepEqual(s1, s3)) // false
    
    // 注意:能处理多维切片
    m1 := [][]int{{1}, {2,3}}
    m2 := [][]int{{1}, {2,3}}
    fmt.Println(reflect.DeepEqual(m1, m2)) // true
    

5.4.5 多维切片

**多维切片:**多维切片与多维数组一样,只是切片不包含大小。

package main 
  
import "fmt"
  
func main() { 
  
    //创建多维切片
    s1 := [][]int{
        {12, 34}, 
        {56, 47}, 
        {29, 40}, 
        {46, 78}, 
    } 
  
    //访问多维切片
    fmt.Println("Slice 1 : ", s1) 
  
    //创建多维切片 
    s2 := [][]string{ 
        []string{"cainiaojcs", "for"}, 
        []string{"cainiaojcs", "GFG"}, 
        []string{"gfg", "cainiaojc"}, 
    } 
  
    //访问多维切片
    fmt.Println("Slice 2 : ", s2) 
  
}

操作多维切片

  1. 访问元素

    // 二维切片
    value := matrix[i][j]
    
    // 三维切片
    value := cube[i][j][k]
    
  2. 修改元素

    matrix[1][2] = 42
    
  3. 追加行

    newRow := []int{10, 11, 12}
    matrix = append(matrix, newRow)
    
  4. 追加列

    for i := range matrix {
        matrix[i] = append(matrix[i], 0) // 每行追加一个元素0
    }
    
  5. 遍历多维切片

    // 二维遍历
    for i, row := range matrix {
        for j, val := range row {
            fmt.Printf("matrix[%d][%d] = %d\n", i, j, val)
        }
    }
    
    // 三维遍历
    for i, plane := range cube {
        for j, row := range plane {
            for k, val := range row {
                fmt.Printf("cube[%d][%d][%d] = %d\n", i, j, k, val)
            }
        }
    }
    

5.4.6 切片排序

在 Go 语言中,切片排序主要通过标准库的 sort 包实现。切片本身是无序的数据结构,但 sort 包提供了多种排序方法,可满足不同场景的需求。

  1. 内置类型切片排序

    import "sort"
    
    // 整型切片排序
    ints := []int{4, 2, 7, 1}
    sort.Ints(ints) // [1, 2, 4, 7]
    
    // 浮点数切片排序
    floats := []float64{3.2, 1.5, 4.8}
    sort.Float64s(floats) // [1.5, 3.2, 4.8]
    
    // 字符串切片排序(字典序)
    strings := []string{"banana", "apple", "cherry"}
    sort.Strings(strings) // ["apple", "banana", "cherry"]
    
  2. 自定义排序接口

    type Person struct {
        Name string
        Age  int
    }
    
    // 定义人员切片类型
    type ByAge []Person
    
    // 实现 sort.Interface 接口
    func (a ByAge) Len() int           { return len(a) }
    func (a ByAge) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
    func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
    
    func main() {
        people := []Person{
            {"Bob", 31},
            {"John", 42},
            {"Alice", 25},
        }
        
        sort.Sort(ByAge(people))
        // 结果: [{Alice 25}, {Bob 31}, {John 42}]
    }
    
  3. 使用 sort.Slice(Go 1.8+)

    people := []Person{
        {"Bob", 31},
        {"John", 42},
        {"Alice", 25},
    }
    
    // 按年龄升序排序
    sort.Slice(people, func(i, j int) bool {
        return people[i].Age < people[j].Age
    })
    
    // 按名字长度降序排序
    sort.Slice(people, func(i, j int) bool {
        return len(people[i].Name) > len(people[j].Name)
    })
    
  4. 多条件排序

    // 先按年龄升序,年龄相同按名字降序
    sort.Slice(people, func(i, j int) bool {
        if people[i].Age != people[j].Age {
            return people[i].Age < people[j].Age
        }
        return people[i].Name > people[j].Name
    })
    

5.4.7 切片作为函数参数

核心行为:

  1. 切片头被复制:切片作为函数参数传递时,会复制切片的描述符(指针、长度、容量)
  2. 底层数组不复制:底层数组不会被复制,函数内外共享同一底层数组
  3. 修改元素影响原切片:函数内修改切片元素会影响原切片
  4. 长度/容量修改不影响原切片:函数内改变切片的长度或容量不会影响原切片

详细行为分析

  1. 修改切片元素(影响原切片)

    func modifyElement(s []int) {
        s[0] = 100 // 修改底层数组
    }
    
    func main() {
        nums := []int{1, 2, 3}
        modifyElement(nums)
        fmt.Println(nums) // [100, 2, 3] 原切片被修改!
    }
    
  2. 修改切片长度/容量(不影响原切片)

    func changeLen(s []int) {
        s = s[:0] // 修改副本的长度
    }
    
    func main() {
        nums := make([]int, 3, 5) // len=3, cap=5
        changeLen(nums)
        fmt.Println(len(nums)) // 仍然是3,未改变!
    }
    
  3. 追加元素(可能影响原切片)

    func appendElement(s []int) {
        // 在容量范围内追加
        s = append(s, 4) // 修改副本的长度
        s[0] = 100      // 修改底层数组
    }
    
    func main() {
        nums := make([]int, 3, 5) // len=3, cap=5
        appendElement(nums)
        fmt.Println(nums) // [100, 0, 0] 
        // 长度未变:len=3,但元素0被修改
        fmt.Println(len(nums)) // 3
    }
    
  4. 追加导致扩容(不影响原切片)

    func appendAndGrow(s []int) {
        s = append(s, 4, 5, 6) // 超出容量,创建新数组
        s[0] = 100            // 修改新数组
    }
    
    func main() {
        nums := []int{1, 2, 3} // len=3, cap=3
        appendAndGrow(nums)
        fmt.Println(nums) // [1, 2, 3] 原切片不变!go
    }
    

总结:切片作为参数传递时,函数获得的是切片头的副本,但共享底层数组。要修改切片头(长度/容量),必须返回新切片或使用指针。

5.4.8 切片分隔

  1. 基础语法

    newSlice := original[start:end:max]
    
    • start:起始索引(包含)
    • end:结束索引(不包含)
    • max:容量上限(容量 = max - start)
    • 结果:长度为 end - start
  2. 分隔示例

    nums := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    
    // 获取索引2-5的元素(不包含5)
    slice1 := nums[2:5] // [2, 3, 4]
    
    // 从开始到索引5
    slice2 := nums[:5] // [0, 1, 2, 3, 4]
    
    // 从索引5到结尾
    slice3 := nums[5:] // [5, 6, 7, 8, 9]
    
    // 完整切片
    slice4 := nums[:] // [0,1,2,3,4,5,6,7,8,9]
    

5.5 切片与数组的区别

特性数组 ([n]T)切片 ([]T)
长度固定,编译时确定可变,运行时动态增长(通过 append
声明显式指定长度(var a [5]int不指定长度(var s []ints := make([]int, len, cap)
类型长度是类型的一部分([5]int[6]int 是不同类型)长度不是类型的一部分([]int 就是一种类型)
赋值/传参值传递(复制整个数组)引用传递(传递切片描述符,共享底层数组)
底层存储直接存储数据存储指向底层数组的指针、长度、容量(描述符)
零值所有元素为该类型的零值nil (指针为 nil, len=0, cap=0)

5.6 切片使用的注意事项

  1. 理解共享底层数组: 这是切片最核心也最容易出问题的地方。多个切片共享同一个底层数组时,修改其中一个切片的元素会影响其他切片。如果不想共享,需要使用 copyappend(触发扩容时)创建新切片。
  2. append 的扩容与返回值: 务必记住 append 可能返回新切片,必须接收其返回值。如果容量不足,新切片指向新数组;容量足够则指向原数组。
  3. 容量与性能: 频繁的 append 操作导致扩容会带来内存分配和数据复制的开销。如果事先知道切片的大致容量,使用 make([]T, len, cap) 指定足够大的 cap 可以显著提高性能。
  4. nil 切片 vs 空切片:
    • var s []int // nil 切片 (s == nil)
    • s := []int{} // 空切片 (s != nil, len=0, cap=0)
    • 两者 lencap 都是0,都可以安全地进行 append。主要区别在于序列化(如JSON)时,nil 切片可能被序列化为 null,而空切片被序列化为 []
  5. 避免内存泄漏:
    • 大切片残留小切片: 如果你从一个很大的底层数组创建了一个小切片,即使这个小切片只引用了一小部分数据,整个底层数组在没有任何其他引用之前都不会被垃圾回收。如果你只需要一小部分数据,使用 copy 创建独立的新切片。
    • 切片中的指针: 如果切片元素是指针或包含指针的结构体,即使切片本身被缩减(如 s = s[:0]),底层数组中被“移除”的元素指向的对象只要还被指针引用着就不会被GC。在不再需要这些对象时,最好显式地将对应位置的元素设为 nil
  6. 切片表达式边界: 确保切片索引在 0 <= low <= high <= cap(originalSlice) 范围内(对于 [low:high]),否则会导致运行时 panic。完整切片表达式 [low:high:max] 要求 0 <= low <= high <= max <= cap(originalSlice)
  7. 并发安全: 切片本身不是并发安全的。如果多个goroutine并发读写同一个切片(尤其是并发调用 append 可能触发扩容),需要加锁(如 sync.Mutex)或使用通道(Channel)来同步访问。

六,字符串

6.1 介绍

Go 语言的字符串是不可变的字节序列,通常用于存储 UTF-8 编码的文本(但也可存储任意二进制数据)。其核心特性包括 不可变性UTF-8 原生支持基于字节的底层存储

内部表示原理

// 运行时表示(简化)
type stringStruct struct {
    str unsafe.Pointer // 指向底层字节数组的指针
    len int            // 字节长度(非字符数)
}
  • 底层存储:连续内存块存储字节([]byte),不一定是有效 UTF-8。
  • 不可变性:任何修改操作(如拼接、替换)都生成新字符串,原字符串不变。
  • UTF-8 编码:默认以 UTF-8 存储文本,一个字符可能占 1~4 字节。
  • 值类型:字符串是值类型,不是引用类型,赋值或传递时会进行值拷贝。

6.2 初始化

s1 := "Hello, 世界"       // 字面值(UTF-8)
s2 := `Raw\nString`      // 反引号(原始字符串,不会进行转义)
s3 := string([]byte{65, 66}) // "AB"(字节切片转换)
s4 := string([]rune{'世', '界'}) // "世界"(rune切片转换)
s5 := `多
行
字符
串`

**注意:**字符串可以为空,但不能为nil。

6.3 字符串注意要点

  1. **字符串是不可变的:**在Go语言中,一旦创建了字符串,则字符串是不可变的,无法更改字符串的值。换句话说,字符串是只读的。如果尝试更改,则编译器将引发错误。

    //字符串是不可变的
    package main 
      
    import "fmt"
      
    func main() { 
      
            //创建和初始化字符串
            //使用简写声明
        mystr := "Hello World"
      
        fmt.Println("String:", mystr) 
      
        /* 果你试图改变字符串的值,编译器将抛出一个错误,例如, 
         cannot assign to mystr[1] 
           mystr[1]= 'G' 
           fmt.Println("String:", mystr) 
        */
      
    }
    
  2. 遍历字符串

    可以使用for range循环遍历字符串。此循环可以在Unicode代码点上迭代一个字符串。

    //遍历字符串
    //使用for范围循环
    package main
    
    import "fmt"
    
    func main() {
    
        //字符串作为for循环中的范围
        for index, s := range "world" {
    
            fmt.Printf("%c 索引值是 %d\n", s, index)
        }
    }
    
  3. 访问字符串的单个字节:字符串是一个字节,因此,我们可以访问给定字符串的每个字节。

    //访问字符串的字节
    package main
    
    import "fmt"
    
    func main() {
    
        //创建和初始化一个字符串
        str := "Hello World"
    
        //访问给定字符串的字节
        for c := 0; c < len(str); c++ {
    
            fmt.Printf("\n字符 = %c 字节 = %v", str[c], str[c])
        }
    }
    
  4. 从切片创建字符串

    //从切片创建一个字符串 
    package main 
      
    import "fmt"
      
    func main() { 
      
        //创建和初始化一个字节片
        myslice1 := []byte{0x47, 0x65, 0x65, 0x6b, 0x73} 
      
        //从切片创建字符串
        mystring1 := string(myslice1) 
      
        //显示字符串
        fmt.Println("String 1: ", mystring1) 
      
        //创建和初始化一个符文切片 
        myslice2 := []rune{0x0047, 0x0065, 0x0065, 0x006b, 0x0073} 
      
        //从切片创建字符串
        mystring2 := string(myslice2) 
      
        //显示字符串
        fmt.Println("String 2: ", mystring2) 
    }
    String 1:  Geeks
    String 2:  Geeks
    
  5. 查找字符串的长度

    在Golang字符串中,可以使用两个函数(一个是len(),另一个是**RuneCountInString())**来找到字符串的长度。

    • UTF-8包提供了RuneCountInString()函数,该函数返回字符串中存在的字符总数
    • len()函数返回字符串的字节数
    //查找字符串的长度
    package main
    
    import (
        "fmt"
        "unicode/utf8"
    )
    
    func main() {
    
        //创建和初始化字符串
        //使用简写声明
        mystr := "Hello World"
    
        //查找字符串的长度
        //使用len()函数
        length1 := len(mystr)
    
        //使用RuneCountInString()函数
        length2 := utf8.RuneCountInString(mystr)
    
        //显示字符串的长度
        fmt.Println("string:", mystr)
        fmt.Println("Length 1:", length1)
        fmt.Println("Length 2:", length2)
    
    }
    

6.4 字符串操作

6.4.1 字符串比较

  1. 使用比较运算符

    s1 := "hello"
    s2 := "world"
    s3 := "hello"
    
    fmt.Println(s1 == s2)  // false
    fmt.Println(s1 == s3)  // true
    fmt.Println(s1 != s2)  // true
    

    特点

    • 区分大小写
    • 比较的是字符串的底层字节序列
    • 对于UTF-8编码的字符串也能正确工作
    • ==比较内容而非地址
  2. 使用><>=<=运算符

    这些运算符按字典序(lexicographical order)比较字符串:

    fmt.Println("apple" < "banana")  // true
    fmt.Println("apple" > "Apple")   // true (因为'a'的ASCII码97 > 'A'的65)
    
  3. 使用strings.Compare函数

    fmt.Println(strings.Compare("a", "b"))  // -1
    fmt.Println(strings.Compare("a", "a"))  // 0
    fmt.Println(strings.Compare("b", "a"))  // 1
    

    注意:官方文档指出,通常直接使用==<>等运算符更清晰,strings.Compare主要用于需要特定比较函数的情况。

  4. 不区分大小写比较

    fmt.Println(strings.EqualFold("Go", "go"))  // true
    

6.4.2 字符串拼接

在Go语言中,字符串是不可变的字节序列,这意味着每次拼接操作实际上都会创建新的字符串。

  1. 使用+运算符

    s1 := "Hello"
    s2 := "World"
    result := s1 + ", " + s2  // "Hello, World"
    

    特点

    • 语法简洁直观
    • 每次+操作都会创建新的字符串
    • 不适合大量或频繁拼接(性能较差)
  2. 使用+=运算符

    var s string
    s += "Hello"
    s += ", "
    s += "World"
    // s == "Hello, World"
    

    特点

    • +类似,每次操作都创建新字符串
    • 代码比多个+更清晰
    • 同样不适合大量拼接
  3. 使用strings.Builder

    var builder strings.Builder
    builder.WriteString("Hello")
    builder.WriteString(", ")
    builder.WriteString("World")
    result := builder.String()  // "Hello, World"
    

    优势

    • 内存预分配,减少内存分配次数
    • 提供WriteStringWrite(写入字节)方法
    • 线程不安全(但单goroutine使用非常高效)
  4. 使用bytes.Buffer

    var buffer bytes.Buffer
    buffer.WriteString("Hello")
    buffer.WriteString(", ")
    buffer.WriteString("World")
    result := buffer.String()  // "Hello, World"
    

    strings.Builder比较

    • 功能类似,但strings.Builder更轻量
    • bytes.Buffer有更多方法(如读取操作)
    • 两者都实现了io.Writer接口
  5. 使用fmt.Sprintf

    result := fmt.Sprintf("%s, %s", "Hello", "World")  // "Hello, World"
    

    适用场景

    • 需要复杂格式化时
    • 拼接少量字符串且代码可读性更重要时
    • 性能不如strings.Builder
  6. 切片拼接模式

parts := []string{"Hello", ", ", "World"}
result := strings.Join(parts, "")  // "Hello, World"

适用场景

  • 已有字符串切片需要拼接
  • 可以指定分隔符(如strings.Join(parts, ", ")

6.4.3 字符串修剪

字符串修剪是指去除字符串首尾不需要的字符(如空格、换行符或特定字符)

  1. strings.TrimSpace() - 去除首尾空白字符

    s := "  \t\n Hello, World \n\t\r\n"
    trimmed := strings.TrimSpace(s)
    // trimmed == "Hello, World"
    

    特点

    • 去除首尾所有空白字符(空格、制表符\t、换行符\n、回车符\r等)
    • 不会去除字符串中间的空白
    • 最常用的修剪函数
  2. strings.Trim() - 去除首尾指定字符

    s := "!!!Hello, World!!!"
    trimmed := strings.Trim(s, "!")
    // trimmed == "Hello, World"
    

    特点

    • 第二个参数是字符集,会去除首尾所有出现在该字符集中的字符

    • 可以指定多个字符:

      s := "abcHello, Worldcba"
      trimmed := strings.Trim(s, "abc")
      // trimmed == "Hello, World"
      
  3. strings.TrimLeft()strings.TrimRight()

    只修剪左侧或右侧:

    s := "xxxHello, Worldxxx"
    leftTrimmed := strings.TrimLeft(s, "x")  // "Hello, Worldxxx"
    rightTrimmed := strings.TrimRight(s, "x") // "xxxHello, World"
    
  4. strings.TrimPrefix()strings.TrimSuffix()

去除特定的前缀或后缀(必须是完整匹配):

s := "Greeting: Hello, World"
prefixTrimmed := strings.TrimPrefix(s, "Greeting: ")
// prefixTrimmed == "Hello, World"

s2 := "Hello, World.txt"
suffixTrimmed := strings.TrimSuffix(s2, ".txt")
// suffixTrimmed == "Hello, World"

特点

  • 完全匹配才会去除
  • 不匹配则返回原字符串
  • 性能优于TrimLeft/TrimRight(不需要检查每个字符)
  1. strings.TrimFunc()和变体

使用自定义函数决定修剪哪些字符

s := "123Hello, World123"
trimmed := strings.TrimFunc(s, func(r rune) bool {
    return unicode.IsNumber(r) // 去除数字
})
// trimmed == "Hello, World"

类似方法:

  • TrimLeftFunc()
  • TrimRightFunc()

6.4.4 字符串分割

  1. strings.Split() - 简单分割
s := "a,b,c"
parts := strings.Split(s, ",")
// parts == ["a", "b", "c"]

s2 := "a,,b,c"
parts2 := strings.Split(s2, ",")
// parts2 == ["a", "", "b", "c"]

特点

  • 使用指定分隔符分割字符串
  • 返回字符串切片
  • 连续分隔符会产生空字符串元素
  1. strings.SplitN() - 控制分割次数

    s := "a,b,c,d"
    parts := strings.SplitN(s, ",", 2)
    // parts == ["a", "b,c,d"]
    
    parts2 := strings.SplitN(s, ",", -1)
    // parts2 == ["a", "b", "c", "d"]
    

    特点

    • n > 0: 最多分割n-1次,得到最多n个子串
    • n == 0: 返回nil
    • n < 0: 无限制分割(等同于Split)
  2. strings.SplitAfter() - 保留分隔符

    s := "a,b,c"
    parts := strings.SplitAfter(s, ",")
    // parts == ["a,", "b,", "c"]
    

    特点

    • 分割后每个子串包含分隔符
    • 其他特性与Split相同
  3. strings.SplitAfterN() - 保留分隔符并控制次数

    s := "a,b,c,d"
    parts := strings.SplitAfterN(s, ",", 2)
    // parts == ["a,", "b,c,d"]
    
  4. strings.Fields() - 按空白分割

    s := "  foo bar  \t\n  baz  "
    parts := strings.Fields(s)
    // parts == ["foo", "bar", "baz"]
    

    特点

    • 用一个或多个连续空白字符作为分隔符
    • 自动去除首尾空白
    • 不会返回空字符串元素
  5. regexp.Split() - 使用正则分割

    re := regexp.MustCompile(`\s+`)  // 一个或多个空白字符
    s := "foo   bar \t baz"
    parts := re.Split(s, -1)
    // parts == ["foo", "bar", "baz"]
    

    特点

    • 最灵活的分割方式
    • 可以使用复杂的分隔模式
    • 性能比其他方法低

6.4.5 字符串包含

  1. strings.Contains() 函数

    功能:检查字符串s中是否包含子串substr

    s := "Hello, 世界"
    fmt.Println(strings.Contains(s, "Hello"))  // true
    fmt.Println(strings.Contains(s, "foo"))    // false
    fmt.Println(strings.Contains(s, ""))      // true (空字符串总是包含)
    

    特点

    • 区分大小写
    • 空字符串("")总是返回true
    • 支持Unicode字符
  2. strings.ContainsAny() 函数

    功能:检查字符串s中是否包含chars中的任意一个字符

    s := "Hello, 世界"
    fmt.Println(strings.ContainsAny(s, "abc"))  // false (不包含a/b/c)
    fmt.Println(strings.ContainsAny(s, "H我"))  // true (包含H)
    fmt.Println(strings.ContainsAny(s, ""))    // false
    

    特点

    • 只要包含chars中的任意一个字符就返回true
    • 空字符集("")返回false
    • 常用于检查非法字符
  3. strings.ContainsRune() 函数

    功能:检查字符串s中是否包含特定的Unicode字符r

    s := "Hello, 世界"
    fmt.Println(strings.ContainsRune(s, '世'))  // true
    fmt.Println(strings.ContainsRune(s, 0x4e16)) // true (世的Unicode码点)
    fmt.Println(strings.ContainsRune(s, 'z'))   // false
    

    特点

    • 直接检查特定rune是否存在
    • 适用于精确查找特定Unicode字符
  4. strings.HasPrefix()strings.HasSuffix()

    功能:判断前缀/后缀

    s := "Hello, 世界"
    fmt.Println(strings.HasPrefix(s, "Hello"))  // true
    fmt.Println(strings.HasPrefix(s, "hello"))  // false (区分大小写)
    fmt.Println(strings.HasSuffix(s, "世界"))   // true
    

    特点

    • 检查字符串是否以特定子串开头或结尾
    • 常用于文件扩展名检查、URL路径检查等场景
  5. 正则表达式判断

    s := "Hello, 世界"
    matched, _ := regexp.MatchString(`H.*界`, s)  // true
    matched, _ = regexp.MatchString(`\d+`, s)     // false (不包含数字)
    

    特点

    • 最灵活的匹配方式
    • 性能相对较低
    • 适合复杂模式匹配

6.5.6 字符串索引

在Go语言中,字符串索引操作需要特别注意,因为Go字符串是UTF-8编码的字节序列。

  1. 直接索引(按字节)

    s := "Hello, 世界"
    b := s[1]  // 获取第2个字节(索引从0开始)
    fmt.Printf("%c (%d)\n", b, b)  // 输出: e (101)
    
    s := "世界"
    fmt.Printf("%c\n", s[0])  // 输出乱码,因为"世"占3个字节
    

    特点

    • 返回指定位置的字节(uint8)
    • 不适用于多字节字符(会返回字符的一部分)
    • 索引超出范围会panic
  2. 转换为rune切片后索引

    s := "Hello, 世界"
    runes := []rune(s)
    r := runes[7]  // 第8个字符
    fmt.Printf("%c\n", r)  // 输出: 界
    

    特点

    • 正确处理多字节字符
    • 需要额外内存分配
    • 适用于需要随机访问字符的场景
  3. 遍历字符串获取字符

    使用range循环

    s := "Hello, 世界"
    for i, r := range s {
        fmt.Printf("%d: %c\n", i, r)
    }
    

    输出

    0: H
    1: e
    2: l
    3: l
    4: o
    5: ,
    6:  
    7: 世
    10: 界
    

    特点

    • i是字节位置,r是Unicode字符
    • 自动处理UTF-8编码
    • 最常用的字符遍历方式
  4. 查找字符位置

    s := "Hello, 世界"
    // 查找字符字节位置
    idx := strings.IndexRune(s, '世')  // 7
    // 查找字符索引位置
    runes := []rune(s)
    for i, r := range runes {
        if r == '世' {
            fmt.Println(i)  // 7
            break
        }
    }
    

6.5 字符串切片

6.5.1 基本切片语法

s := "Hello, 世界"
sub := s[start:end]  // 包含start,不包含end

示例

s := "Hello, 世界"
fmt.Println(s[0:5])   // "Hello"
fmt.Println(s[7:])    // "世界"
fmt.Println(s[:5])    // "Hello"

6.5.2 切片特性

底层行为

  1. 共享底层数组

    s := "Hello, 世界"
    sub := s[0:5]
    // sub和s共享底层字节数组,不会产生新分配
    
  2. 不可变性保证

    • 字符串不可变,切片操作安全
    • 无法通过切片修改原字符串

索引规则

  • 基于字节位置,而非字符位置
  • 索引从0开始
  • 左闭右开区间[start:end)
  • 省略start默认为0,省略end默认为len(s)

6.5.3 UTF-8编码注意事项

多字节字符问题

s := "世界"
fmt.Println(s[0:1])  // 无效UTF-8,输出乱码
fmt.Println(s[0:3])  // 正确输出"世"

关键点

  • 中文字符通常占3个字节
  • 不完整的切片会产生无效UTF-8
  • 运行时不会报错,但可能引发逻辑错误

6.5.4 字符串与字节切片转换

s := "Hello"
b := []byte(s)     // 字符串转字节切片
s2 := string(b)    // 字节切片转字符串
sub := s2[1:3]     // 切片操作

重要区别

  • 字符串切片返回字符串
  • 字节切片切片返回字节切片

6.6 rune切片

6.6.1 什么是rune

  1. 基本定义
    • rune是Go语言的内建类型,它是int32的别名(4字节)
    • 用于表示一个Unicode码点(Unicode code point)
    • 可以表示任何Unicode字符,包括中文、日文、emoji等
  2. 与byte的区别
    • byteuint8的别名(1字节),只能表示ASCII字符
    • rune可以表示完整的Unicode字符集(目前Unicode字符最多需要4字节)

6.6.2 rune操作

[]rune是一个rune类型的切片,用于存储和操作Unicode字符序列。它本质上是将字符串转换为Unicode码点的切片表示。

创建rune切片

// 从字符串转换
s := "你好,世界"
runes := []rune(s)  // 转换为rune切片

// 直接创建
myRunes := []rune{'H', 'e', 'l', 'l', 'o', '世', '界'}

6.6.3 rune切片的特性

  1. 长度反映字符数

    s := "你好,世界"
    fmt.Println(len([]rune(s)))  // 输出5(5个字符)
    fmt.Println(len(s))         // 输出15(UTF-8编码的字节数)
    
  2. 支持索引访问单个字符

    runes := []rune("你好")
    fmt.Printf("%c\n", runes[0])  // 输出'你'
    
  3. 可修改性

    runes := []rune("hello")
    runes[0] = 'H'  // 可以修改
    

6.6.4 常见使用场景

  1. 获取字符串的真实字符数

    func CharCount(s string) int {
        return len([]rune(s))
    }
    
  2. 正确处理字符串中的字符

    s := "你好,世界"
    
    // 错误方式(按字节遍历)
    for i := 0; i < len(s); i++ {
        fmt.Printf("%c ", s[i])  // 会输出乱码
    }
    
    // 正确方式1(使用range)
    for _, r := range s {
        fmt.Printf("%c ", r)  // 输出: 你 好 , 世 界 
    }
    
    // 正确方式2(转换为rune切片)
    runes := []rune(s)
    for i := 0; i < len(runes); i++ {
        fmt.Printf("%c ", runes[i])  // 输出: 你 好 , 世 界 
    }
    
  3. 修改字符串中的字符

    s := "hello"
    runes := []rune(s)
    runes[0] = 'H'
    modified := string(runes)  // "Hello"
    
  4. 字符串反转

    func ReverseString(s string) string {
        runes := []rune(s)
        for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
            runes[i], runes[j] = runes[j], runes[i]
        }
        return string(runes)
    }
    
  5. rune切片与字符串转换

    // 字符串 -> rune切片
    runes := []rune("你好")
    
    // rune切片 -> 字符串
    str := string(runes)
    
  6. 注意事项

    • rune切片是Go语言中处理Unicode字符的有力工具,它解决了UTF-8编码字符串按字节操作时的问题。虽然它比直接操作字符串消耗更多资源,但在需要精确字符处理的场景下是不可或缺的。
    • 对于大多数只需要遍历字符串的场景,使用for range循环就足够了,它会自动按rune处理字符,无需显式转换为rune切片。只有在需要随机访问或修改字符时,才需要转换为rune切片。
    • 内存占用
      • rune切片比原始字符串占用更多内存(每个字符固定4字节)
      • UTF-8字符串中ASCII字符只占1字节,转为rune后占4字节
    • 转换开销
      • 字符串和rune切片相互转换需要分配新内存和复制数据

6.7 字符串相关包

6.7.1 strings.Builder

  1. 基本使用

    var builder strings.Builder
    
    builder.WriteString("Hello")
    builder.WriteString(", ")
    builder.WriteString("World")
    
    result := builder.String()  // "Hello, World"
    
  2. 主要方法

    package main
    
    import (
    	"fmt"
    	"strings"
    )
    
    func main() {
    	// 1. 创建一个新的Builder
    	var builder strings.Builder
    
    	// 2. Write 方法 - 写入字节切片
    	bytes := []byte{'H', 'e', 'l', 'l', 'o', ' '}
    	n, err := builder.Write(bytes)
    	if err != nil {
    		fmt.Println("Write error:", err)
    		return
    	}
    	fmt.Printf("Written %d bytes: %q\n", n, builder.String())
    
    	// 3. WriteString 方法 - 直接写入字符串
    	n, err = builder.WriteString("World!")
    	if err != nil {
    		fmt.Println("WriteString error:", err)
    		return
    	}
    	fmt.Printf("Written %d bytes: %q\n", n, builder.String())
    
    	// 4. WriteByte 方法 - 写入单个字节
    	err = builder.WriteByte(' ')
    	if err != nil {
    		fmt.Println("WriteByte error:", err)
    		return
    	}
    	fmt.Println("After WriteByte:", builder.String())
    
    	// 5. WriteRune 方法 - 写入Unicode字符
    	n, err = builder.WriteRune('🚀')
    	if err != nil {
    		fmt.Println("WriteRune error:", err)
    		return
    	}
    	fmt.Printf("Written %d bytes for rune: %q\n", n, builder.String())
    
    	// 6. Len 方法 - 获取当前内容的字节长度
    	fmt.Println("Current length:", builder.Len())
    
    	// 7. Cap 方法 - 获取底层缓冲区的容量
    	fmt.Println("Current capacity:", builder.Cap())
    
    	// 8. Grow 方法 - 预分配空间
    	builder.Grow(100)
    	fmt.Println("Capacity after Grow(100):", builder.Cap())
    
    	// 9. Reset 方法 - 清空内容
    	builder.Reset()
    	fmt.Println("After reset - Length:", builder.Len(), "Content:", builder.String())
    
    	// 10. String 方法 - 获取构建的字符串
    	builder.WriteString("Final string")
    	result := builder.String()
    	fmt.Println("Final result:", result)
    
    	// 11. 不可复制性示例
    	// builder2 := builder // 这会导致编译错误,因为strings.Builder不能被复制
    	// 正确做法是使用指针或创建新的Builder
    }
    

    主要方法说明

    1. Write([]byte) - 写入字节切片
    2. WriteString(string) - 直接写入字符串(比转换为字节切片更高效)
    3. WriteByte(byte) - 写入单个字节
    4. WriteRune(rune) - 写入Unicode字符
    5. Len() - 获取当前内容的字节长度
    6. Cap() - 获取底层缓冲区的容量
    7. Grow(int) - 预分配空间以提高性能
    8. Reset() - 清空内容并重置Builder
    9. String() - 获取构建的字符串
  3. 性能差异

    • 普通拼接:O(n²)时间复杂度,每次操作都创建新字符串
    • Builder:O(n)时间复杂度,内部使用字节切片动态增长
  4. 其他用法

    预分配空间

    var builder strings.Builder
    builder.Grow(1024)  // 预分配1KB空间
    // 然后进行写入操作
    

    优点

    • 减少内存分配次数
    • 提升性能,特别是大字符串构建时

    重置重用

    builder.Reset()  // 清空内容,可重复使用
    builder.WriteString("New content")
    
  5. 线程安全性

    strings.Builder不是线程安全的,文档明确说明:

    It is not safe to call any of the builder's methods concurrently from multiple goroutines.

    解决方案

    1. 每个goroutine使用自己的Builder

    2. 使用互斥锁保护:

      var (
          builder strings.Builder
          mu      sync.Mutex
      )
      
      func safeWrite(s string) {
          mu.Lock()
          defer mu.Unlock()
          builder.WriteString(s)
      }
      
  6. 注意事项

    1. 不可随机访问:不能修改已写入内容
    2. 不支持撤回:写入后不能撤销
    3. 容量限制:与切片相同,受限于内存
    4. 不要保留生成的字符串的引用:可能影响GC

6.7.2 bytes.Buffer

bytes.Buffer 是Go标准库中一个非常实用的类型,它实现了读写可变字节缓冲区的功能。与 strings.Builder 类似但功能更全面,bytes.Buffer 既可以用于字符串构建,也可以用于二进制数据处理。

基本特性

  1. 可变字节缓冲区:动态增长的字节存储空间
  2. 同时支持读写:既可以写入数据也可以读取数据
  3. 零值可用:无需初始化即可使用(零值为空缓冲区)
  4. 非线程安全:并发访问需要外部同步

主要方法

  1. 创建Buffer
// 空Buffer
var buf1 bytes.Buffer

// 从字节切片创建
buf2 := bytes.NewBuffer([]byte("initial data"))

// 从字符串创建
buf3 := bytes.NewBufferString("initial string")
  1. 写入方法

    // Write - 写入字节切片
    buf.Write([]byte("hello "))
    
    // WriteString - 直接写入字符串
    buf.WriteString("world")
    
    // WriteByte - 写入单个字节
    buf.WriteByte('!')
    
    // WriteRune - 写入Unicode字符
    buf.WriteRune('🚀')
    
  2. 读取方法

    // Read - 读取到字节切片
    p := make([]byte, 5)
    n, err := buf.Read(p)  // 读取最多len(p)字节
    
    // ReadByte - 读取单个字节
    b, err := buf.ReadByte()
    
    // ReadRune - 读取单个Unicode字符
    r, size, err := buf.ReadRune()
    
    // ReadString - 读取直到分隔符
    str, err := buf.ReadString('\n')
    
    // ReadBytes - 读取直到分隔符(返回字节切片)
    data, err := buf.ReadBytes('\n')
    
  3. 其他实用方法

    // Len - 未读数据的字节数
    length := buf.Len()
    
    // Cap - 底层缓冲区的容量
    capacity := buf.Cap()
    
    // Bytes - 获取未读部分的字节切片
    data := buf.Bytes()
    
    // String - 获取未读部分的字符串
    str := buf.String()
    
    // Next - 读取接下来的n个字节(不移动读取位置)
    peek := buf.Next(5)
    
    // Reset - 清空缓冲区
    buf.Reset()
    
    // Grow - 预分配空间
    buf.Grow(1024)  // 预分配1KB空间
    
    // Truncate - 截断缓冲区
    buf.Truncate(10)  // 保留前10个字节
    

6.7.3 strcov

strconv 是 Go 标准库中用于字符串和基本数据类型之间转换的包,名称是 "string conversion" 的缩写。它提供了各种函数来实现字符串与布尔值、整数、浮点数之间的相互转换。

  1. 字符串与布尔值转换

    • 字符串转布尔值

      b, err := strconv.ParseBool("true")  // true, nil
      b, err := strconv.ParseBool("FALSE") // false, nil
      b, err := strconv.ParseBool("yes")   // false, error
      
      • 接受 "1", "t", "T", "true", "TRUE", "True" 返回 true

      • 接受 "0", "f", "F", "false", "FALSE", "False" 返回 false

      • 其他值返回错误

    • 布尔值转字符串

      s := strconv.FormatBool(true)  // "true"
      s := strconv.FormatBool(false) // "false"
      
  2. 字符串与整数转换

    • 字符串转整数

      func ParseInt(s string, base int, bitSize int) (i int64, err error)
      func ParseUint(s string, base int, bitSize int) (uint64, error)
      func Atoi(s string) (int, error)  // 等同于 ParseInt(s, 10, 0)
      
      i, err := strconv.ParseInt("42", 10, 32)  // 42, nil
      i, err := strconv.ParseInt("0xFF", 0, 64) // 255, nil (自动检测16进制)
      i, err := strconv.Atoi("-42")             // -42, nil
      

      参数:

      • base: 进制 (2到36),0表示自动检测(根据字符串前缀)
      • bitSize: 0表示int/uint,8/16/32/64表示对应位数
    • 整数转字符串

      s := strconv.FormatInt(255, 16)  // "ff"
      s := strconv.FormatUint(42, 10)  // "42"
      s := strconv.Itoa(-42)           // "-42"
      
  3. 字符串与浮点数转换

    • 字符串转浮点数

      func ParseFloat(s string, bitSize int) (float64, error)
      
      f, err := strconv.ParseFloat("3.14159", 64)  // 3.14159, nil
      f, err := strconv.ParseFloat("1.23e-4", 32) // 0.000123, nil
      

      参数:

      • bitSize: 32或64,指定精度
    • 浮点数转字符串

      func FormatFloat(f float64, fmt byte, prec, bitSize int) string
      
      s := strconv.FormatFloat(3.14159, 'f', 2, 64) // "3.14"
      s := strconv.FormatFloat(1234.5, 'e', 3, 64)  // "1.235e+03"
      s := strconv.FormatFloat(0.000123, 'g', 3, 64) // "1.23e-04"
      

      参数:

      • fmt: 格式 ('b', 'e', 'E', 'f', 'g', 'G')
        • 'f': 普通小数格式
        • 'e', 'E': 科学计数法
        • 'g', 'G': 根据情况选择最紧凑的表示
      • prec: 精度(小数位数或有效数字)
      • bitSize: 32或64,指定源值精度
  4. 引用与带引号的字符串转换

    • 字符串加引号

      q := strconv.Quote("Hello, 世界")  // `"Hello, 世界"`
      q := strconv.QuoteToASCII("Hello") // `"Hello"`
      
    • 解析带引号的字符串

      s, err := strconv.Unquote(`"Hello,\nWorld!"`) // "Hello,\nWorld!", nil
      
  5. 字符转换

    func QuoteRune(r rune) string
    func QuoteRuneToASCII(r rune) string
    func QuoteRuneToGraphic(r rune) string
    
    s := strconv.QuoteRune('世')  // `'世'`
    
  6. 其他实用函数

// 判断字符是否可打印
func IsPrint(r rune) bool

// 判断字符是否为图形字符
func IsGraphic(r rune) bool