字节笔记本字节笔记本

一小时转职Golang工程师

2024-01-15

《一小时转职Golang工程师》教程涵盖了从安装Golang环境到掌握基本编程概念的全过程。

Golang环境

  1. 下载Golang:

    • 访问Golang的官方网站 golang.org
    • 根据操作系统(Windows, macOS, Linux)选择合适的安装包下载。
  2. 安装Golang:

    • 对于Windows
      • 运行下载的安装程序并遵循指示完成安装。
      • 安装程序通常会自动设置环境变量(如GOPATHGOROOT)。
    • 对于macOSLinux
      • 使用包管理器(如Homebrew for macOS)或通过解压下载的压缩文件并执行安装脚本。
      • 例如,在Linux上,您可以解压下载的文件到/usr/local,然后将/usr/local/go/bin添加到您的PATH环境变量中。
  3. 配置环境变量:

    • 确保GOPATHGOROOT环境变量被正确设置。GOROOT是Go安装的位置,而GOPATH是您的工作目录(您的Go代码和依赖库存放的位置)。
    • 通常,现代版本的Go会自动设置这些变量。但如果遇到问题,您可能需要手动配置。
  4. 验证安装:

    • 打开命令行或终端。
    • 输入go version来验证Go是否已经正确安装。这应该会显示安装的Go版本。
    • 输入go env可以查看Go的环境配置。
  5. 搭建一个简单的工作区:

    • 创建一个新的目录作为您的工作空间(例如:~/go)。
    • 在这个目录下创建子目录src,在src下编写您的Go代码。
  6. 开始编写Go代码:

    • src目录下创建您的第一个Go文件(如hello.go)。
    • 使用文本编辑器编写一个简单的Hello World程序。
    • 在命令行中运行go run hello.go来运行您的程序。

main函数

package main

import "fmt"

func main() {
    fmt.Println("Hello, world!")
}
  1. 包声明(Package Declaration):

    • 每个Go文件都以包声明开始,这里是package mainmain包是一个特殊的包,它告诉Go编译器这个程序是可执行的,而不是一个库。
  2. 导入语句(Import Statement):

    • import "fmt"这行代码导入了Go的标准库fmt,它包含格式化I/O(输入/输出)的函数。这里使用它来打印文本到标准输出。
  3. main函数(Main Function):

    • func main() {...}定义了程序的入口点。Go程序从main函数开始执行。
    • 在Go中,函数使用func关键字声明。这里的main函数没有参数也没有返回类型。
  4. 打印输出(Print Statement):

    • fmt.Println("Hello, world!")这行代码使用了fmt包的Println函数来打印一行文本到标准输出。这里它打印了经典的“Hello, world!”消息。

四种常见的变量声明方式

  1. 使用 var 关键字带明确类型:

    var a int
    a = 5
    
  2. 使用 var 关键字带初始值(类型推断):

    var b = 10
    
  3. 短变量声明(类型推断):

    • 这是在函数内部声明局部变量的简洁方式。
    c := 15
    
  4. 声明一个常量:

    • 常量使用 const 关键字,常量的值在编译时已确定,且不能改变。
    const d = 20
    

多变量声明

  1. 多变量同时声明:

    var e, f int
    e = 25
    f = 30
    
  2. 多变量同时声明并初始化:

    var g, h = 35, 40
    
  3. 短变量声明多个变量:只能在函数内部使用。

    i, j := 45, 50
    
  4. 同时声明多个常量:

    const (
        k = 55
        l = 60
    )
    

const和iota

常量可以是带类型的,也可以是无类型的。无类型常量有更高的灵活性,它们在需要类型时会自动适应。

const typedConst int = 100
const untypedConst = 100

常量的值必须是在编译时就能确定的,不能是运行时才能确定的值。

const Pi = 3.14 // 正确
const Random = rand.Int() // 错误,因为它的值在运行时才能确定

常用于声明一系列相关的值。

const (
    Monday = 1
    Tuesday = 2
    // ...
)

在一个const声明块中,iota从0开始,每新增一行常量声明,iota的值就递增1。

const (
    a = iota // 0
    b = iota // 1
    c = iota // 2
)

在一个const块中,如果省略了表达式,则表示与上一行的表达式相同。

const (
    a = iota // 0
    b        // 1
    c        // 2
)

iota常用于声明位掩码(bitmask),这是一种有效的表示一组布尔值的方法。

const (
    Flag1 = 1 << iota // 1 << 0 == 1
    Flag2             // 1 << 1 == 2
    Flag3             // 1 << 2 == 4
)

使用_可以跳过某个值。

const (
    a = iota // 0
    _        // 跳过1
    b        // 2
)

使用iota时需要注意的是,它只在const块内有效,每个新的const块都会重置iota的计数。

函数

当涉及到 Go 语言中的函数示范时,以下是一些常见的示例,涵盖了不同类型的函数用途和功能:

  1. 基本函数示例:定义一个简单的函数,用于执行加法操作并返回结果。
