一,函数
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),它引用了其函数体之外的变量。这个函数可以访问这些变量,并“记住”这些变量的状态,即使外部函数已经执行完毕。闭包的核心是:函数 + 它引用的外部变量环境。
- 用生活场景类比:闭包就像一个随身携带背包的函数,背包里装的是它需要用到的外部变量。
- 闭包的本质是函数能够访问并记住其词法作用域外的变量
闭包的特性
- 捕获外部变量:闭包函数可以访问定义它的作用域中的变量。
- 变量生命周期延长:被闭包引用的变量不会随外部函数执行结束而销毁,而是会一直存在,直到闭包不再被使用。
代码示例
-
最简单的闭包
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()
的注意事项
-
性能开销
DeepEqual()
依赖反射(reflect
包),在递归比较大型数据结构时可能产生显著性能损耗。在性能敏感场景需谨慎使用。 -
未导出字段(私有字段)
`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 Author | Author |
访问内嵌字段 | hr.details.Name | hr2.Name |
字段冲突 | 天然避免(需显式路径) | 可能发生(需类型名限定) |
方法提升 | 无 | 有(可直接调用) |
设计意图 | 组合(强调包含关系) | 模拟继承(行为复用) |
2.6 匿名结构
在Go语言中,允许创建匿名结构。匿名结构是不包含名称的结构。当要创建一次性可用结构时,它很有用。
语法
// 定义并初始化匿名结构体
variable := struct {
Field1 Type1
Field2 Type2
// ...
}{
Field1: Value1,
Field2: Value2,
}
// 示例
person := struct {
Name string
Age int
}{
Name: "Alice",
Age: 30,
}
核心特点
-
无类型名称 没有通过
type
关键字声明,直接通过结构体字面量定义。 -
即定义即用 定义和初始化在同一个表达式中完成。
-
作用域受限 只能在当前作用域使用,无法在其他地方复用。
-
类型唯一性 即使两个匿名结构体字段完全相同,也被视为不同类型:
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 结构体的比较
-
逐字段递归比较
结构体比较时会递归比较每个字段:
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
-
指针比较
指针比较的是内存地址,而不是指向的值:
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 - 相同地址
-
空结构体的特殊性
空结构体始终相等
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
是不同类型)。 - 内存连续:元素在内存中连续存储,访问高效。
- 值类型:赋值或传参时复制整个数组(而非引用),修改副本不影响原数组。
- 零值初始化:声明未初始化时,元素自动初始化为对应类型的零值(如
int
为0
,string
为""
)。
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 注意事项
-
长度固定 数组长度是类型的一部分,无法动态扩展。
var a [3]int a = [4]int{} // 编译错误:类型不匹配
-
值复制行为 赋值或函数传参时复制整个数组:
x := [2]int{1, 2} y := x // 复制整个数组 y[0] = 99 // 修改副本 fmt.Println(x) // [1 2](原数组不变)
-
数组的比较
在数组中,如果数组的元素类型是可比较的,则数组类型也是可比较的。因此,我们可以使用==运算符直接比较两个数组。
//如何比较两个数组 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]
:创建一个包含从索引low
到high-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)
}
操作多维切片
-
访问元素
// 二维切片 value := matrix[i][j] // 三维切片 value := cube[i][j][k]
-
修改元素
matrix[1][2] = 42
-
追加行
newRow := []int{10, 11, 12} matrix = append(matrix, newRow)
-
追加列
for i := range matrix { matrix[i] = append(matrix[i], 0) // 每行追加一个元素0 }
-
遍历多维切片
// 二维遍历 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
包提供了多种排序方法,可满足不同场景的需求。
-
内置类型切片排序
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"]
-
自定义排序接口
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}] }
-
使用 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) })
-
多条件排序
// 先按年龄升序,年龄相同按名字降序 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 切片作为函数参数
核心行为:
- 切片头被复制:切片作为函数参数传递时,会复制切片的描述符(指针、长度、容量)
- 底层数组不复制:底层数组不会被复制,函数内外共享同一底层数组
- 修改元素影响原切片:函数内修改切片元素会影响原切片
- 长度/容量修改不影响原切片:函数内改变切片的长度或容量不会影响原切片
详细行为分析
-
修改切片元素(影响原切片)
func modifyElement(s []int) { s[0] = 100 // 修改底层数组 } func main() { nums := []int{1, 2, 3} modifyElement(nums) fmt.Println(nums) // [100, 2, 3] 原切片被修改! }
-
修改切片长度/容量(不影响原切片)
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,未改变! }
-
追加元素(可能影响原切片)
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 }
-
追加导致扩容(不影响原切片)
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 切片分隔
-
基础语法
newSlice := original[start:end:max]
start
:起始索引(包含)end
:结束索引(不包含)max
:容量上限(容量 = max - start)- 结果:长度为
end - start
-
分隔示例
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 []int 或 s := make([]int, len, cap) ) |
类型 | 长度是类型的一部分([5]int 和 [6]int 是不同类型) | 长度不是类型的一部分([]int 就是一种类型) |
赋值/传参 | 值传递(复制整个数组) | 引用传递(传递切片描述符,共享底层数组) |
底层存储 | 直接存储数据 | 存储指向底层数组的指针、长度、容量(描述符) |
零值 | 所有元素为该类型的零值 | nil (指针为 nil , len=0 , cap=0 ) |
5.6 切片使用的注意事项
- 理解共享底层数组: 这是切片最核心也最容易出问题的地方。多个切片共享同一个底层数组时,修改其中一个切片的元素会影响其他切片。如果不想共享,需要使用
copy
或append
(触发扩容时)创建新切片。 append
的扩容与返回值: 务必记住append
可能返回新切片,必须接收其返回值。如果容量不足,新切片指向新数组;容量足够则指向原数组。- 容量与性能: 频繁的
append
操作导致扩容会带来内存分配和数据复制的开销。如果事先知道切片的大致容量,使用make([]T, len, cap)
指定足够大的cap
可以显著提高性能。 nil
切片 vs 空切片:var s []int
//nil
切片 (s == nil
)s := []int{}
// 空切片 (s != nil
,len=0
,cap=0
)- 两者
len
和cap
都是0,都可以安全地进行append
。主要区别在于序列化(如JSON)时,nil
切片可能被序列化为null
,而空切片被序列化为[]
。
- 避免内存泄漏:
- 大切片残留小切片: 如果你从一个很大的底层数组创建了一个小切片,即使这个小切片只引用了一小部分数据,整个底层数组在没有任何其他引用之前都不会被垃圾回收。如果你只需要一小部分数据,使用
copy
创建独立的新切片。 - 切片中的指针: 如果切片元素是指针或包含指针的结构体,即使切片本身被缩减(如
s = s[:0]
),底层数组中被“移除”的元素指向的对象只要还被指针引用着就不会被GC。在不再需要这些对象时,最好显式地将对应位置的元素设为nil
。
- 大切片残留小切片: 如果你从一个很大的底层数组创建了一个小切片,即使这个小切片只引用了一小部分数据,整个底层数组在没有任何其他引用之前都不会被垃圾回收。如果你只需要一小部分数据,使用
- 切片表达式边界: 确保切片索引在
0 <= low <= high <= cap(originalSlice)
范围内(对于[low:high]
),否则会导致运行时panic
。完整切片表达式[low:high:max]
要求0 <= low <= high <= max <= cap(originalSlice)
。 - 并发安全: 切片本身不是并发安全的。如果多个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 字符串注意要点
-
**字符串是不可变的:**在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) */ }
-
遍历字符串
可以使用for range循环遍历字符串。此循环可以在Unicode代码点上迭代一个字符串。
//遍历字符串 //使用for范围循环 package main import "fmt" func main() { //字符串作为for循环中的范围 for index, s := range "world" { fmt.Printf("%c 索引值是 %d\n", s, index) } }
-
访问字符串的单个字节:字符串是一个字节,因此,我们可以访问给定字符串的每个字节。
//访问字符串的字节 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]) } }
-
从切片创建字符串
//从切片创建一个字符串 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
-
查找字符串的长度
在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) }
- UTF-8包提供了
6.4 字符串操作
6.4.1 字符串比较
-
使用比较运算符
s1 := "hello" s2 := "world" s3 := "hello" fmt.Println(s1 == s2) // false fmt.Println(s1 == s3) // true fmt.Println(s1 != s2) // true
特点:
- 区分大小写
- 比较的是字符串的底层字节序列
- 对于UTF-8编码的字符串也能正确工作
==
比较内容而非地址
-
使用
>
、<
、>=
、<=
运算符这些运算符按字典序(lexicographical order)比较字符串:
fmt.Println("apple" < "banana") // true fmt.Println("apple" > "Apple") // true (因为'a'的ASCII码97 > 'A'的65)
-
使用
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
主要用于需要特定比较函数的情况。 -
不区分大小写比较
fmt.Println(strings.EqualFold("Go", "go")) // true
6.4.2 字符串拼接
在Go语言中,字符串是不可变的字节序列,这意味着每次拼接操作实际上都会创建新的字符串。
-
使用
+
运算符s1 := "Hello" s2 := "World" result := s1 + ", " + s2 // "Hello, World"
特点:
- 语法简洁直观
- 每次
+
操作都会创建新的字符串 - 不适合大量或频繁拼接(性能较差)
-
使用
+=
运算符var s string s += "Hello" s += ", " s += "World" // s == "Hello, World"
特点:
- 与
+
类似,每次操作都创建新字符串 - 代码比多个
+
更清晰 - 同样不适合大量拼接
- 与
-
使用
strings.Builder
var builder strings.Builder builder.WriteString("Hello") builder.WriteString(", ") builder.WriteString("World") result := builder.String() // "Hello, World"
优势:
- 内存预分配,减少内存分配次数
- 提供
WriteString
和Write
(写入字节)方法 - 线程不安全(但单goroutine使用非常高效)
-
使用
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
接口
- 功能类似,但
-
使用
fmt.Sprintf
result := fmt.Sprintf("%s, %s", "Hello", "World") // "Hello, World"
适用场景:
- 需要复杂格式化时
- 拼接少量字符串且代码可读性更重要时
- 性能不如
strings.Builder
-
切片拼接模式
parts := []string{"Hello", ", ", "World"}
result := strings.Join(parts, "") // "Hello, World"
适用场景:
- 已有字符串切片需要拼接
- 可以指定分隔符(如
strings.Join(parts, ", ")
)
6.4.3 字符串修剪
字符串修剪是指去除字符串首尾不需要的字符(如空格、换行符或特定字符)
-
strings.TrimSpace()
- 去除首尾空白字符s := " \t\n Hello, World \n\t\r\n" trimmed := strings.TrimSpace(s) // trimmed == "Hello, World"
特点:
- 去除首尾所有空白字符(空格、制表符\t、换行符\n、回车符\r等)
- 不会去除字符串中间的空白
- 最常用的修剪函数
-
strings.Trim()
- 去除首尾指定字符s := "!!!Hello, World!!!" trimmed := strings.Trim(s, "!") // trimmed == "Hello, World"
特点:
-
第二个参数是字符集,会去除首尾所有出现在该字符集中的字符
-
可以指定多个字符:
s := "abcHello, Worldcba" trimmed := strings.Trim(s, "abc") // trimmed == "Hello, World"
-
-
strings.TrimLeft()
和strings.TrimRight()
只修剪左侧或右侧:
s := "xxxHello, Worldxxx" leftTrimmed := strings.TrimLeft(s, "x") // "Hello, Worldxxx" rightTrimmed := strings.TrimRight(s, "x") // "xxxHello, World"
-
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
(不需要检查每个字符)
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 字符串分割
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"]
特点:
- 使用指定分隔符分割字符串
- 返回字符串切片
- 连续分隔符会产生空字符串元素
-
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
: 返回niln < 0
: 无限制分割(等同于Split)
-
strings.SplitAfter()
- 保留分隔符s := "a,b,c" parts := strings.SplitAfter(s, ",") // parts == ["a,", "b,", "c"]
特点:
- 分割后每个子串包含分隔符
- 其他特性与Split相同
-
strings.SplitAfterN()
- 保留分隔符并控制次数s := "a,b,c,d" parts := strings.SplitAfterN(s, ",", 2) // parts == ["a,", "b,c,d"]
-
strings.Fields()
- 按空白分割s := " foo bar \t\n baz " parts := strings.Fields(s) // parts == ["foo", "bar", "baz"]
特点:
- 用一个或多个连续空白字符作为分隔符
- 自动去除首尾空白
- 不会返回空字符串元素
-
regexp.Split()
- 使用正则分割re := regexp.MustCompile(`\s+`) // 一个或多个空白字符 s := "foo bar \t baz" parts := re.Split(s, -1) // parts == ["foo", "bar", "baz"]
特点:
- 最灵活的分割方式
- 可以使用复杂的分隔模式
- 性能比其他方法低
6.4.5 字符串包含
-
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字符
-
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
- 常用于检查非法字符
- 只要包含
-
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字符
-
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路径检查等场景
-
正则表达式判断
s := "Hello, 世界" matched, _ := regexp.MatchString(`H.*界`, s) // true matched, _ = regexp.MatchString(`\d+`, s) // false (不包含数字)
特点:
- 最灵活的匹配方式
- 性能相对较低
- 适合复杂模式匹配
6.5.6 字符串索引
在Go语言中,字符串索引操作需要特别注意,因为Go字符串是UTF-8编码的字节序列。
-
直接索引(按字节)
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
-
转换为rune切片后索引
s := "Hello, 世界" runes := []rune(s) r := runes[7] // 第8个字符 fmt.Printf("%c\n", r) // 输出: 界
特点:
- 正确处理多字节字符
- 需要额外内存分配
- 适用于需要随机访问字符的场景
-
遍历字符串获取字符
使用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编码
- 最常用的字符遍历方式
-
查找字符位置
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 切片特性
底层行为
-
共享底层数组:
s := "Hello, 世界" sub := s[0:5] // sub和s共享底层字节数组,不会产生新分配
-
不可变性保证:
- 字符串不可变,切片操作安全
- 无法通过切片修改原字符串
索引规则
- 基于字节位置,而非字符位置
- 索引从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
- 基本定义:
rune
是Go语言的内建类型,它是int32
的别名(4字节)- 用于表示一个Unicode码点(Unicode code point)
- 可以表示任何Unicode字符,包括中文、日文、emoji等
- 与byte的区别:
byte
是uint8
的别名(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切片的特性
-
长度反映字符数:
s := "你好,世界" fmt.Println(len([]rune(s))) // 输出5(5个字符) fmt.Println(len(s)) // 输出15(UTF-8编码的字节数)
-
支持索引访问单个字符:
runes := []rune("你好") fmt.Printf("%c\n", runes[0]) // 输出'你'
-
可修改性:
runes := []rune("hello") runes[0] = 'H' // 可以修改
6.6.4 常见使用场景
-
获取字符串的真实字符数
func CharCount(s string) int { return len([]rune(s)) }
-
正确处理字符串中的字符
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]) // 输出: 你 好 , 世 界 }
-
修改字符串中的字符
s := "hello" runes := []rune(s) runes[0] = 'H' modified := string(runes) // "Hello"
-
字符串反转
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) }
-
rune切片与字符串转换
// 字符串 -> rune切片 runes := []rune("你好") // rune切片 -> 字符串 str := string(runes)
-
注意事项
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
-
基本使用
var builder strings.Builder builder.WriteString("Hello") builder.WriteString(", ") builder.WriteString("World") result := builder.String() // "Hello, World"
-
主要方法
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 }
主要方法说明
Write([]byte)
- 写入字节切片WriteString(string)
- 直接写入字符串(比转换为字节切片更高效)WriteByte(byte)
- 写入单个字节WriteRune(rune)
- 写入Unicode字符Len()
- 获取当前内容的字节长度Cap()
- 获取底层缓冲区的容量Grow(int)
- 预分配空间以提高性能Reset()
- 清空内容并重置BuilderString()
- 获取构建的字符串
-
性能差异:
- 普通拼接:O(n²)时间复杂度,每次操作都创建新字符串
- Builder:O(n)时间复杂度,内部使用字节切片动态增长
-
其他用法
预分配空间
var builder strings.Builder builder.Grow(1024) // 预分配1KB空间 // 然后进行写入操作
优点:
- 减少内存分配次数
- 提升性能,特别是大字符串构建时
重置重用
builder.Reset() // 清空内容,可重复使用 builder.WriteString("New content")
-
线程安全性
strings.Builder
不是线程安全的,文档明确说明:It is not safe to call any of the builder's methods concurrently from multiple goroutines.
解决方案:
-
每个goroutine使用自己的Builder
-
使用互斥锁保护:
var ( builder strings.Builder mu sync.Mutex ) func safeWrite(s string) { mu.Lock() defer mu.Unlock() builder.WriteString(s) }
-
-
注意事项
- 不可随机访问:不能修改已写入内容
- 不支持撤回:写入后不能撤销
- 容量限制:与切片相同,受限于内存
- 不要保留生成的字符串的引用:可能影响GC
6.7.2 bytes.Buffer
bytes.Buffer
是Go标准库中一个非常实用的类型,它实现了读写可变字节缓冲区的功能。与 strings.Builder
类似但功能更全面,bytes.Buffer
既可以用于字符串构建,也可以用于二进制数据处理。
基本特性
- 可变字节缓冲区:动态增长的字节存储空间
- 同时支持读写:既可以写入数据也可以读取数据
- 零值可用:无需初始化即可使用(零值为空缓冲区)
- 非线程安全:并发访问需要外部同步
主要方法
- 创建Buffer
// 空Buffer
var buf1 bytes.Buffer
// 从字节切片创建
buf2 := bytes.NewBuffer([]byte("initial data"))
// 从字符串创建
buf3 := bytes.NewBufferString("initial string")
-
写入方法
// Write - 写入字节切片 buf.Write([]byte("hello ")) // WriteString - 直接写入字符串 buf.WriteString("world") // WriteByte - 写入单个字节 buf.WriteByte('!') // WriteRune - 写入Unicode字符 buf.WriteRune('🚀')
-
读取方法
// 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')
-
其他实用方法
// 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" 的缩写。它提供了各种函数来实现字符串与布尔值、整数、浮点数之间的相互转换。
-
字符串与布尔值转换
-
字符串转布尔值
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"
-
-
字符串与整数转换
-
字符串转整数
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"
-
-
字符串与浮点数转换
-
字符串转浮点数
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,指定源值精度
-
-
引用与带引号的字符串转换
-
字符串加引号
q := strconv.Quote("Hello, 世界") // `"Hello, 世界"` q := strconv.QuoteToASCII("Hello") // `"Hello"`
-
解析带引号的字符串
s, err := strconv.Unquote(`"Hello,\nWorld!"`) // "Hello,\nWorld!", nil
-
-
字符转换
func QuoteRune(r rune) string func QuoteRuneToASCII(r rune) string func QuoteRuneToGraphic(r rune) string s := strconv.QuoteRune('世') // `'世'`
-
其他实用函数
// 判断字符是否可打印
func IsPrint(r rune) bool
// 判断字符是否为图形字符
func IsGraphic(r rune) bool