Go语言基础笔记| 青训营

144 阅读21分钟

Go语言基础1

简介

  • Go语言特点

    1. 高性能、高并发

    2. 语法简单、学习曲线平缓

       //创建一个静态文件服务器
       package main
       ​
       import (
           "net/http"
       )
       ​
       func main(){
           http.Handle("/",http.FileServer(http.Dir(".")))//注册了一个处理器,将根 URL "/" 映射到文件服务器。http.FileServer() 函数创建一个文件服务器处理器,它会将请求映射到当前目录(.)中的文件。换句话说,它会将当前目录下的文件暴露给客户端,允许客户端通过 HTTP 访问这些文件。
           http,ListenAndServe(":8080",nil)//启动了 HTTP 服务器并监听来自 8080 端口的请求。http.ListenAndServe() 函数在指定的端口上启动一个 HTTP 服务器,并且它会一直运行,直到遇到错误或显式地被关闭。
       }
      
    3. 丰富的标准库

    4. 完善的工具链

    5. 静态链接

    6. 快速编译

    7. 跨平台

    8. 垃圾回收

入门

基础语法
  • Hello World

     package main
     ​
     import "fmt"
     ​
     func main() {
         fmt.Println("hello,world")
     }
    
    • package main: 这是 Go 语言中的包声明。每个 Go 程序都必须有一个 main 包。main 包是一个特殊的包,它标识了这是一个可执行程序而不是一个库。
    • import "fmt": 这是导入所需的包。在这里,我们导入了 fmt 包,它是 Go 标准库中的格式化(I/O)包。它提供了在终端输出内容和格式化输出的功能。
    • func main() { ... }: 这是程序的入口点。Go 语言中的每个可执行程序都必须包含一个 main() 函数,它会在程序启动时自动调用。
    • fmt.Println("hello, world"): 这行代码使用 fmt 包中的 Println() 函数将字符串 "hello, world" 输出到控制台(终端)。
  • 变量与常量

    • Go 语言中的基本变量类型

      类型描述取值范围
      int有符号整数-2147483648 到 2147483647(32位系统) -9223372036854775808 到 9223372036854775807(64位系统)
      int88位有符号整数-128 到 127
      int1616位有符号整数-32768 到 32767
      int3232位有符号整数-2147483648 到 2147483647
      int6464位有符号整数-9223372036854775808 到 9223372036854775807
      uint无符号整数0 到 4294967295(32位系统) 0 到 18446744073709551615(64位系统)
      uint88位无符号整数0 到 255
      uint1616位无符号整数0 到 65535
      uint3232位无符号整数0 到 4294967295
      uint6464位无符号整数0 到 18446744073709551615
      float3232位浮点数约 -3.4e38 到 3.4e38
      float6464位浮点数约 -1.7e308 到 1.7e308
      complex64由两个 32 位浮点数组成的复数实部和虚部都是 32 位浮点数
      complex128由两个 64 位浮点数组成的复数实部和虚部都是 64 位浮点数
      bool布尔类型true 或 false
      string字符串类型一个 UTF-8 字符序列
      byte字节类型,uint8 的别名0 到 255
      rune符文类型,int32 的别名一个 Unicode 码点
    • 变量声明方式

      在 Go 语言中,变量的声明方式有两种:短变量声明和常规变量声明。

      • 短变量声明:短变量声明使用 := 运算符,可以在函数内部创建新的变量并对其赋值。这种声明方式只能在函数内部使用,不能用于全局变量。

         func main() {
             name := "pidan"
             age := 23
         ​
             fmt.Println(name, age)
         }
        
      • 常规变量声明:常规变量声明使用 var 关键字,可以在全局范围或函数内部声明变量。如果在全局范围声明变量,则该变量可以在整个包内使用。

         // 全局变量声明
         var globalName string = "pidan"
         ​
         func main() {
             // 函数内部变量声明
             var localAge int = 23
         ​
             fmt.Println(globalName, localAge)
         }   
        

        对于全局变量声明,也可以使用简短形式,但需要注意全局变量的简短形式必须使用 var 关键字,并且不能使用 := 运算符。

         // 全局变量声明的简短形式
         var globalName = "pidan"
         ​
         func main() {
             // 函数内部变量声明的简短形式
             localAge := 23
         ​
             fmt.Println(globalName, localAge)
         }
        

        无论是短变量声明还是常规变量声明,Go 语言都会根据变量值的类型自动推断变量的类型。如果变量在声明时已经赋值,那么 Go 编译器将根据右侧表达式的类型来确定变量的类型。例如,age := 23 会将 age 推断为 int 类型。如果变量声明时没有赋值,那么需要显式指定变量类型,例如 var name string

    • 常量声明

      在 Go 语言中,可以使用 const 关键字来声明常量。常量是在程序运行时不可更改的值,其值在编译时就已经确定。以下是一些常量声明的示例:

       const pi = 3.14
       const age = 23
       const name = "pidan"
      

      在 Go 语言中,常量的作用范围是包级别的,也就是说,常量在整个包内都可以访问。如果希望常量只在特定的代码块内可见,可以使用块级常量(在代码块内使用 const 关键字声明)。

  • if-else

    在 Go 语言中,if-else 是一种条件控制语句,用于在程序中根据条件的真假来执行不同的代码块。if 后面的条件表达式会被求值,如果条件为真,则执行 if 代码块中的语句;如果条件为假,则执行 else 代码块中的语句(如果有的话)。

    if-else 的一般语法结构如下:

     if condition {
         // 条件为真时执行的代码块
     } else {
         // 条件为假时执行的代码块
     }
    

    其中,condition 是一个布尔表达式,可以是一个返回布尔值的条件判断。

     func main() {
         score := 100
     ​
         if score >= 120 {
             fmt.Println("优秀")
         } else if score >= 90 {
             fmt.Println("良好")
         } else if score >= 75 {
             fmt.Println("及格")
         } else {
             fmt.Println("不及格")
         }
     }
    

    在这个例子中,根据 score 变量的值,判断学生成绩的等级。根据条件的不同,输出不同的等级。

    需要注意的是,ifelse 后面的代码块必须用花括号 {} 括起来,即使代码块只有一行语句也不能省略。另外,在条件判断时,条件表达式的结果必须是布尔值,不能是其他类型。在 Go 中,没有隐式的类型转换。

  • 循环

    for 循环是 Go 语言中最常用的循环语句,用于重复执行一段代码块,直到指定的条件不再满足。for 循环有两种形式:基本的 for 循环和无限循环。

    • 基本的 for 循环
     for 初始化语句; 条件表达式; 循环后操作 {
         // 循环体
     }
    
     func main() {
         for i := 1; i <= 5; i++ {
             fmt.Println(i)
         }
     }
    

    在这个例子中,for 循环会从 1 开始,每次递增 1,打印出 1 到 5。

    • 无限循环
     for {
         // 循环体
     }
    
     func main() {
         for {
             fmt.Println("无限")
         }
     }
    

    在这个例子中,使用无限循环的形式,for 循环将一直执行循环体中的代码,形成无限循环。要注意的是,这种无限循环必须通过 break 或其他条件来终止,否则程序将一直执行下去。

    在循环中,可以使用 break 关键字来提前结束循环,也可以使用 continue 关键字来跳过本次循环,进入下一次循环。循环体内的代码块必须用花括号 {} 括起来,即使代码块只有一行语句也不能省略。

  • switch

    在 Go 语言中,switch 是一种条件控制语句,用于根据表达式的不同值执行不同的代码块。switch 语句可以取代一系列连续的 if-else 语句,使代码更加简洁易读。switch 语句可以用于处理多个选项,以及默认情况下的处理。

    switch 的一般语法结构如下:

     switch expression {
     case value1:
         // 当 expression 的值等于 value1 时执行这里的代码
     case value2:
         // 当 expression 的值等于 value2 时执行这里的代码
     // 更多 case ...
     default:
         // 如果 expression 的值不匹配任何 case 时执行这里的代码
     }
    

    其中,expression 是一个表达式,它的值将与每个 case 的值进行比较。如果 expression 的值与某个 case 的值相匹配,就会执行该 case 下的代码块。如果没有匹配到任何 case,就会执行 default 下的代码块(可选的)。

     func main() {
         num := 2
     ​
         switch num {
         case 1:
             fmt.Println("1")
         case 2:
             fmt.Println("2")
         case 3:
             fmt.Println("3")
         default:
             fmt.Println("4")
         }
     }
     ​
    

    需要注意的是,在 Go 语言的 switch 语句中,每个 case 的值必须是唯一的,不能有重复。而且,每个 case 下的代码块会自动终止,不需要显式地使用 break 关键字来跳出 switch 块。如果希望继续执行下一个 case,可以使用 fallthrough 关键字。

     func main() {
         num := 2
     ​
         switch num {
         case 1:
             fmt.Println("1")
             fallthrough
         case 2:
             fmt.Println("2")
         case 3:
             fmt.Println("3")
         }
     }
    

    在这个例子中,由于 num 的值是 2,它会匹配 case 2,输出 "数字是 2",同时由于有 fallthrough 关键字,会继续执行下一个 case,输出 "数字是 3"。

  • 数组

    在 Go 语言中,数组是一种固定长度、类型相同的数据结构,它可以用来存储一组相同类型的元素。数组的长度在创建时就确定,并且不能动态地改变大小。Go 语言中的数组声明的语法如下:

     var name [len]Type
    

    其中,name 是数组的名称,len 是数组的长度(即可以存储的元素个数),Type 是数组中元素的类型。

     var numbers [5]int
    

    上面的代码声明了一个包含 5 个整数的数组,数组名为 numbers,元素类型为 int

    数组的索引从 0 开始,到 length-1 结束。可以使用索引来访问数组中的元素。

     func main() {
         var numbers [5]int
         numbers[0] = 10
         numbers[1] = 20
         numbers[2] = 30
         numbers[3] = 40
         numbers[4] = 50
     ​
         fmt.Println(numbers[0]) // 输出 10
         fmt.Println(numbers[2]) // 输出 30
     }
    

    在这个例子中,声明了一个长度为 5 的整数数组 numbers,并对数组中的元素进行赋值。通过索引访问数组元素,例如 numbers[0] 将输出 10。

    如果在声明数组时不指定具体的元素值,Go 语言会使用相应类型的零值来初始化数组。

     func main() {
         var numbers [5]int
         fmt.Println(numbers) // 输出 [0 0 0 0 0]
     }
    

    除了上面的示例中使用的数组初始化方式,还可以使用值来声明和初始化数组。

     func main() {
         // 使用值初始化数组
         numbers := [5]int{10, 20, 30, 40, 50}
         fmt.Println(numbers) // 输出 [10 20 30 40 50]
     }
    

    数组的长度是数组类型的一部分,因此不同长度的数组是不同的类型,所以不能直接将长度为 5 的数组赋值给长度为 3 的数组。如果你需要动态大小的数组,可以使用切片(slice)类型。切片是 Go 语言中更加灵活和常用的数据结构,它相比于数组更具扩展性。

  • 切片

    在 Go 语言中,切片(Slice)是一种动态数组,它提供了更灵活的数组操作和动态大小的功能。切片允许你对数组进行部分或整体操作,而无需重新创建一个新的数组。切片是对底层数组的引用,因此对切片的修改会影响到底层数组的内容。

    切片的声明语法如下:

     var name []Type
    

    其中,name 是切片的名称,Type 是切片中元素的类型。不同于数组,切片的长度不固定,它会根据需要动态增长或缩小。

    切片的创建有几种方式:

    1. 使用 make 函数创建切片
     slice := make([]Type, len, cap)
    
    • Type 是切片中元素的类型。
    • len 是切片的初始长度,表示当前切片中的元素个数。
    • cap 是可选参数,表示底层数组的容量,即底层数组最多能容纳的元素个数。如果不指定 cap,则默认与 length 相同。
     func main() {
         // 创建一个长度为 3,容量为 5 的整数切片
         slice := make([]int, 3, 5)
         fmt.Println(slice)         // 输出 [0 0 0]
         fmt.Println(len(slice))    // 输出 3
         fmt.Println(cap(slice))    // 输出 5
     }
    
    1. 使用值初始化切片
     slice := []Type{value1, value2, ...}
    
     func main() {
         // 使用值初始化切片
         slice := []int{10, 20, 30, 40, 50}
         fmt.Println(slice)         // 输出 [10 20 30 40 50]
         fmt.Println(len(slice))    // 输出 5
         fmt.Println(cap(slice))    // 输出 5
     }
    

    切片也可以通过索引来访问和修改元素,类似于数组。它还提供了一些有用的内置函数,如 append() 用于在切片末尾追加元素,copy() 用于复制切片内容等。切片是动态大小的,它会根据需要自动扩容。当切片长度超过容量时,Go 语言会自动分配更大的底层数组,并将原有的元素复制到新的底层数组中。因此,切片在底层是对数组的引用,而不会像数组一样存在大小固定的限制。总之,切片是 Go 语言中非常重要且常用的数据结构,用于处理动态大小的数据集合。

  • map

    在 Go 语言中,map 是一种无序的键值对(key-value)集合,也被称为字典。map 提供了一种快速查找的数据结构,可以根据键(key)来获取对应的值(value)。map 中的键必须是唯一的,并且是支持相等运算符(==)的类型(基本类型、字符串、指针、数组、结构体等),而值可以是任意类型。

    map 的声明语法如下:

     var name map[keyType]valueType
    

    其中,namemap 的名称,keyType 是键的类型,valueType 是值的类型。

     func main() {
         // 声明一个键为 string 类型,值为 int 类型的 map
         scores := make(map[string]int)
     ​
         // 添加键值对
         scores["Alice"] = 90
         scores["Bob"] = 85
         scores["Eve"] = 95
     ​
         // 获取值
         fmt.Println("Alice's score:", scores["Alice"]) // 输出 "Alice's score: 90"
     ​
         // 删除键值对
         delete(scores, "Bob")
     ​
         // 遍历 map
         for name, score := range scores {
             fmt.Println(name, "scored", score)
         }
     }
    

    在上面的示例中,创建了一个 scoresmap,键是字符串类型,值是整数类型。使用 scores["Alice"] 的方式给键值对中添加了元素,使用 delete() 函数删除了一个键值对。最后,使用 for range 循环遍历了 map 中的所有键值对。

    需要注意的是,如果对于一个不存在的键访问其值,map 会返回值类型的零值。如果想检查键是否存在,可以使用多返回值的方式,通过判断第二个返回值(布尔值)来确定键是否存在。

     func main() {
         scores := make(map[string]int)
         scores["Alice"] = 90
         scores["Eve"] = 95
     ​
         // 检查键是否存在
         score, exists := scores["Bob"]
         if exists {
             fmt.Println("Bob's score:", score)
         } else {
             fmt.Println("Bob's score not found")
         }
     }
    

    在这个示例中,Bob 这个键不存在于 scoresmap 中,所以 exists 将为 false,并输出 "Bob's score not found"。

    map 是一种引用类型,当将一个 map 赋值给另一个变量时,它们指向同一个底层数据结构,因此对其中一个变量的修改会影响到另一个变量。

  • 函数

    1. 函数定义: 在Go语言中,使用func关键字定义函数。函数定义的一般形式如下:

       func 函数名(参数列表) 返回值列表 {
           // 函数体
       }
      
      • 函数名:标识函数的名称,要遵循标识符规则。
      • 参数列表:用于接收调用函数时传递的参数。可以有零个或多个参数,每个参数由参数名和参数类型组成,多个参数之间使用逗号分隔。
      • 返回值列表:用于指定函数返回的结果。可以有零个或多个返回值,每个返回值由返回值类型组成,多个返回值之间使用逗号分隔。
    2. 函数调用: 调用函数时,通过函数名和实参列表来调用函数。如果函数有返回值,可以将其赋值给变量。

       result := 函数名(参数1, 参数2, ...)
      
    3. 无参数函数: 如果函数不需要接收任何参数,函数名后面的括号保持空白。

       func sayHello() {
           fmt.Println("Hello!")
       }
      
    4. 无返回值函数: 如果函数没有返回值,返回值列表可以省略。

       func printSum(a, b int) {
           sum := a + b
           fmt.Println(sum)
       }
      
    5. 多返回值函数: 在Go语言中,函数可以返回多个值。

       func divide(a, b float64) (float64, error) {
           if b == 0 {
               return 0, errors.New("division by zero")
           }
           return a / b, nil
       }
      
    6. 命名返回值: 函数定义时可以为返回值命名,使得在函数体内可以直接使用这些变量,并且可以通过return语句省略明确指定返回值。

       func divide(a, b float64) (result float64, err error) {
           if b == 0 {
               err = errors.New("division by zero")
               return 
           }
           result = a / b
           return 
       }
      
    7. 可变参数: 使用...语法可以创建可变参数的函数,允许函数接受任意数量的参数。

       func sum(nums ...int) int {
           total := 0
           for _, num := range nums {
               total += num
           }
           return total
       }
      
    8. 匿名函数: 在Go语言中,可以创建匿名函数并将其赋值给变量。这些函数可以像其他变量一样传递、调用和返回。

       add := func(a, b int) int {
           return a + b
       }
      
    9. 闭包: 一个函数和与其相关的引用环境组合而成的实体,闭包=函数+引用环境。 在Go语言中,函数可以形成闭包。闭包是指捕获其所在函数内部变量的函数。闭包允许函数访问和操作在其外部作用域声明的变量。

       func counter() func() int {
           count := 0
           return func() int {
               count++
               return count
           }
       }
      
    • 指针

      指针是一种特殊的变量类型,它存储了一个变量的内存地址。使用指针可以在程序中间直接操作变量所在的内存地址,从而避免数据的拷贝,提高程序的执行效率。在Go语言中,通过使用&操作符可以获取变量的内存地址,通过*操作符可以解引用指针,即获取指针指向的变量的值。

      1. 创建指针
       var num int = 42
       var ptr *int  // 声明一个整数类型的指针变量
       ptr = &num   // 将num的内存地址赋值给指针ptr
      
      1. 解引用指针
       var value int = *ptr  // 从指针ptr获取它指向的变量的值,并赋值给value
      
      1. 使用指针修改变量的值
       *ptr = 100  // 通过指针ptr修改它指向的变量的值,此时num的值也会被修改为100
      
      1. 空指针

      在Go语言中,指针的零值是nil,表示指针不指向任何有效的内存地址。

       var ptr *int = nil
      
      1. 传递指针给函数

      可以将指针作为参数传递给函数,这样函数可以直接修改指针指向的变量的值。

       func modifyValue(ptr *int) {
           *ptr = 200
       }
       ​
       // 使用
       modifyValue(&num)
      

      注意事项

      • 指针使用时需要确保指针指向的变量已经被初始化,否则会导致运行时错误。
      • 在使用指针之前,通常需要进行非空判断,以避免空指针引发的错误。
  • 结构体

    结构体是一种自定义数据类型,它允许将不同类型的数据组合在一起形成一个新的数据类型。通过使用type关键字和大括号{}来定义一个结构体。

    1. 创建结构体

       // 定义一个结构体类型
       type Person struct {
           Name    string
           Age     int
           Address string
       }
      
    2. 创建结构体变量

       // 使用结构体类型创建结构体变量
       var p1 Person
       p1.Name = "Alice"
       p1.Age = 30
       p1.Address = "123 Main St"
      
    3. 结构体初始化

       // 使用字面值初始化结构体
       p2 := Person{
           Name:    "Bob",
           Age:     25,
           Address: "456 Elm St",
       }
      
    4. 结构体指针

      可以使用结构体指针来操作结构体变量,避免数据的拷贝。

       // 创建结构体指针
       var ptr *Person
       ptr = &p1
       ​
       // 通过指针修改结构体字段的值
       ptr.Age = 31
      
    5. 匿名结构体

      可以直接定义匿名结构体,通常用于临时存储数据。

       // 定义匿名结构体并初始化
       data := struct {
           ID      int
           Message string
       }{
           ID:      1,
           Message: "Hello, World!",
       }
      
    6. 结构体嵌套

      结构体可以嵌套其他结构体,形成复杂的数据结构。

       type Address struct {
           City  string
           State string
       }
       ​
       type Person struct {
           Name    string
           Age     int
           Address Address
       }
       ​
       // 使用嵌套结构体
       p := Person{
           Name: "John",
           Age:  35,
           Address: Address{
               City:  "New York",
               State: "NY",
           },
       }
      
  • 结构体方法

    方法是一种特殊类型的函数,它和结构体关联在一起,可以用于对结构体数据进行操作和处理。通过结构体方法,可以将数据和操作封装在一起,使代码更加模块化和易于维护。在Go语言中,通过在函数名前添加接收者(receiver)来定义结构体方法。接收者相当于方法的调用者,用于绑定方法与特定的结构体类型。

    1. 定义结构体类型

       // 定义一个结构体类型
       type Rectangle struct {
           Width  float64
           Height float64
       }
      
    2. 定义结构体方法

       // 定义一个计算矩形面积的方法,接收者为Rectangle类型
       func (r Rectangle) Area() float64 {
           return r.Width * r.Height
       }
      
    3. 调用结构体方法

       // 创建Rectangle结构体变量
       rect := Rectangle{
           Width:  10,
           Height: 5,
       }
       ​
       // 调用结构体方法计算面积
       area := rect.Area()
      
    4. 指针接收者的方法

      使用指针接收者可以在方法中修改结构体数据,而不仅仅是对副本进行操作。

       // 定义一个修改矩形高度的方法,接收者为指向Rectangle的指针
       func (r *Rectangle) SetHeight(height float64) {
           r.Height = height
       }
      
    5. 调用指针接收者的方法

       // 创建Rectangle结构体指针变量
       rectPtr := &Rectangle{
           Width:  8,
           Height: 6,
       }
       ​
       // 调用指针接收者的方法修改高度
       rectPtr.SetHeight(7)
      
    6. 值接收者 vs. 指针接收者

      • 值接收者:用于不需要修改结构体数据的场景,避免不必要的内存拷贝。
      • 指针接收者:用于需要修改结构体数据的场景,可以避免数据的拷贝。

      注意:结构体方法的接收者类型必须在同一包内定义,不能对来自其他包的类型定义方法。

  • 错误处理

    在Go语言中,错误通常由函数的返回值来表示,通常是最后一个返回值,类型为error

    1. 定义错误类型

      在标准库中,错误通常由内置的errors包来处理,通过errors.New函数可以创建一个新的错误。

       import "errors"
       ​
       func divide(a, b float64) (float64, error) {
           if b == 0 {
               return 0, errors.New("division by zero")
           }
           return a / b, nil
       }
      
    2. 返回错误值

      在函数中,当发生错误的时候,使用return语句返回错误值,同时将结果设为零值或合适的默认值。

       func doSomething() error {
           if someCondition {
               return errors.New("something went wrong")
           }
           // ... 正常情况下的处理 ...
           return nil
       }
      
    3. 调用函数并处理错误

      在调用可能返回错误的函数时,需要显式地检查错误,并根据错误情况采取相应的处理措施。

       result, err := divide(10.0, 2.0)
       if err != nil {
           // 处理错误情况
           fmt.Println("Error:", err)
       } else {
           // 处理正常情况
           fmt.Println("Result:", result)
       }
      
    4. 自定义错误类型

      除了使用errors.New来创建错误外,还可以定义自己的错误类型,只需实现error接口的Error()方法即可。

       type MyError struct {
           Message string
       }
       ​
       func (e MyError) Error() string {
           return e.Message
       }
       ​
       func doSomething() error {
           return MyError{Message: "something went wrong"}
       }
      
  • 字符串

    1. 创建字符串

      在Go语言中,可以使用双引号""或反引号`来创建字符串。

       str1 := "Hello, World!"  // 使用双引号创建字符串
       str2 := `Hello, Go!`     // 使用反引号创建字符串,支持多行字符串
      
    2. 字符串长度

      可以使用len()函数获取字符串的字节长度(不是字符数)。

       length := len(str1) // 获取字符串str1的字节长度
      
    3. 字符串拼接

      可以使用+运算符来拼接字符串。

       greeting := "Hello, "
       name := "Alice"
       message := greeting + name // 将两个字符串拼接为一个新的字符串
      
    4. 字符串索引

      可以使用下标运算符[]来访问字符串中的单个字节。

       char := str1[0] // 获取字符串str1中第一个字节(即第一个字符)
      
    5. 字符串遍历

      可以使用for range循环遍历。

       for _, char := range str1 {
           // 处理每个字符
           fmt.Println(char)
       }
      
    6. 字符串切片

      可以使用切片操作截取字符串的子串。

       subStr := str1[0:5] // 截取str1中从索引0到索引4的子串(不包含索引5)
      
    7. 字符串比较

      可以使用==!=运算符来比较字符串是否相等。

       str1 := "hello"
       str2 := "Hello"
       isEqual := (str1 == str2) // 比较字符串str1和str2是否相等
      
    8. 注意事项

      • 字符串是不可变的,一旦创建就不能直接修改其内容,任何修改操作都会创建一个新的字符串。
      • 使用rune类型处理Unicode字符可能更加方便,因为rune类型可以表示一个Unicode码点。