package main

import "fmt"

// 定义一个加法函数
func add(a, b int) int {
    return a + b
}

func main() {
    result := add(3, 5)
    fmt.Println("3 + 5 =", result)
}
  1. 函数返回多个值:示范一个函数,返回多个值,如商和余数。
package main

import "fmt"

// 定义一个除法函数,返回商和余数
func divide(dividend, divisor int) (int, int) {
    quotient := dividend / divisor
    remainder := dividend % divisor
    return quotient, remainder
}

func main() {
    quotient, remainder := divide(10, 3)
    fmt.Printf("Quotient: %d, Remainder: %d\n", quotient, remainder)
}
  1. 匿名函数:示范创建和使用匿名函数。
package main

import "fmt"

func main() {
    // 创建并调用匿名函数
    result := func(a, b int) int {
        return a * b
    }(3, 4)

    fmt.Println("3 * 4 =", result)
}
  1. 闭包:示范使用闭包,其中一个函数返回另一个函数。
package main

import "fmt"

// 返回一个函数,该函数将参数与闭包中的值相加
func adder(base int) func(int) int {
    return func(x int) int {
        return base + x
    }
}

func main() {
    addTwo := adder(2)
    result := addTwo(5)
    fmt.Println("2 + 5 =", result)
}
  1. 可变参数函数:示范定义可接受可变数量参数的函数。
package main

import "fmt"

// 定义可变参数函数,计算所有参数的和
func sum(numbers ...int) int {
    total := 0
    for _, num := range numbers {
        total += num
    }
    return total
}

func main() {
    result := sum(1, 2, 3, 4, 5)
    fmt.Println("Sum:", result)
}

指针

指针允许你直接操作变量的内存地址,从而提供了一种操作和共享程序中数据的有效方式。

  • 取址:使用&运算符可以获得一个变量的内存地址。

  • 取值:使用*运算符可以获取指针指向的实际值(解引用)。

  • 声明指针:指针声明类似于其他变量声明,但在类型前加上*

    var p *int
    
  • 初始化指针

    var x int = 10
    p = &x
    
  • 读取指针指向的值(解引用):

    fmt.Println(*p)  // 输出x的值,即10
    
  • 修改指针指向的值:可以通过指针来修改它指向的值。

    *p = 20  // 改变x的值
    fmt.Println(x)  // 现在x的值是20
    
  • 使用指针作为函数参数:指针可以作为函数的参数传递,这允许函数修改传递给它的变量的值。

    func increment(p *int) {
        *p++
    }
    

defer语句

一个函数中有多个defer语句时,它们的执行顺序是后进先出(LIFO)的。也就是说,最后一个被defer的语句将会最先执行。

package main

import "fmt"

func main() {
    defer fmt.Println("First defer")
    defer fmt.Println("Second defer")
    defer fmt.Println("Third defer")

    fmt.Println("Function body")
}

这段代码的输出将会是:

Function body
Third defer
Second defer
First defer

defer语句的这种特性使其非常适合用于处理成对的操作,如打开/关闭文件、加锁/解锁、分配/释放资源等。

file, err := os.Open("file.txt")
if err != nil {
    // 错误处理
}
defer file.Close()  // 确保在函数结束时关闭文件

值类型和引用类型

值类型的变量直接存储值,它们的值存储在栈上。当这些变量被赋值给另一个变量时,或者作为参数传递给函数时,实际上是对值进行了拷贝。

常见的值类型包括:

  • 基本数据类型:intfloatboolstring

  • 复合数据类型:arraystruct

  • 值的赋值和传递是独立的副本。

  • 修改副本不会影响原始数据。

代码示例

package main

import "fmt"

func modifyArray(a [3]int) {
    a[0] = 90
}

func main() {
    arr := [3]int{10, 20, 30}
    modifyArray(arr)
    fmt.Println(arr) // 输出 [10 20 30],原数组未改变
}

引用类型的变量存储的是一个指向存储值的内存地址的引用,而不是值本身。这些值存储在堆上。当引用类型的变量被赋值或传递时,实际上是引用(内存地址)的拷贝。

