Golang环境
-
下载Golang:
- 访问Golang的官方网站 golang.org。
- 根据操作系统(Windows, macOS, Linux)选择合适的安装包下载。
-
安装Golang:
- 对于Windows:
- 运行下载的安装程序并遵循指示完成安装。
- 安装程序通常会自动设置环境变量(如
GOPATH
和GOROOT
)。
- 对于macOS和Linux:
- 使用包管理器(如Homebrew for macOS)或通过解压下载的压缩文件并执行安装脚本。
- 例如,在Linux上,您可以解压下载的文件到
/usr/local
,然后将/usr/local/go/bin
添加到您的PATH
环境变量中。
- 对于Windows:
-
配置环境变量:
- 确保
GOPATH
和GOROOT
环境变量被正确设置。GOROOT
是Go安装的位置,而GOPATH
是您的工作目录(您的Go代码和依赖库存放的位置)。 - 通常,现代版本的Go会自动设置这些变量。但如果遇到问题,您可能需要手动配置。
- 确保
-
验证安装:
- 打开命令行或终端。
- 输入
go version
来验证Go是否已经正确安装。这应该会显示安装的Go版本。 - 输入
go env
可以查看Go的环境配置。
-
搭建一个简单的工作区:
- 创建一个新的目录作为您的工作空间(例如:
~/go
)。 - 在这个目录下创建子目录
src
,在src
下编写您的Go代码。
- 创建一个新的目录作为您的工作空间(例如:
-
开始编写Go代码:
- 在
src
目录下创建您的第一个Go文件(如hello.go
)。 - 使用文本编辑器编写一个简单的Hello World程序。
- 在命令行中运行
go run hello.go
来运行您的程序。
- 在
main函数
package main import "fmt" func main() { fmt.Println("Hello, world!") }
-
包声明(Package Declaration):
- 每个Go文件都以包声明开始,这里是
package main
。main
包是一个特殊的包,它告诉Go编译器这个程序是可执行的,而不是一个库。
- 每个Go文件都以包声明开始,这里是
-
导入语句(Import Statement):
import "fmt"
这行代码导入了Go的标准库fmt
,它包含格式化I/O(输入/输出)的函数。这里使用它来打印文本到标准输出。
-
main函数(Main Function):
func main() {...}
定义了程序的入口点。Go程序从main
函数开始执行。- 在Go中,函数使用
func
关键字声明。这里的main
函数没有参数也没有返回类型。
-
打印输出(Print Statement):
fmt.Println("Hello, world!")
这行代码使用了fmt
包的Println
函数来打印一行文本到标准输出。这里它打印了经典的“Hello, world!”消息。
四种常见的变量声明方式
-
使用
var
关键字带明确类型:var a int a = 5
-
使用
var
关键字带初始值(类型推断):var b = 10
-
短变量声明(类型推断):
- 这是在函数内部声明局部变量的简洁方式。
c := 15
-
声明一个常量:
- 常量使用
const
关键字,常量的值在编译时已确定,且不能改变。
const d = 20
- 常量使用
多变量声明
-
多变量同时声明:
var e, f int e = 25 f = 30
-
多变量同时声明并初始化:
var g, h = 35, 40
-
短变量声明多个变量:只能在函数内部使用。
i, j := 45, 50
-
同时声明多个常量:
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 语言中的函数示范时,以下是一些常见的示例,涵盖了不同类型的函数用途和功能:
- 基本函数示例:定义一个简单的函数,用于执行加法操作并返回结果。
package main import "fmt" // 定义一个加法函数 func add(a, b int) int { return a + b } func main() { result := add(3, 5) fmt.Println("3 + 5 =", result) }
- 函数返回多个值:示范一个函数,返回多个值,如商和余数。
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) }
- 匿名函数:示范创建和使用匿名函数。
package main import "fmt" func main() { // 创建并调用匿名函数 result := func(a, b int) int { return a * b }(3, 4) fmt.Println("3 * 4 =", result) }
- 闭包:示范使用闭包,其中一个函数返回另一个函数。
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) }
- 可变参数函数:示范定义可接受可变数量参数的函数。
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() // 确保在函数结束时关闭文件
值类型和引用类型
值类型的变量直接存储值,它们的值存储在栈上。当这些变量被赋值给另一个变量时,或者作为参数传递给函数时,实际上是对值进行了拷贝。
常见的值类型包括:
-
基本数据类型:
int
、float
、bool
、string
等 -
复合数据类型:
array
、struct
等 -
值的赋值和传递是独立的副本。
-
修改副本不会影响原始数据。
代码示例:
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中一些常见类型的零值:
- 布尔型(
bool
):- 零值为
false
。
- 零值为
- 数值类型(整型
int
、浮点型float64
等):- 零值为
0
。
- 零值为
- 字符串(
string
):- 零值为空字符串
""
。
- 零值为空字符串
- 指针(
*T
):- 零值为
nil
,表示没有指向任何对象。
- 零值为
- 切片(
[]T
):- 零值为
nil
,表示没有分配内存。
- 零值为
- 映射(
map[K]V
):- 零值为
nil
,表示没有分配内存。注意,nil
映射不能被赋值。
- 零值为
- 通道(
chan T
):- 零值为
nil
,表示没有分配内存。
- 零值为
- 接口(
interface
):- 零值为
nil
,表示没有类型和值。
- 零值为
- 数组(
[N]T
):- 数组的零值是其元素类型的零值构成的数组。
- 结构体(
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
函数的特点和调用流程如下:
-
初始化顺序:
- 包的初始化顺序是按照它们被导入的顺序进行的。
- 首先初始化被导入的包,然后才是导入它们的包。
-
无参数、无返回值:
init
函数没有参数也没有返回值。
func init() { // 初始化代码 }
-
自动调用:
init
函数在包首次被加载时自动调用,无需手动调用。- 每个包可以有多个
init
函数,它们按照在代码中出现的顺序依次执行。
-
用途:
- 常用于执行包级别的初始化任务,如设置全局变量、注册、运行一次性计算等。
-
执行顺序:
- 若一个包被多个其他包导入,它的
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]
使用for
或for 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
函数中发生了panic
。main
函数中的defer
语句用于捕获并恢复panic
,以便程序可以正常继续运行。
接口(interfaces)
-
接口(Interfaces):
接口是一组方法签名的集合。当一个类型为接口中的所有方法提供定义时,它被称为实现了该接口。 -
类型(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.Type
和reflect.Value
。
reflect.Type
用于表示一个Go值的类型。reflect.Value
用于表示一个Go值。
使用reflect.TypeOf()
和reflect.ValueOf()
函数来获取任意对象的Type
和Value
。
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
,你可以获取关联值的原始类型。使用Value
的Interface()
方法可以返回一个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
标签。
Name
和Age
字段在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的零值是
nil
。nil
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的基本模型
- 轻量级:goroutine是轻量级的,它们的创建和销毁的开销远小于传统的线程。这允许程序同时运行成千上万的goroutine。
- 非抢占式多任务处理:在Go语言中,goroutines在用户空间中被多路复用(multiplexed)到少量的操作系统线程上,而不是像传统的线程那样与操作系统线程一一对应。
- 独立的堆栈:每个goroutine都有自己独立的堆栈,这个堆栈可以根据需要动态地增长和缩减。
- 通道(Channel):goroutine之间主要通过通道(channel)进行通信和同步。通道可以安全地在多个goroutine之间传递数据,避免了传统线程编程中常见的数据竞争和锁问题。
调度设计策略
Go语言的运行时包含了自己的调度器,这个调度器使用了一种称为M:P:G
的调度模型:
- M(Machine):代表操作系统的线程。
- P(Processor):代表逻辑处理器,其数量可配置,通常设置为CPU核心数。每个P都有一个本地运行队列,存放待运行的goroutines。
- 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") }