Go学习笔记

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位系统)
      int8 8位有符号整数 -128 到 127
      int16 16位有符号整数 -32768 到 32767
      int32 32位有符号整数 -2147483648 到 2147483647
      int64 64位有符号整数 -9223372036854775808 到 9223372036854775807
      uint 无符号整数 0 到 4294967295(32位系统)
      0 到 18446744073709551615(64位系统)
      uint8 8位无符号整数 0 到 255
      uint16 16位无符号整数 0 到 65535
      uint32 32位无符号整数 0 到 4294967295
      uint64 64位无符号整数 0 到 18446744073709551615
      float32 32位浮点数 约 -3.4e38 到 3.4e38
      float64 64位浮点数 约 -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码点。

  • 并发:多线程程序在一个核的CPU上运行 并行:多线程程序在多个核上的CPU上运行

  • 协程:用户态,轻量级线程,栈KB级别 线程:内核态,线程跑多个协程,栈MB级别

  • goroutine

    //快速打印“hello goroutine”
    func hello(i int){
        println("hello goroutine:" + fmt.Sprint(i))
    }
    
    go func HelloGoRoutine(){
        for i:=0;i<5;i++{
            hello(j)
        }(i)
        time.Sleep(time.Second)
    }
    
  • 协程之间的通信:CSP(Communicating Sequential Processes)提倡通过通信共享内存而不是通过共享内存而实现通信

  • channel

    make(chan 元素类型,[缓冲大小])
    make(chan int)//无缓冲通道,同步通道
    make(chan int,1)//有缓冲通道
    
  • WaitGroup实现并发编程的同步

    • ADD(delta int)计数器+delta
    • Done()计数器-1
    • Wait()阻塞直到计数器为0
    var wg sync.WaitGroup
    wg.Add
    ...
    
  • 并发
    • 基本概念

      • 进程:程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。
      • 线程:操作系统基于进程开启的轻量级进程,是操作系统调度执行的最小单位
      • 协程:非操作系统提供而是由用户自行创建和控制的用户态“线程”,比线程更轻量级
    • 并发模型

      • 线程&锁模型
      • Actor模型
      • CSP模型
      • Fork&Join模型
      • Go语言中的并发程序主要是通过基于CSP的goroutine和channel来实现
    • Goroutine是Go语言支持并发的核心,在一个Go程序中同时创建成百上千个goroutine是非常普遍的,一个goroutine会以一个很小的栈开始其生命周期,一般只需要2KB

      • 区别于操作系统线程由系统内核进行调度, goroutine 是由Go运行时(runtime)负责调度。例如Go运行时会智能地将 m个goroutine 合理地分配给n个操作系统线程,实现类似m:n的调度机制,不再需要Go开发者自行在代码层面维护一个线程池。
      • Goroutine 是 Go 程序中最基本的并发执行单元。每一个 Go 程序都至少包含一个 goroutine——main goroutine,当 Go 程序启动时它会自动创建。
      • 在Go语言编程中不需要去自己写进程、线程、协程,需要让某个任务并发执行的时候,只需要把这个任务包装成一个函数,开启一个 goroutine 去执行这个函数就可以了
    • go关键字

      • 使用goroutine在函数或方法在调用前加上go关键字就可以创建一个goroutine,从而让该函数或方法在新创建的goroutine中执行
      go f()//创建一个新的goroutine运行函数f
      go func(){
      //
      }()
      //一个goroutine必定对应一个函数/方法,可以创建多个goroutine去执行相同的函数/方法
      
    • 启动单个goroutine

      • 在调用函数(普通函数和匿名函数)前加上一个go关键字

        package main
        import "fmt"
        func hello(){
        	fmt.Println("hello")
        }
        func main(){
        	hello()
        	fmt.Println("你好")
        }
        /*
        hello
        你好
        */
        
        

      func main(){
      	go hello()//启动另一个goroutine去执行hello()函数
      	fmt.Println("你好")
      }
      /*
      你好
      */
      
      • 在Go程序启动时,会为main函数创建一个默认的goroutine。在上面的代码中在main函数中使用go关键字创建了另外一个goroutine去执行hello函数,而此时main goroutine还在继续往下执行,此时存在两个并发执行的goroutine。

      • 当main函数结束时整个程序也就结束了,同时main goroutine也结束了,所有由main goroutine创建的goroutine也会一同退出。

      • 上述”hello“没有打印出的原因是:main函数退出太快,另外一个goroutine中的函数还未执行完程序就退出了,导致未打印出“hello”

        package main
        import "fmt"
        func hello(){
        	fmt.Println("hello")
        }
        func main(){
        	go hello()
        	fmt.Println("你好")
        	time.Sleep(time.Second)//time.Sleep一秒钟
        }
        /*
        你好
        hello
        */
        
      • 先打印“你好”的原因:在程序中创建goroutine执行函数需要一定的开销,与此同时main函数所在的goroutine是继续执行的

      • Go语言中通过sync包提供了一些常用的并发原语。

        //当并不关心并发操作的结果或有其它方式收集并发操作的结果时,WaitGroup是实现等待一组并发操作完成的好方法
        packgae main
        import (
        	"fmt"
            "sync"
        )
        var wg sync.WaitGroup//声明全局等待组变量
        func hello(){
            fmt.Println("hello")
            wg.Done()//告知当前goroutine完成
        }
        func main(){
            wg.Add(1)//登记一个goroutine
            go hello()
            fmt.Println("你好")
            wg.Wait()//阻塞等待登记的goroutine完成
        }
        /*
        你好
        hello
        */
        //此次程序不会有多余的停顿,hello goroutine执行完毕后程序直接退出
        
    • 启动多个goroutine

      package main
      import (
      	"fmt"
          "sync"
      )
      var wg sync.WaitGroup
      func hello(i int){
          defer wg.Done()//
          fmt.Println("hello",i)
      }
      func main(){
          for i:=0;i<10;i++{
              wg.Add(1)
              go hello(i)
          }
          wg.Wait()
      }
      //每次终端上打印数字的顺序都不一致,因为10个goroutine是并发执行的,而goroutine的调度是随机的
      
    • 动态栈

      • 操作系统的线程一般都有固定的栈内存(通常为2MB),而Go语言中一个goroutine的初始栈空间一般为2KB。
      • Go的runtime会自动为goroutine分配合适的栈空间
    • goroutine调度

      • 操作系统内核调度时会挂起当前正在执行的线程并将寄存器中的内容保存到内存中,然后选出接下来要执行的线程并从内存中恢复该线程的寄存器信息,然后恢复执行该线程的现场并开始执行线程。从一个线程切换到另一个线程需要完整的上下文切换,因为可能需要多次内存访问,索引整个切换上下文的操作开销较大,会增加运行的cpu周期。
      • 区别于操作系统内核调度操作系统线程,goroutine的调度是Go语言运行时(runtime)层面的实现,是完全由Go语言本身实现的一套调度系统—go scheduler。按照一定的规则将所有的goroutine调度到操作系统线程上执行。
posted @   皮蛋呐  阅读(21)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示