常见的引用类型包括:

  • slice
  • map
  • chan
  • 指针(pointer
  • 函数(func

特点:

  • 赋值和传递是对同一内存地址的引用。
  • 修改副本会影响原始数据。

代码示例

package main

import "fmt"

func modifySlice(s []int) {
    s[0] = 90
}

func main() {
    slice := []int{10, 20, 30}
    modifySlice(slice)
    fmt.Println(slice) // 输出 [90 20 30],原切片被改变
}
  • 值类型是指变量直接包含其值,而引用类型则是变量包含了对其值的引用。
  • 修改值类型的变量副本不会影响原始变量,而修改引用类型的副本则会影响到所有引用了相同数据的变量。
  • 选择使用值类型还是引用类型取决于具体情况,如是否需要跨函数共享数据,以及对性能的考虑等。

零值

零值(Zero Value)是变量在声明后自动赋予的初始默认值,如果没有显式初始化。每种类型都有其对应的零值。

以下是Go中一些常见类型的零值:

  1. 布尔型(bool:
    • 零值为false
  2. 数值类型(整型int、浮点型float64等):
    • 零值为0
  3. 字符串(string:
    • 零值为空字符串""
  4. 指针(*T:
    • 零值为nil,表示没有指向任何对象。
  5. 切片([]T:
    • 零值为nil,表示没有分配内存。
  6. 映射(map[K]V:
    • 零值为nil,表示没有分配内存。注意,nil映射不能被赋值。
  7. 通道(chan T:
    • 零值为nil,表示没有分配内存。
  8. 接口(interface:
    • 零值为nil,表示没有类型和值。
  9. 数组([N]T:
    • 数组的零值是其元素类型的零值构成的数组。
  10. 结构体(struct:
    • 结构体的零值是其所有字段都设置为各自类型的零值的结构体。

导入包

在Go语言中,import语句用于导入包(package),而init函数则用于在程序开始执行之前初始化包。理解这两个特性的工作方式对于编写和管理Go程序非常重要。

当导入标准库中的包时,只需使用包名。

import "fmt"

对于自定义包,需要使用包的完整路径

import "github.com/user/project/mypackage"

对于项目内部的包,导入路径相对于项目根目录。

import "./subfolder/mypackage"

如果导入的两个包有相同的名字,或者为了提高可读性,可以使用别名。

import fm "fmt"

使用点导入,可以直接使用该包中的公开标识符,而无需通过包名限定。

import . "fmt"

下划线(_)导入 仅为了包的副作用(如初始化),而不直接使用包中的任何函数、类型或变量。

import _ "net/http/pprof"

init 方法调用流程

每个包可以包含一个或多个init函数,这些函数在程序启动时自动调用,用于初始化包。init函数的特点和调用流程如下:

  1. 初始化顺序:

    • 包的初始化顺序是按照它们被导入的顺序进行的。
    • 首先初始化被导入的包,然后才是导入它们的包。
  2. 无参数、无返回值:

    • init函数没有参数也没有返回值。
    func init() {
        // 初始化代码
    }
    
  3. 自动调用:

    • init函数在包首次被加载时自动调用,无需手动调用。
    • 每个包可以有多个init函数,它们按照在代码中出现的顺序依次执行。
  4. 用途:

    • 常用于执行包级别的初始化任务,如设置全局变量、注册、运行一次性计算等。
  5. 执行顺序:

    • 若一个包被多个其他包导入,它的init只会执行一次。
    • 主包(main package)的init函数是最后被调用的。

切片

创建切片

package main

import "fmt"

func main() {
    // 创建一个空的切片
    var s1 []int
    fmt.Println(s1) // 输出 []

    // 使用make创建切片
    s2 := make([]int, 5) // 长度和容量为5
    fmt.Println(s2) // 输出 [0 0 0 0 0]

    // 通过字面量创建切片
    s3 := []int{1, 2, 3}
    fmt.Println(s3) // 输出 [1 2 3]
}

修改切片

s := []int{1, 2, 3}
s[0] = 10
fmt.Println(s) // 输出 [10 2 3]

追加元素

append函数可以向切片中追加新的元素。

s := []int{1, 2, 3}
s = append(s, 4, 5)
fmt.Println(s) // 输出 [1 2 3 4 5]

以通过指定开始和结束的索引来创建一个新的切片。新切片和原切片共享同一个底层数组。

s := []int{1, 2, 3, 4, 5}
sub := s[1:4] // 获取索引1到3的元素
fmt.Println(sub) // 输出 [2 3 4]

使用forfor range循环可以遍历切片中的所有元素。

s := []int{1, 2, 3, 4, 5}
for i, v := range s {
    fmt.Printf("Index: %d, Value: %d\n", i, v)
}

长度len)是切片中元素的数量。

容量cap)是从切片的第一个元素开始到底层数组末尾的元素数量。

s := []int{1, 2, 3, 4, 5}
fmt.Println(len(s)) // 输出 5
fmt.Println(cap(s)) // 输出 5

map 的使用演示

声明和初始化

package main

import "fmt"

func main() {
    // 声明并初始化一个映射
    var myMap map[string]int = make(map[string]int)

    // 使用映射字面量初始化映射
    myMap = map[string]int{"apple": 5, "banana": 6, "orange": 9}

    fmt.Println(myMap) // 输出: map[apple:5 banana:6 orange:9]
}

添加和修改元素

package main

import "fmt"

func main() {
    myMap := make(map[string]int)

    // 添加键值对
    myMap["apple"] = 5
    myMap["banana"] = 6

    // 修改键对应的值
    myMap["apple"] = 7

    fmt.Println(myMap) // 输出: map[apple:7 banana:6]
}

获取元素

package main

import "fmt"

func main() {
    myMap := map[string]int{"apple": 5, "banana": 6}

    // 获取键对应的值
    value, ok := myMap["apple"]
    if ok {
        fmt.Println("apple:", value) // 输出: apple: 5
    }

    // 尝试获取不存在的键
    value, ok = myMap["pear"]
    if !ok {
        fmt.Println("pear not found")
    }
}

删除元素

package main

import "fmt"

func main() {
    myMap := map[string]int{"apple": 5, "banana": 6}

    // 删除键值对
    delete(myMap, "apple")

    fmt.Println(myMap) // 输出: map[banana:6]
}

遍历映射

package main

import "fmt"

func main() {
    myMap := map[string]int{"apple": 5, "banana": 6, "orange": 9}

    // 遍历映射
    for key, value := range myMap {
        fmt.Println(key, value)
    }
}

结构体

定义结构体需要使用type关键字,后跟结构体的名称和struct关键字,以及定义在大括号{}内的一系列字段。

package main

import "fmt"

// 定义一个结构体
type Person struct {
    Name string
    Age  int
}

创建结构体实例

func main() {
    // 使用字段名
    person1 := Person{Name: "Alice", Age: 30}

    // 不使用字段名(必须按照结构体定义的顺序)
    person2 := Person{"Bob", 25}

    fmt.Println(person1) // 输出: {Alice 30}
    fmt.Println(person2) // 输出: {Bob 25}
}

使用new关键字: 创建结构体的指针。

func main() {
    personPtr := new(Person)  // 创建一个指向Person类型新实例的指针
    personPtr.Name = "Charlie"
    personPtr.Age = 40

    fmt.Println(personPtr)  // 输出: &{Charlie 40}
}

访问结构体字段可以直接通过.操作符实现。

func main() {
    person := Person{"Dave", 35}
    fmt.Println(person.Name) // 输出: Dave
    fmt.Println(person.Age)  // 输出: 35
}

组合和继承

package main

import "fmt"

// 定义一个人的结构体
type Person struct {
    FirstName string
    LastName  string
}

// 定义一个学生的结构体,嵌套了 Person 结构体
type Student struct {
    Person      // 匿名字段,嵌套了 Person 结构体
    StudentID   int
    Grade       string
}

func main() {
    // 创建一个学生对象
    student := Student{
        Person:    Person{FirstName: "John", LastName: "Doe"},
        StudentID: 12345,
        Grade:     "A",
    }

    // 访问学生和人的字段
    fmt.Println("学生的姓氏:", student.LastName) // 直接访问 Person 结构体的字段
    fmt.Println("学生的学号:", student.StudentID)
    fmt.Println("学生的年级:", student.Grade)
}

字符串操作

package main

import (
    "fmt"
    "strings"
)

func main() {
    str := "Hello, World!"

    // 拼接
    greeting := "Hello, " + "Go!"
    fmt.Println(greeting)

    // 格式化
    name := "John"
    age := 30
    formattedStr := fmt.Sprintf("My name is %s and I am %d years old.", name, age)
    fmt.Println(formattedStr)

    // 分割
    parts := strings.Split(str, ",")
    fmt.Println(parts)

    // 查找
    contains := strings.Contains(str, "World")
    fmt.Println("Contains 'World':", contains)

    // 替换
    newStr := strings.Replace(str, "Hello", "Hi", -1)
    fmt.Println(newStr)

    // 大小写转换
    lower := strings.ToLower(str)
    fmt.Println("Lowercase:", lower)
    upper := strings.ToUpper(str)
    fmt.Println("Uppercase:", upper)

    // 去除空白
    trimmed := strings.TrimSpace("   some text   ")
    fmt.Println("Trimmed:", trimmed)
}

字符串转换

package main

import (
    "fmt"
    "strconv"
)

func main() {
    // 字符串转整数
    strInt := "100"
    intValue, err := strconv.Atoi(strInt)
    if err != nil {
        fmt.Println(err)
    } else {
        fmt.Println("Integer value:", intValue)
    }

    // 整数转字符串
    strFromInt := strconv.Itoa(intValue)
    fmt.Println("String from Integer:", strFromInt)

    // 字符串转浮点数
    strFloat := "123.45"
    floatValue, err := strconv.ParseFloat(strFloat, 64)
    if err != nil {
        fmt.Println(err)
    } else {
        fmt.Println("Float value:", floatValue)
    }

    // 浮点数转字符串
    strFromFloat := fmt.Sprintf("%f", floatValue)
    fmt.Println("String from Float:", strFromFloat)

    // 字符串转布尔
    strBool := "true"
    boolValue, err := strconv.ParseBool(strBool)
    if err != nil {
        fmt.Println(err)
    } else {
        fmt.Println("Boolean value:", boolValue)
    }

    // 布尔转字符串
    strFromBool := fmt.Sprintf("%t", boolValue)
    fmt.Println("String from Boolean:", strFromBool)

    // 字符串与字节切片
    strBytes := "hello"
    bytes := []byte(strBytes)
    fmt.Println("Bytes:", bytes)
    strFromBytes := string(bytes)
    fmt.Println("String from Bytes:", strFromBytes)
}

正则表达式

package main

import (
    "fmt"
    "regexp"
)

func main() {
    // 定义正则表达式
    pattern := `^\w+@\w+\.\w+$` // 简单的电子邮件地址匹配
    input := "[email protected]"

    // 编译正则表达式
    r, err := regexp.Compile(pattern)
    if err != nil {
        fmt.Println("Error compiling regex:", err)
        return
    }

    // 检查匹配
    matched := r.MatchString(input)
    fmt.Println("Matched:", matched)

    // 查找匹配项
    match := r.FindString(input)
    fmt.Println("FindString:", match)

    // 替换匹配的字符串
    replacement := "[email protected]"
    result := r.ReplaceAllString(input, replacement)
    fmt.Println("ReplaceAllString:", result)

    // 多个匹配示例
    input = "[email protected] [email protected]"
    matches := r.FindAllString(input, -1)
    fmt.Println("FindAllString:", matches)
}

面向对象

package main

import (
    "fmt"
    "math"
)

// 定义一个结构体表示几何形状
type Shape interface {
    Area() float64
}

// 定义一个矩形结构体
type Rectangle struct {
    Width  float64
    Height float64
}

// 定义一个圆形结构体
type Circle struct {
    Radius float64
}

// 为矩形定义一个方法计算面积
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

// 为圆形定义一个方法计算面积
func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

// 定义一个函数,接受一个几何形状,并计算其面积
func CalculateArea(shape Shape) float64 {
    return shape.Area()
}

func main() {
    // 创建一个矩形和一个圆形
    rectangle := Rectangle{Width: 5, Height: 3}
    circle := Circle{Radius: 2}

    // 使用 CalculateArea 函数计算它们的面积
    rectangleArea := CalculateArea(rectangle)
    circleArea := CalculateArea(circle)

    // 打印结果
    fmt.Printf("矩形的面积: %.2f\n", rectangleArea)
    fmt.Printf("圆形的面积: %.2f\n", circleArea)
}

错误处理

定义错误

err := errors.New("错误信息")

返回错误: 在函数中,当遇到问题时,应该返回错误。

func doSomething() error {
    return errors.New("出错了")
}

检查错误:调用函数后,应该立即检查错误。

err := doSomething()
if err != nil {
    // 处理错误
}

可以通过实现error接口来创建自定义错误类型。error接口非常简单,只要实现了Error() string方法即可。

type MyError struct {
    Msg string
    Code int
}

func (e *MyError) Error() string {
    return fmt.Sprintf("code=%d, msg=%s", e.Code, e.Msg)
}

panic 中断程序

func mayPanic() {
    // 这里是一些逻辑
    panic("something bad happened")
}

func main() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from panic:", r)
        }
    }()
    
    mayPanic()
    fmt.Println("After panic")
}

例子中,mayPanic函数中发生了panicmain函数中的defer语句用于捕获并恢复panic,以便程序可以正常继续运行。

接口(interfaces)

  1. 接口(Interfaces):
    接口是一组方法签名的集合。当一个类型为接口中的所有方法提供定义时,它被称为实现了该接口。

  2. 类型(Types):
    任何类型,只要它实现了接口中的所有方法,就被认为实现了该接口。在Go中,这种实现是隐式的,无需显式声明。

多态的实现依赖于接口。这意味着你可以编写接受接口类型参数的函数或方法,然后使用实现了该接口的任何类型的实例来调用它。

示例

下面是一个演示Go中多态性的例子:

package main

import "fmt"

// Talker 接口
type Talker interface {
    Talk() string
}

// Dog 类型
type Dog struct {
    Name string
}

// Dog 类型实现 Talker 接口
func (d Dog) Talk() string {
    return "Woof! I am " + d.Name
}

// Cat 类型
type Cat struct {
    Name string
}

// Cat 类型实现 Talker 接口
func (c Cat) Talk() string {
    return "Meow! I am " + c.Name
}

// PerformTalk 函数接受 Talker 接口类型的参数
// 任何实现了 Talker 接口的类型都可以作为参数传入
func PerformTalk(t Talker) {
    fmt.Println(t.Talk())
}

func main() {
    dog := Dog{Name: "Buddy"}
    cat := Cat{Name: "Whiskers"}

    PerformTalk(dog)
    PerformTalk(cat)
}

反射和标签

反射在Go中主要涉及两个类型:reflect.Typereflect.Value

  • reflect.Type用于表示一个Go值的类型。
  • reflect.Value用于表示一个Go值。

使用reflect.TypeOf()reflect.ValueOf()函数来获取任意对象的TypeValue

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var x float64 = 3.4
    fmt.Println("Type:", reflect.TypeOf(x))
    fmt.Println("Value:", reflect.ValueOf(x))
}

通过reflect.Value,你可以获取关联值的原始类型。使用ValueInterface()方法可以返回一个interface{}类型,然后你可以使用类型断言来获取实际的值。

v := reflect.ValueOf(x)
y := v.Interface().(float64)
fmt.Println(y)

如果reflect.Value是可设置的(可寻址的),你可以修改它包含的值。要想修改,首先需要确保reflect.Value是通过指针创建的。

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var x float64 = 3.4
    v := reflect.ValueOf(&x)  // 注意:传入x的指针
    v.Elem().SetFloat(7.1)
    fmt.Println(x)
}

使用反射来动态地调用方法。

package main

import (
    "fmt"
    "reflect"
)

type MyStruct struct {
    Field int
}

func (s MyStruct) Add(a, b int) int {
    return a + b
}

func main() {
    s := MyStruct{Field: 10}
    v := reflect.ValueOf(s)
    m := v.MethodByName("Add")
    args := []reflect.Value{reflect.ValueOf(1), reflect.ValueOf(2)}
    result := m.Call(args)
    fmt.Println(result[0].Int())  // 输出:3
}

序列化和反序列化

结构体标签定义在结构体字段之后,格式为key:"value"。对于JSON,关键字是json,后面跟上对应的JSON字段名。

type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
    Email string `json:"email,omitempty"`
}

在这个例子中,我们定义了一个User结构体,每个字段都有一个json标签。

  • NameAge字段在JSON中将使用相同的名称。
  • Email字段有一个额外的选项omitempty,这意味着如果Email字段是空字符串,它将在JSON中被忽略。

序列化(Struct到JSON)

user := User{Name: "Alice", Age: 25, Email: "[email protected]"}
jsonData, err := json.Marshal(user)
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(jsonData))  // 输出: {"name":"Alice","age":25,"email":"[email protected]"}

反序列化(JSON到Struct)

var user User
jsonData := []byte(`{"name":"Bob","age":30,"email":"[email protected]"}`)
err := json.Unmarshal(jsonData, &user)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("%+v\n", user)  // 输出: {Name:Bob Age:30 Email:[email protected]}

并发与并行的基础

  • 并发(Concurrency):指的是程序中多个任务的执行时间上重叠。在单核CPU上,这通常通过任务间切换实现,给人一种“同时执行”的错觉。
  • 并行(Parallelism):当程序在多核CPU上运行时,不同的任务可以在不同的核上真正同时执行。

goroutine

创建一个新的goroutine,在函数或方法调用前加上go关键字

这样做会导致函数或方法在新的goroutine中异步执行

package main

import (
    "fmt"
    "time"
)

// 一个简单的函数
func say(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}

func main() {
    // 创建一个新的goroutine执行say函数
    go say("world")

    // 在当前goroutine中执行say函数
    say("hello")
}

  • 轻量级:goroutine在内存占用和初始化成本方面比操作系统线程更高效。
  • 动态增长的堆栈:每个goroutine的堆栈大小在运行时会根据需要动态增长和缩减。
  • 简化的并发:goroutine简化了并发编程的复杂性。通过goroutine,可以轻松地实现并行任务、并发执行和通道通信。

通道

Channels是一种特殊的类型

用于在不同的goroutine之间安全地传递数据

Channels可以被看作是goroutine之间的管道

使用make函数创建一个Channel:

ch := make(chan int) // 创建一个传递int类型的Channel

也可以创建带有缓冲区的Channel:

ch := make(chan int, 100) // 创建一个容量为100的缓冲Channel
  • 发送数据到Channel:使用<-操作符将数据发送到Channel。

    ch <- 10 // 将10发送到Channel
    
  • 从Channel接收数据:使用<-操作符从Channel接收数据。

    value := <-ch // 从Channel接收数据并赋值给变量
    
  • 关闭Channel:使用内置的close函数关闭Channel。

    close(ch) // 关闭Channel
    

    关闭一个Channel意味着不能再向它发送数据。试图向一个已关闭的Channel发送数据会导致panic。但仍然可以从已关闭的Channel接收数据。

  • 零值:Channel的零值是nilnil Channel既不能用于发送,也不能用于接收。

  • 阻塞:发送操作会阻塞,直到另一个goroutine在相同的Channel上进行接收操作,反之亦然。这为goroutine之间提供了一种同步的方式。

  • 非缓冲与缓冲Channel

    • 非缓冲Channel没有存储空间,其发送操作会阻塞,直到接收操作发生。
    • 缓冲Channel有一个固定大小的存储空间,只有当缓冲区满时,发送操作才会阻塞,只有当缓冲区空时,接收操作才会阻塞。
package main

import (
    "fmt"
    "time"
)

func writeToChannel(ch chan int, x int) {
    fmt.Println("Sending", x)
    ch <- x
    fmt.Println("Sent", x)
}

func main() {
    ch := make(chan int)
    go writeToChannel(ch, 10)
    time.Sleep(time.Second) // 延迟确保goroutine有足够的时间运行
    value := <-ch
    fmt.Println("Received", value)
}

在这个示例中,writeToChannel函数向Channel发送一个整数,而main函数从Channel接收这个整数。

  • 使用未初始化的Channel(nil Channel)会导致永久阻塞。
  • 向一个已关闭的Channel发送数据会导致panic。
  • 从一个已关闭且为空的Channel接收数据会立即返回,且返回该类型的零值。

Goroutine的基本模型

  1. 轻量级:goroutine是轻量级的,它们的创建和销毁的开销远小于传统的线程。这允许程序同时运行成千上万的goroutine。
  2. 非抢占式多任务处理:在Go语言中,goroutines在用户空间中被多路复用(multiplexed)到少量的操作系统线程上,而不是像传统的线程那样与操作系统线程一一对应。
  3. 独立的堆栈:每个goroutine都有自己独立的堆栈,这个堆栈可以根据需要动态地增长和缩减。
  4. 通道(Channel):goroutine之间主要通过通道(channel)进行通信和同步。通道可以安全地在多个goroutine之间传递数据,避免了传统线程编程中常见的数据竞争和锁问题。

调度设计策略

Go语言的运行时包含了自己的调度器,这个调度器使用了一种称为M:P:G的调度模型:

  1. M(Machine):代表操作系统的线程。
  2. P(Processor):代表逻辑处理器,其数量可配置,通常设置为CPU核心数。每个P都有一个本地运行队列,存放待运行的goroutines。
  3. G(Goroutine):代表goroutine本身。

调度过程大致如下:

  • 每个P都会绑定一个M,并从其本地运行队列中取出一个G来执行。
  • 如果一个P的本地运行队列为空,它会尝试从其他P的队列中窃取G,或从全局运行队列中获取。
  • 当goroutine进行系统调用或者被阻塞时,M会被解绑并挂起,而P会绑定到其他的M上继续执行goroutine。

此外,Go调度器采用的是协作式调度,这意味着goroutines会在某些关键点进行调度,如I/O操作、channel操作、系统调用等,而不是像抢占式调度那样在任意时刻中断。

无缓冲通道(Unbuffered Channel)

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan int) // 创建一个无缓存区的channel

    go func() {
        fmt.Println("Sending value")
        ch <- 42 // 发送操作,将在接收方准备好之前阻塞
        fmt.Println("Value sent")
    }()

    // 模拟延时以展示发送操作的阻塞性质
    time.Sleep(time.Second)

    fmt.Println("Receiving value")
    value := <-ch // 接收操作
    fmt.Println("Value received:", value)
}


无缓冲通道是指在创建时没有指定缓冲区大小的通道。它提供了一种在两个goroutine之间进行同步的方式。

  • 发送操作:对无缓冲通道的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作。
  • 接收操作:类似地,从无缓冲通道接收数据也会阻塞,直到另一个goroutine在该通道上发送数据。

有缓冲通道(Buffered Channel)

package main

import (
    "fmt"
)

func main() {
    ch := make(chan int, 2) // 创建一个缓冲大小为2的channel

    // 由于channel有缓冲,这些发送操作不会立即阻塞
    ch <- 1
    ch <- 2

    fmt.Println(<-ch) // 接收一个值
    fmt.Println(<-ch) // 接收另一个值
}

有缓冲通道是指在创建时指定了缓冲区大小的通道。它允许发送方和接收方在没有直接同步的情况下交换数据。

  • 发送操作:只要缓冲区未满,就可以向有缓冲通道发送数据,而无需等待接收方准备好。
  • 接收操作:只要缓冲区内有数据,就可以从有缓冲通道接收数据,而无需等待发送方发送数据。

通道与range

ch := make(chan int, 2)

ch <- 1
ch <- 2
close(ch)  // 关闭通道

for v := range ch {
    fmt.Println(v) // 依次打印 1 和 2
}

range可以用来迭代通道中的数据。当通道被关闭,并且没有更多的数据可供接收时,range循环会自动结束。

使用常规接收语法从channel接收数据

ch := make(chan int, 2)

// 发送两个数据到channel
ch <- 1
ch <- 2
close(ch) // 关闭channel

// 使用常规接收语法从channel接收数据
for {
    value, ok := <-ch
    if !ok {
        fmt.Println("Channel closed!")
        break
    }
    fmt.Println("Received:", value)
}

通道与select

package main

import (
	"fmt"
	"time"
)

func networkRequest(ch chan string) {
	// 模拟网络请求
	time.Sleep(2 * time.Second)
	ch <- "Network Response"
}

func databaseQuery(ch chan string) {
	// 模拟数据库查询
	time.Sleep(3 * time.Second)
	ch <- "Database Query Result"
}

func main() {
	networkCh := make(chan string)
	databaseCh := make(chan string)

	// 启动两个协程执行网络请求和数据库查询
	go networkRequest(networkCh)
	go databaseQuery(databaseCh)

	// 使用 select 等待并处理通道操作
	select {
	case networkResponse := <-networkCh:
		fmt.Println("Received:", networkResponse)
	case databaseResult := <-databaseCh:
		fmt.Println("Received:", databaseResult)
	case <-time.After(4 * time.Second):
		fmt.Println("Timeout: No response received")
	}

	fmt.Println("Program finished")
}

select语句可以同时等待多个通道操作,使得goroutine可以在多个通信操作上进行等待。

  • select会阻塞,直到其中一个通道操作可以执行。
  • 如果有多个通道同时就绪,select会随机选择一个执行。
  • select可以与default子句一起使用,确保非阻塞。

三种不同的定时器使用

package main

import (
	"fmt"
	"time"
)

func main() {
	// 方式1: 使用 time.Timer 实现一次性延时任务
	timer1 := time.NewTimer(2 * time.Second)
	fmt.Println("Waiting for timer1 to expire...")
	<-timer1.C
	fmt.Println("Timer1 expired!")

	// 方式2: 使用 time.After 实现超时操作
	select {
	case <-time.After(3 * time.Second):
		fmt.Println("Timed out (from time.After)")
		// 在超时时执行任务
		// 例如:取消某个操作或处理超时情况
	}

	// 方式3: 使用 time.Ticker 定期执行任务
	ticker := time.NewTicker(1 * time.Second)
	fmt.Println("Starting ticker...")
	for i := 0; i < 5; i++ {
		<-ticker.C
		fmt.Println("Tick")
		// 在这里执行你的定期任务
		// 例如:定时记录系统状态
	}
	ticker.Stop()
	fmt.Println("Ticker stopped.")
}

sync.WaitGroup

package main

import (
	"fmt"
	"sync"
	"time"
)

func worker(id int, wg *sync.WaitGroup) {
	defer wg.Done() // 在函数退出时减少计数器

	fmt.Printf("Worker %d started\n", id)
	time.Sleep(2 * time.Second)
	fmt.Printf("Worker %d completed\n", id)
}

func main() {
	var wg sync.WaitGroup

	for i := 1; i <= 3; i++ {
		wg.Add(1)          // 增加计数器
		go worker(i, &wg)  // 启动goroutine
	}

	wg.Wait() // 阻塞等待所有goroutine完成

	fmt.Println("All workers have completed.")
}

互斥锁

package main

import (
	"fmt"
	"sync"
	"time"
)

var sharedResource int
var mutex sync.Mutex

func increment() {
	mutex.Lock()         // 锁定互斥锁
	defer mutex.Unlock() // 确保解锁
	sharedResource++
}

func main() {
	var wg sync.WaitGroup
	iterations := 5

	for i := 0; i < iterations; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			increment()
		}()
	}

	wg.Wait() // 等待所有goroutine完成

	fmt.Printf("Shared resource value: %d\n", sharedResource)
}

Mutex 用于保护共享资源的访问,sync.WaitGroup 用于等待 goroutines 的完成,channel 用于在 goroutines 之间传递数据和信号,而 select 用于选择不同的通道操作。这些工具可以根据需求结合使用,以实现复杂的并发控制和通信。

优雅地关闭 goroutines 和通道

使用 select 和通道关闭信号

func worker(done chan bool) {
    defer func() {
        // 在函数结束时通知关闭
        done <- true
    }()
    
    // 执行工作任务
    // ...
}

func main() {
    done := make(chan bool)
    
    go worker(done)
    
    // 主程序等待关闭信号
    <-done
}

使用 context

import (
    "context"
    "fmt"
    "time"
)

func worker(ctx context.Context) {
    select {
    case <-ctx.Done():
        fmt.Println("Worker received cancellation signal")
        // 执行清理工作
    case <-time.After(2 * time.Second):
        fmt.Println("Worker completed successfully")
        // 执行工作任务
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel() // 在不需要时取消 context
    
    go worker(ctx)
    
    // 等待一段时间后取消
    time.Sleep(1 * time.Second)
    cancel() // 发送取消信号
    time.Sleep(1 * time.Second)
}

使用 range 遍历通道

func main() {
    ch := make(chan int)
    
    go func() {
        defer close(ch) // 关闭通道
        for i := 0; i < 5; i++ {
            ch <- i
        }
    }()
    
    for num := range ch {
        fmt.Println(num)
    }
}

使用 sync.WaitGroup

import (
    "fmt"
    "sync"
    "time"
)

func worker(wg *sync.WaitGroup) {
    defer wg.Done()
    
    // 执行工作任务
    time.Sleep(1 * time.Second)
}

func main() {
    var wg sync.WaitGroup
    
    for i := 0; i < 3; i++ {
        wg.Add(1)
        go worker(&wg)
    }
    
    wg.Wait() // 等待所有 goroutines 完成
    fmt.Println("All workers have completed")
}