当前位置: 首页 > article >正文

Go 语言 | 入门 | 快速入门

快速入门

1.第一份代码

先检查自己是否有正确下载 Go,如果没有直接去 Go 安装 进行安装。

# 检查是否有 Go
$ go version
go version go1.23.4 linux/amd64

然后根据 Go 的入门教程 开始进行学习。

# 初始化 Go 项目
$ mkdir example && cd example # Go 会在 example 目录下创建一个 go.mod 文件, 并将模块路径设置为 example/hello

$ go mod init example/hello 
go: creating new go.mod: module example/hello

$ ls -al
总计 16
drwxrwxr-x  3 ljp ljp 4096  23 23:24 .
drwxrwxr-x 15 ljp ljp 4096  127 23:44 ..
-rw-rw-r--  1 ljp ljp   32  23 23:24 go.mod

$ cat go.mod
module example/hello

go 1.23.4

$ ll -al hello/
总计 8.0K
drwxrwxr-x 2 ljp ljp 4.0K  23 23:21 .
drwxrwxr-x 3 ljp ljp 4.0K  23 23:24 ..

go mod init example/hello 是用来在 example/ 下初始化一个 Go 项目的模块的命令:

  • go mod:这是 Go 1.11 引入的模块系统的命令,用于管理项目的依赖
  • init:该命令用于创建一个新的模块并初始化 go.mod 文件,go.mod 文件是 Go 项目的模块定义文件,用于记录模块的依赖信息和 Go 版本等信息
  • example/hello:这是您为某一个模块指定的名称。通常模块名称是一个符合 Go 命名规则的路径,也可能是一个 Git 仓库地址(比如 github.com/user/repo)或者本地路径(比如 example/hello
# 编写第一份代码
$ vim hello.go && cat hello.go
package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}

$ go run .
Hello, World!

$ go run hello.go
Hello, World!

简单说一下关于上述代码的一些重点。

  • 第一行代码 package main 定义了包名。您必须在源文件中非注释的第一行指明这个文件属于哪个包。package main 表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包
  • 下一行 import "fmt" 告诉 Go 编译器这个程序需要使用 fmt 包(的函数或其他元素),fmt 包实现了格式化 IO(输入/输出)的函数(也支持通过 + 实现字符串连接)
  • 下一行 func main() 是程序开始执行的函数。main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)
  • 单行注释是最常见的注释形式,您可以在任何地方使用以 // 开头的单行注释。多行注释也叫块注释,均已以 /* 开头,并以 */ 结尾,且不可以嵌套使用,多行注释一般用于包的文档描述或注释成块的代码片段
  • 下一行 fmt.Println(...) 可以将字符串输出到控制台,并在最后自动增加换行字符 \n。使用 fmt.Print("hello, world\n") 可以得到相同的结果。Print()Println() 这两个函数也支持使用变量,如:fmt.Println(arr)。如果没有特别指定,它们会以默认的打印格式将变量 arr 输出到控制台。
  • 另外标识符的大小写是有说法的,大小写决定了包内外的可见性
    • 标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如 Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public
    • 标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 protected
  • Go 程序中,一行代表一个语句结束。每个语句不需要像 C 家族中的其它语言一样以分号 ; 结尾,因为这些工作都将由 Go 编译器自动完成。如果您打算将多个语句写在同一行,它们则必须使用 ; 人为区分,但在实际开发中我们并不鼓励这种做法

[!IMPORTANT]

补充:不过其实就算是没有初始化项目,只引入 Go 内部模块(例如 fmt)的情况下,一个单独的 .go 文件也可以使用 go run xxx.go 运行起来。

go 的运行方式有两种,一种是即时编译运行,一种是执行编译后的可执行文件。

# 编译 Go 程序
$ go build -o hello.exe hello.go

$ ls -al
总计 2100
drwxrwxr-x 2 ljp ljp    4096  24 00:13 .
drwxrwxr-x 3 ljp ljp    4096  23 23:49 ..
-rw-rw-r-- 1 ljp ljp      32  24 00:12 go.mod
-rwxrwxr-x 1 ljp ljp 2130759  24 00:13 hello.exe
-rw-rw-r-- 1 ljp ljp      77  24 00:13 hello.go

$ ./hello.exe
Hello, World!

接下来尝试引入外部的包,相关的包可以在 pkggodev 上搜索,例如搜索 quote

在这里插入图片描述

# 修改代码以使用外部的模块
$ vim hello.go && cat hello.go
package main

import "fmt"
import "rsc.io/quote" // 引入外部依赖

func main() {
    fmt.Println(quote.Go())
}

$ go mod tidy # 会自动下载 rsc.io/quote 包并更新 go.mod 文件

go: finding module for package rsc.io/quote
go: downloading rsc.io/quote v1.5.2
go: found rsc.io/quote in rsc.io/quote v1.5.2
go: downloading rsc.io/sampler v1.3.0
go: downloading golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c

$ ls -al
总计 2.1M
drwxrwxr-x  3 ljp ljp 4.0K  23 23:42 .
drwxrwxr-x 15 ljp ljp 4.0K  127 23:44 ..
-rw-rw-r--  1 ljp ljp  175  23 23:42 go.mod
-rw-rw-r--  1 ljp ljp  499  23 23:42 go.sum # 多出该文件
-rwxrwxr-x  1 ljp ljp 2.1M  23 23:36 hello.exe
-rw-rw-r--  1 ljp ljp   95  23 23:39 hello.go

$ cat go.mod
module example/hello

go 1.23.4

require rsc.io/quote v1.5.2

require (
        golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c // indirect
        rsc.io/sampler v1.3.0 // indirect
)

$ go run hello.go
Don't communicate by sharing memory, share memory by communicating.

go.sum 文件在 Go 项目中扮演着重要的角色,它用于记录项目依赖的 模块校验和,确保模块在下载时的完整性和一致性。防止恶意代码和不一致的版本被引入项目中,确保依赖项的安全和可重现性。

2.数据类型

2.1.分类

一般来说,Go 使用 var 来定义变量,并且会根据手动赋予的初始值来确定变量类型,如果不赋予初始值则需要显式声明变量的类型,并且这样做会赋予一个默认值(不过哪怕是手动赋予了初始值也可以显式声明变量类型)。Go 的类型分为四种,和其他的语言(尤其是类 C 家族的语言)非常类似。

类型描述
布尔型布尔类型 bool 可以是常量 true 或者 false
数字类型数字类型有两种,整数型 int(可分为 uint64、uint32、uint16、uint8、int8、int16、int32、int64)、浮点型(可分为 float32、float64、complex64、complex128),其中位运算采用补码进行运行。
字符串类型字符串类型 string,字符串就是一串固定长度的字符连接起来的字符序列,Go 的字符串是由单个字节连接起来的。Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本。
派生类型派生类型包括:(a)指针类型 (b)数组类型 ©结构类型 (d)通道类型 (e)函数类型 (f)切片类型 (g)接口类型 (h)键值对类型。

此外还可以自己定义新的类型,也就是使用结构体,结构体需要使用 type ... struct {} 来定义。并且通常使用 {} 语法来初始化,也可以采用类似 Python{ key: value, ... } 的键值对方式来初始化,忽略赋值的结构体字段将为默认值。

[!WARNING]

注意:使用 . 就可以访问结构体的成员,Go 没有 -> 这种符号。

[!WARNING]

注意:Go 没有 class 这种类语法。

2.2.变量

// 展示大部分的数据类型
package main

import (
	"fmt"
)

type Person struct {
	Name string
	Age  int
}

func main() {
	// 布尔类型
	var isGoFun bool = true
	fmt.Println("布尔类型:", isGoFun)

	// 数字类型
	var i int = 42
	var f float64 = 3.1415
	var c complex128 = complex(1, 2) // 这定义了一个复数
	fmt.Println("整型:", i)
	fmt.Println("浮点型:", f)
	fmt.Println("复数:", c)

	// 字符串类型
	var str string = "Hello, Go!"
	fmt.Println("字符串:", str)

    // 复合类型
    // (1)指针类型
	var ptr *int = &i
	fmt.Println("指针:", ptr, "指向的值:", *ptr)

    // (2)数组类型
	var arr [3]int = [3]int{1, 2, 3}
	fmt.Println("数组:", arr)

    // (3)结构体类型
	var p Person = Person{Name: "Alice", Age: 25}
	fmt.Println("结构体:", p)

    // (4)切片类型(动态数组)
	var slice []int = []int{4, 5, 6}
	fmt.Println("切片:", slice)

    // (5)Map 类型(键值对)
	var m map[string]int = map[string]int{"apple": 5, "banana": 10}
	fmt.Println("Map:", m)

    // (6)函数类型
	var add func(a, b int) int
	add = func(a, b int) int { return a + b }
	fmt.Println("函数类型: 3 + 7 =", add(3, 7))
}

[!IMPORTANT]

补充:我们知道可以在变量的初始化时省略变量的类型而由系统自动推断,声明语句写上 var 关键字其实是显得有些多余了,因此我们可以将它们简写为 a := 50b := false 这些形式,等价于 var a = 50var b = false

[!IMPORTANT]

补充:Go 允许像 Python 在一行定义或赋值多个变量(并行赋值)。因此如果您想要快速交换两个变量的值,则可以简单地使用 a, b = b, a,但是两个变量的类型必须是相同。并行赋值也被用于当一个函数返回多个返回值时,比如 val, err = Func(var)

[!IMPORTANT]

补充:空白标识符 _ 也被用于抛弃值,如在 _, b = 5, 75 被抛弃。_ 实际上是一个只写变量,您不能得到它的值。有时您会遇到并不需要使用从一个函数得到的所有返回值的情况,这个时候 _ 会非常有用。

[!IMPORTANT]

补充:切片其实就是一种强悍的动态数组。

[!WARNING]

注意:有几种类型我没有给出,后面慢慢研究。

[!CAUTION]

警告:如果在相同的代码块中,我们不可以再次对于相同名称的变量使用初始化声明 :=,这会出现编译错误,但是可以给相同的变量赋予 = 一个新的值。

[!CAUTION]

警告:如果您声明了一个局部变量却没有在相同的代码块中使用它,同样会得到编译错误,但是全局变量则不会编译失败。

在这里插入图片描述
在这里插入图片描述

值类型:所有像 int、float、booltring… 这些基本类型都属于值类型,使用这些类型的变量直接指向存在内存中的值。

当使用等号 = 将一个变量的值赋值给另一个变量时,如:j = i,实际上是在内存中将 i 的值进行了拷贝。

您可以通过 &i 来获取变量 i 的内存地址,例如:0xf840000040(每次的地址都可能不一样)。

内存地址会根据机器的不同而有所不同,甚至相同的程序在不同的机器上执行后也会有不同的内存地址。因为每台机器可能有不同的存储器布局,并且位置分配也可能不同。

在这里插入图片描述

引用类型:更复杂的数据通常会需要使用多个字,这些数据一般使用引用类型保存。一个引用类型的变量 r1 存储的是 r1 的值所在的内存地址(数字),或内存地址中第一个字所在的位置。

这个内存地址称之为指针,这个指针实际上也被存在另外的某一个值中。

同一个引用类型的指针指向的多个字可以是在连续的内存地址中(内存布局是连续的),这也是计算效率最高的一种存储形式;也可以将这些字分散存放在内存中,每个字都指示了下一个字所在的内存地址。

当使用赋值语句 r2 = r1 时,只有引用(地址)被复制。如果 r1 的值被改变了,那么这个值的所有引用都会指向被修改后的内容,在这个例子中,r2 也会受到影响。

2.3.常量

Go 语言中也有常量 const 的概念,是指程序运行中不会被修改的值。可以一次性定义多个常量,也可以干脆结合 () 定义一个枚举值量。

// 使用常量
package main

import "fmt"

func main() {
    const val1, val2, val3 int = 1, 2, 3
    fmt.Println(val1, val2, val3) // 这里可以注意到一个特性, 在打印时会自动空格待打印的多个变量值

    const (
        v1 = 4
        v2 = 5 // 如果这里没有写 '= 5' 那么 v2 默认和 v1 值相同
        v3 = 6
    )
    fmt.Println(v1, v2, v3)
}

iota 是一个特殊的常量,可以认为是一个可以被编译器修改的常量。iotaconst 关键字出现时将被重置为 0const 中每新增一行常量声明将使 iota 计数一次(可理解为 const 语句块中的行索引)。

// 使用枚举常量
package main

import "fmt"

func main() {
    const (
        a = iota   // 0, iota += 1
        b          // 1, iota += 1
        c          // 2, iota += 1
        d = "ha"   // 设置独立值 "ha", iota += 1
        e          // "ha", iota += 1
        f = 100    // 设置独立值 100, iota +=1
        g          // 100, iota +=1
        h = iota   // 7, iota +=1
        i          // 8, iota +=1
    )
    
    fmt.Println(a, b, c, d, e, f, g, h, i) // 0 1 2 ha ha 100 100 7 8
}

[!IMPORTANT]

补充:在 const 中可以使用 iota 进行正常的运算,犹如对普通常量进行计算。

2.4.范围

谈及变量就需要考虑变量的作用域,变量可以在三个地方声明。

  • 函数内定义的变量称为局部变量
  • 函数外定义的变量称为全局变量
  • 函数定义中的变量称为形式参数

2.5.转换

Go 语言类型转换基本格式如下,和 Python 以及现代的 Cpp 类似。

type_name(expression)
// 使用类型转换
package main

import "fmt"

func main() {
   var sum int = 17
   var count int = 5
   var mean float32
   
   mean = float32(sum)/float32(count)
   fmt.Printf("mean 的值为: %f\n",mean)
}

不过有一类比较的特殊的转化是字符串和数字值的互相转化。

// 字符串和数字值的互相转化
package main

import (
	"fmt"
	"strconv"
)

func main() {
	// 字符串转整数
	str := "123"
	num, err := strconv.Atoi(str)
	if err != nil {
		fmt.Println("转换错误:", err)
	} else {
		fmt.Printf("字符串 '%s' 转换为整数为:%d\n", str, num)
	}

	// 整数转字符串
	num2 := 456
	str2 := strconv.Itoa(num2)
	fmt.Printf("整数 %d 转换为字符串为:'%s'\n", num2, str2)

	// 字符串转浮点数
	str3 := "3.1415"
	num3, err := strconv.ParseFloat(str3, 64)
	if err != nil {
		fmt.Println("转换错误:", err)
	} else {
		fmt.Printf("字符串 '%s' 转换为浮点数为:%f\n", str3, num3)
	}

	// 浮点数转字符串
	num4 := 3.1415
	str4 := strconv.FormatFloat(num4, 'f', 2, 64)
	fmt.Printf("浮点数 %f 转换为字符串为:'%s'\n", num4, str4)
}

3.控制程序流

C 语言类似,不过有一些简化和加强。首先 GoPython 类似去掉了冗余的 () 来放置布尔表达式,但仍需要 {},可以用一个代码来解释清楚。

package main

import (
	"fmt"
	"time"
)

func main() {
	// if 语句
	num := 10
	if num > 5 {
		fmt.Println("num 大于 5")
	}

	// if...else 语句
	if num%2 == 0 {
		fmt.Println("num 是偶数")
	} else {
		fmt.Println("num 是奇数")
	}

	// 嵌套 if 语句
	if num > 0 {
		fmt.Println("num 是正数")
		if num%5 == 0 {
			fmt.Println("num 还是 5 的倍数")
		}
	}
    
	// 多个 else if 语句
	score := 85
    
	if score >= 90 {
		fmt.Println("成绩等级: A")
	} else if score >= 80 {
		fmt.Println("成绩等级: B")
	} else if score >= 70 {
		fmt.Println("成绩等级: C")
	} else if score >= 60 {
		fmt.Println("成绩等级: D")
	} else {
		fmt.Println("成绩等级: F (不及格)")
	}

	// switch 语句
	day := time.Now().Weekday()
	switch day {
	case time.Monday:
		fmt.Println("今天是星期一")
	case time.Tuesday:
		fmt.Println("今天是星期二")
	case time.Wednesday, time.Thursday:
		fmt.Println("今天是星期三或星期四")
	default:
		fmt.Println("今天是周末或其他时间")
	}

	// for 循环
	fmt.Println("普通 for 循环:")
	for i := 0; i < 5; i++ {
		fmt.Println(i)
	}
}

而其实这里我说的所谓“加强”仅仅是指多了个 select,这种控制流有些类似 switch,但需要和多并发的代码结合使用,这点我将会在语言特性中进行讲解。

[!IMPORTANT]

补充:Go 也继承了 Cbreak、continue、goto 三个关键字,并且使用方法一样。

[!WARNING]

警告:Go 没有三目运算符,所以不支持 ?: 形式的条件判断。

[!WARNING]

警告:Go 也没有 while 循环关键字,只有 for

4.运算符

Go 的运算符几乎完美继承了 C 的特色,没啥好讲的…

5.函数

5.1.内置函数

len(), cap(), unsafe.Sizeof() 在 Go 语言中分别用于不同的目的,是内置的函数,无需引入任何的模块即可使用。

5.1.1.len()

返回数组、切片、字符串、映射、通道等量的长度(或元素个数)。

package main

import "fmt"

func main() {
    arr := [5]int{1, 2, 3, 4, 5}
    slice := []int{1, 2, 3, 4}
    str := "Hello, 世界"
    m := map[string]int{"a": 1, "b": 2}

    fmt.Println("数组长度:", len(arr)) // 5
    fmt.Println("切片长度:", len(slice)) // 4
    fmt.Println("字符串长度:", len(str)) // 13 (UTF-8 编码,每个中文占 3 字节)
    fmt.Println("键值对长度:", len(m)) // 2
}
5.1.2.cap()

返回数组、切片或通道的容量(底层分配的存储空间大小)。

package main

import "fmt"

func main() {
    s := make([]int, 3, 10) // make() 可以设置切面的长度为 3,容量 10
    fmt.Println("len(s):", len(s)) // 3
    fmt.Println("cap(s):", cap(s)) // 10
}

[!IMPORTANT]

补充:另外在 Go 的模块中有专门的函数可以静态计算一个变量的内存占用(计算出字节的个数,类似 Csizeof())。

5.2.自定函数

5.2.1.普通用法

函数是基本的代码块,用于执行一个任务,Go 语言最少有个 main()Go 语言函数定义格式如下。

// 定义一个函数的模板
func function_name( [parameter_list] ) [return_types] {
   函数体
}

  • func:函数由 func 开始声明,至少您可以认为 function_name 是一种函数变量
  • function_name:函数名称,参数列表和返回值类型构成了函数签名
  • parameter_list:参数列表,参数就像一个占位符,当函数被调用时,您可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参数
  • return_types:返回类型,函数返回一列值。return_types 是该列值的数据类型。有些功能不需要返回值,这种情况下 return_types 不是必须的
  • 函数体:函数定义的代码集合
// 函数返回两个数的最大值
package main

import "fmt"

func main() {
   // 定义局部变量
   var a int = 100
   var b int = 200
   var ret int

   // 调用函数并返回最大值
   ret = max(a, b)

   fmt.Printf( "最大值是 : %d\n", ret )
}

func max(num1, num2 int) int { // 这个定义也可以放在 main() 后, 并且不用像 C 语言一样需要先声明函数后才能使用
   // 声明局部变量
   var result int

   if (num1 > num2) {
      result = num1
   } else {
      result = num2
   }

   return result
}

就像前面说的那样,Go 的函数可以返回多个返回值,并且使用并行赋值来获取。

package main

import "fmt"

func swap(x, y string) (string, string) {
   return y, x
}

func main() {
   a, b := swap("Google", "limou")
   fmt.Println(a, b)
}

不过我们需要讨论一个值得注意的问题,就是传递给函数的参数究竟是怎么传递的。普通变量、结构体变量、数组变量都使用值传递,在函数内修改传递过来的普通变量、结构体变量、数组变量是不会影响传递前的量。而其他引用变量则会使用引用传递,在函数内修改传递过来的引用变量会影响传递前的量。

package main

import "fmt"

// 传递普通变量(值传递)
func modifyInt(x int) {
    x = 100
}

// 传递结构体(值传递)
type Person struct {
    name string
    age  int
}

func modifyStruct(p Person) {
    p.age = 30
}

// 传递切片(引用传递)
func modifySlice(s []int) {
    s[0] = 100
}

// 传递映射(引用传递)
func modifyMap(m map[string]int) {
    m["age"] = 30
}

func main() {
    // 普通变量
    num := 10
    modifyInt(num)
    fmt.Println("num:", num) // 仍然是 10(值传递)

    // 结构体
    p := Person{name: "Alice", age: 25}
    modifyStruct(p)
    fmt.Println("p.age:", p.age) // 仍然是 25(值传递)

    // 切片
    slice := []int{1, 2, 3}
    modifySlice(slice)
    fmt.Println("slice:", slice) // [100 2 3](引用传递)

    // 映射
    myMap := map[string]int{"age": 25}
    modifyMap(myMap)
    fmt.Println("myMap:", myMap) // map[age:30](引用传递)
}

5.2.2.高阶用法
函数用法描述
回调函数定义后可作为另外一个函数的实参数传入
闭包闭包是匿名函数,可在动态编程中使用
方法方法就是一个包含了接受者的函数
递归函数自己调用自己
// 回调
package main

import "fmt"

// 定义一个函数, 参数是另一个函数
func operate(a, b int, op func(int, int) int) int {
    return op(a, b) // 调用传入的函数
}

// 具体的函数实现
func add(x, y int) int {
    return x + y
}

func multiply(x, y int) int {
    return x * y
}

func main() {
    fmt.Println(operate(3, 4, add)) // 7
    fmt.Println(operate(3, 4, multiply)) // 12
}

// 闭包
package main

import "fmt"

// 返回一个函数, 内部引用了外部变量
func counter() func() int {
    count := 0
    return func() int {
        count++ // 外部变量 count 并且捕获并存储, 即使函数执行完毕, 变量依然客观存在
        return count
    }
}

func main() {
    c := counter() // 创建闭包
    fmt.Println(c()) // 1
    fmt.Println(c()) // 2
    fmt.Println(c()) // 3

    // 重新创建新的闭包, count 变量重新初始化
    d := counter()
    fmt.Println(d()) // 1
}

// 方法
package main

import "fmt"

// 定义结构体
type Person struct {
    name string
    age  int
}

// 绑定方法, 值接收者
func (p Person) greet() {
    fmt.Println("Hello, my name is", p.name)
}

// 绑定方法, 指针接收者(可修改原始数据)
func (p *Person) growUp() {
    p.age++
}

func main() {
    p1 := Person{"Alice", 25}
    p1.greet() // Hello, my name is Alice

    p1.growUp()
    fmt.Println("p1.age:", p1.age) // 26
}

// 递归
package main

import "fmt"

func Factorial(n uint64)(result uint64) {
    if (n > 0) {
        result = n * Factorial(n-1)
        return result
    }
    return 1
}

func main() {  
    var i int = 15
    fmt.Printf("%d 的阶乘是 %d\n", i, Factorial(uint64(i)))
}

[!IMPORTANT]

补充:Go 的闭包可以保持调用函数的状态,经常作为计数器或共享变量来用。

[!NOTE]

补充:Go 的方法有些像是语法糖,挺接近现代面向对象语言的实现原理。

6.数组和字符串

6.1.数组

Go 语言数组声明需要指定元素类型及元素个数,语法格式如下。

var arrayName [size]dataType
// 使用数组
package main

import "fmt"

func modifyArray(arr [5]int) {
    arr[0] = 100 // 修改的是副本, 不影响原数组
}

func main() {
    // 1.声明数组,默认初始化为 0
    var numbers [5]int
    fmt.Println("默认初始化的数组:", numbers)

    // 2.使用初始化列表初始化数组
    numbers = [5]int{1, 2, 3, 4, 5}
    fmt.Println("初始化列表赋值:", numbers)

    // 3.使用 := 简短声明并初始化数组
    nums := [5]int{10, 20, 30, 40, 50}
    fmt.Println("简短声明初始化:", nums)

    // 4.使用 ... 让编译器推断数组大小
    autoSize := [...]int{100, 200, 300, 400}
    fmt.Println("自动推断大小的数组:", autoSize)

    // 5.指定索引初始化
    indexed := [5]float32{1: 2.0, 3: 7.0}
    fmt.Println("指定索引初始化:", indexed)

    // 6.遍历数组
    fmt.Println("遍历数组 elements:")
    for i, val := range numbers { // 类似 Cpp 的范围 for 循环
        fmt.Printf("numbers[%d] = %d\n", i, val)
    }

    // 7.访问和修改数组元素
    numbers[2] = 99
    fmt.Println("修改后 numbers:", numbers)

    // 8.读取数组元素
    value := numbers[2]
    fmt.Println("读取 numbers[2]:", value)
    
    // 9.验证数组传递过程中是按值传递的
    a := [5]int{1, 2, 3, 4, 5}
    modifyArray(a)
    fmt.Println(a) // 输出 [1 2 3 4 5], 原数组未修改

    // 10.使用高维数组
    arr := [3][4]int{
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12},
    }
    fmt.Println(arr) // [[1 2 3 4] [5 6 7 8] [9 10 11 12]]
}

[!IMPORTANT]

补充:range 关键字,用于 for 循环中迭代数组、切片、通道、集合的元素。在数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value 对。

但是上面的数组的长度是固定的,在特定场景中不太好用,我们需要动态的数组,这歌时候就需要使用切片,也就是一种动态数组。

package main

import "fmt"

func main() {
	// 创建一个长度为 3,容量为 5 的切片
	var numbers = make([]int, 3, 5)
	printSlice(numbers)

	// 空切片
	var emptySlice []int // 不指定数组的大小就会变成创建切片的语法
	printSlice(emptySlice)

	// 判断切片是否为空
	if emptySlice == nil {
		fmt.Println("切片是空的")
	}

	// 创建一个切片并初始化
	numbers2 := []int{0, 1, 2, 3, 4, 5, 6, 7, 8}
	printSlice(numbers2)
	fmt.Println("numbers2 ==", numbers2)

	// 截取切片: 从索引 1 到 4 (不包括4)
	fmt.Println("numbers2[1:4] ==", numbers2[1:4])

	// 默认下限为 0
	fmt.Println("numbers2[:3] ==", numbers2[:3])

	// 默认上限为 len(slice)
	fmt.Println("numbers2[4:] ==", numbers2[4:])

	// 截取空切片
	numbers3 := make([]int, 0, 5) // 一个长度为 0, 容量为 5 的切片
	printSlice(numbers3)

	// 子切片从索引 0 到索引 2 (不包括2)
	numbers4 := numbers2[:2]
	printSlice(numbers4)

	// 子切片从索引 2 到索引 5 (不包括5)
	numbers5 := numbers2[2:5]
	printSlice(numbers5)

	// 使用 append() 向切片追加元素
	numbers = append(numbers, 0)
	printSlice(numbers)

	// 向切片追加一个元素
	numbers = append(numbers, 1)
	printSlice(numbers)

	// 向切片追加多个元素
	numbers = append(numbers, 2, 3, 4)
	printSlice(numbers)

	// 创建一个新的切片, 容量是原切片的两倍
	numbers1 := make([]int, len(numbers), (cap(numbers))*2)
	// 拷贝切片内容
	copy(numbers1, numbers)
	printSlice(numbers1)
}

// 打印切片的长度、容量和元素
func printSlice(x []int) {
	fmt.Printf("len=%d cap=%d slice=%v\n", len(x), cap(x), x)
}

6.2.字符串

Go 语言中使用 fmt.Sprintf()fmt.Printf() 格式化字符串并赋值给新串:

  • Sprintf() 根据格式化参数生成格式化的字符串并返回该字符串
  • Printf() 根据格式化参数生成格式化的字符串并写入标准输出

因此下面两段代码等价。

// 使用 Sprintf()
package main

import (
    "fmt"
)

func main() {
    // %d 表示整型数字,%s 表示字符串
    var stockcode = 123
    var enddate = "2020-12-31"
    var url = "Code=%d&endDate=%s"
    var target_url = fmt.Sprintf(url, stockcode, enddate)
    fmt.Println(target_url)
}

// 使用 Printf()
package main

import (
    "fmt"
)

func main() {
    // %d 表示整型数字,%s 表示字符串
    var stockcode = 123
    var enddate = "2020-12-31"
    var url = "Code=%d&endDate=%s"
    fmt.Printf(url, stockcode, enddate)
}

分别运行代码,输出结果均为 Code=123&endDate=2020-12-31

// 使用字符串
package main

import (
    "fmt"
    "strings" // 需要引入
)

func main() {
    // 1.定义字符串
    str1 := "Hello"
    str2 := "World"
    fmt.Println("原始字符串:", str1, str2)

    // 2.字符串拼接
    str3 := str1 + " " + str2 // 和 Cpp 相同
    fmt.Println("拼接字符串:", str3)

    str4 := fmt.Sprintf("%s, %s!", str1, str2) // 使用 fmt.Sprintf 拼接
    fmt.Println("格式化拼接:", str4)

    // 3.获取字符串长度
    fmt.Println("字符串长度:", len(str3))

    // 4.截取字符串(使用切片)
    substr := str3[0:5] // 截取前 5 个字符
    fmt.Println("截取的字符串:", substr)

    // 5.遍历字符串
    fmt.Println("遍历字符串:")
    for i, ch := range str3 {
        fmt.Printf("索引: %d, 字符: %c\n", i, ch)
    }

    // 6.查找字符串
    index := strings.Index(str3, "World")
    fmt.Println("查找子串 'World' 的索引:", index)

    // 7.判断字符串是否包含某个子串
    contains := strings.Contains(str3, "Hello")
    fmt.Println("是否包含 'Hello':", contains)

    // 8.统计某个字符出现次数
    count := strings.Count(str3, "o")
    fmt.Println("'o' 出现的次数:", count)

    // 9.替换字符串
    replaced := strings.Replace(str3, "World", "Go", 1)
    fmt.Println("替换后的字符串:", replaced)

    // 10.分割字符串
    splitStr := strings.Split(str3, " ")
    fmt.Println("分割字符串:", splitStr)
    
    // 11.修改字符串(Go 字符串是不可变的,需要转换成切片)
    strBytes := []rune(str3) // 转换为 rune 切片, 写成 []byte(str3) 可能会导致多字节字符被拆分, 这种适合处理 ASCII
    strBytes[0] = 'h' // 修改第一个字符
    modifiedStr := string(strBytes)
    fmt.Println("修改后的字符串:", modifiedStr)

    // 12.去除首尾空格
    trimmed := strings.TrimSpace("  Hello Go!  ")
    fmt.Println("去除空格:", trimmed)

    // 13.转换大小写
    fmt.Println("转换为大写:", strings.ToUpper(str3))
    fmt.Println("转换为小写:", strings.ToLower(str3))
}

[!WARNING]

注意:Go 的字符串默认只读无法被修改,需要被转化为切片后才可以修改。

7.语言特点

7.1.内存

// 使用指针
package main

import "fmt"

// 通过指针修改变量值
func modifyValue(ptr *int) {
    *ptr = 100 // 修改指针指向的值
}

// 指向指针的指针示例
func pointerToPointerExample() {
    var a int = 10
    var ptr *int = &a   // 指针
    var pptr **int = &ptr // 指向指针的指针

    fmt.Printf("变量 a 的值: %d\n", a)
    fmt.Printf("指针 ptr 存储的地址: %x\n", ptr)
    fmt.Printf("指针 ptr 指向的值: %d\n", *ptr)
    fmt.Printf("指针 pptr 存储的地址: %x\n", pptr)
    fmt.Printf("指针 pptr 指向的值: %d\n", **pptr)
}

func main() {
    // 1. 指针的基本使用
    var a int = 20
    var ptr *int

    ptr = &a // 赋值指针

    fmt.Printf("a 变量的地址: %x\n", &a)
    fmt.Printf("ptr 变量存储的地址: %x\n", ptr)
    fmt.Printf("ptr 指向的值: %d\n", *ptr)

    // 2. 空指针
    var nilPtr *int
    if nilPtr == nil {
        fmt.Println("nilPtr 是空指针")
    }

    // 3. 指针数组
    arr := [3]int{10, 20, 30}
    var ptrArr [3]*int // 指针数组
    for i := 0; i < 3; i++ {
        ptrArr[i] = &arr[i] // 存储地址
    }
    fmt.Println("指针数组的内容:")
    for i := 0; i < 3; i++ {
        fmt.Printf("ptrArr[%d] 存储的地址: %x, 值: %d\n", i, ptrArr[i], *ptrArr[i])
    }

    // 4. 指向指针的指针
    pointerToPointerExample()

    // 5. 通过指针修改值
    fmt.Printf("修改前的值: %d\n", a)
    modifyValue(ptr)
    fmt.Printf("修改后的值: %d\n", a)
}

指针让 Go 的性能可以有机会追上 C,并且不失去简洁的语法。有了指针可能就需要担心内存泄漏的问题,不过 Go 有独特的内存管理方案,Go 可以根据逃逸分析,自动分析一个变量该存储在栈空间还是堆空间,并且会做自动 GC

7.2.接口

接口是 Go 语言中的一种类型,用于定义行为的集合,它通过描述类型必须实现的方法,规定了类型的行为契约,因此经常用来实现多态。接口变量可以接受一个已经实现所有接口定义行为的结构体变量,然后就可以借助接口变量来调用接口的行为。

// 使用接口
package main

import (
        "fmt"
        "math"
)

// 定义接口
type Shape interface {
        Area() float64
        Perimeter() float64
}

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

// Circle 实现 Shape 接口
func (c Circle) Area() float64 {
        return math.Pi * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
        return 2 * math.Pi * c.Radius
}

func main() {
        c := Circle{Radius: 5}
        var s Shape = c // 接口变量可以存储实现了接口的类型
        fmt.Println("Area:", s.Area())
        fmt.Println("Perimeter:", s.Perimeter())
}

另外接口也可以被嵌套使用。

// 嵌套使用接口
package main

import "fmt"

type Reader interface {
    Read() string
}

type Writer interface {
    Write(data string)
}

type ReadWriter interface {
    Reader
    Writer
}

type File struct{}

func (f File) Read() string {
    return "Reading data"
}

func (f File) Write(data string) {
    fmt.Println("Writing data:", data)
}

func main() {
    var rw ReadWriter = File{}
    fmt.Println(rw.Read())
    rw.Write("Hello, Go!")
}

不过接口还有另外一个用法,空接口是所有类型的超集类型,是 Go 实现泛型编程的关键。

package main

import "fmt"

// 使用空接口处理不同类型的数据
func printValue(val interface{}) {
    fmt.Printf("Value: %v, Type: %T\n", val, val)
}

func main() {
    // 传递不同类型的数据到空接口
    printValue(42)           // int 类型
    printValue("hello")      // string 类型
    printValue(3.14)         // float64 类型
    printValue([]int{1, 2})  // []int(切片)类型

    // 类型断言示例
    var i interface{} = "hello"
    str, ok := i.(string)  // 带检查的类型断言
    if ok {
        fmt.Println("类型断言成功,值是:", str)  // 输出:hello
    } else {
        fmt.Println("类型断言失败")
    }

    // 错误的类型断言, 触发 panic
    var j interface{} = 42
    str2 := j.(string)  // 这会导致 panic,因为 j 是 int 类型
    fmt.Println(str2)   // 不会执行到这行
}

结合 switch 还能写出根据变量的不同类型来执行不同的逻辑。

// 根据变量的不同类型来执行不同的逻辑
package main

import "fmt"

func printType(val interface{}) {
    switch v := val.(type) {
        case int:
        fmt.Println("Integer:", v)
        case string:
        fmt.Println("String:", v)
        case float64:
        fmt.Println("Float:", v)
        default:
        fmt.Println("Unknown type")
    }
}

func main() {
    printType(42)
    printType("hello")
    printType(3.14)
    printType([]int{1, 2, 3})
}

[!IMPORTANT]

补充:接口变量实际上包含了两部分

  • 动态类型:接口变量存储的具体类型
  • 动态值:具体类型的值
// 动态值和动态类型
package main

import "fmt"

func main() {
    var i interface{} = 42
    fmt.Printf("Dynamic type: %T, Dynamic value: %v\n", i, i) // Dynamic type: int, Dynamic value: 42
}

[!IMPORTANT]

补充:接口的零值是 nil,当接口变量的动态值和动态类型都为 nil 时,接口变量为 nil

package main

import "fmt"

func main() {
   var i interface{}
   fmt.Println(i == nil) // 输出:true
}

7.3.并发

7.3.1.协程创建

Go 语言支持并发,通过 goroutineschannels 提供了一种简洁且高效的方式来实现并发。

  • Goroutines
    • Go 中的并发执行单位,类似于轻量级的线程
    • Goroutine 的调度由 Go 运行时管理,用户无需手动分配线程
    • 使用 go 关键字启动 Goroutine
    • Goroutine 是非阻塞的,可以高效地运行成千上万个 Goroutine
  • Channel
    • Go 中用于在 Goroutine 之间通信的机制
    • 支持同步和数据共享,避免了显式的锁机制
    • 使用 chan 关键字创建,通过 <- 操作符发送和接收数据
  • Scheduler
    • Go 的调度器基于 GMP 模型
      • G:内置协程(Goroutine
      • M:系统线程(Machine
      • P:逻辑处理器(Processor
    • 协程会先进入到 Go 自己的本地运行队列 Run Queue 中,准备就绪后逻辑处理器负责把内置协程交给系统线程进行执行,如果系统线程太多就会在运行时自动回收系统线程避免资源浪费(因此需要依靠专门的工具才可以直接查看协程的状态)
// 使用协程
// 以下代码存在两个协程 Goroutine
package main

import (
    "fmt"
    "time"
)

func sayHello() {
    for i := 0; i < 5; i++ {
        fmt.Println("Hello")
        time.Sleep(100 * time.Millisecond)
    }
}

func main() {
    go sayHello() // 启动协程 Goroutine
    for i := 0; i < 5; i++ {
        fmt.Println("Main")
        time.Sleep(100 * time.Millisecond)
    }
}

[!IMPORTANT]

补充:一般来说,线程和协程的区别如下。

  • 线程:线程是操作系统级别的执行单元。每个线程都有自己的栈空间和寄存器等资源,线程的创建、调度和管理大部分由操作系统内核负责。
  • 协程:协程是普通用户级别的执行单元。协程由程序运行时(如 Go 运行时、Python 解释器等)管理,而不需要操作系统的直接参与。
7.3.2.协程通信

协程直接可以通过通道来通信,因此通道必须具备传递参数的能力,通道参数传递行为和函数参数传递行为类似,但是多了关于数据流向的问题。

// 使用无缓存通道
// 一个数组经过两个协程分别计算后使用通道进行汇总
package main

import "fmt"

func sum(s []int, c chan int) {
    sum := 0
    for _, v := range s { // 遍历传递过来的数组进行累加
        sum += v
    }
    c <- sum // 把 sum 发送到通道 c
}

func main() {
    s := []int{7, 2, 8, -9, 4, 0}

    c := make(chan int)
    go sum(s[:len(s)/2], c)
    go sum(s[len(s)/2:], c)
    x, y := <-c, <-c // 从通道 c 中接收

    fmt.Println(x, y, x+y)
}

默认情况下,使用 make 关键字既 make(chan_name type_name) 创建的通道是不带缓冲区的。发送端发送数据,同时必须有接收端相应的接收数据。这意味着整个过程是同步的,否则会发生阻塞。不过由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送端就进入阻塞状态无法再发送数据了。

// 使用有缓冲通道
package main

import "fmt"

func main() {
    // 这里我们定义了一个可以存储整数类型的带缓冲通道
    // 缓冲区大小为 2
    ch := make(chan int, 2) // 如果没有 2 就会编译失败

    // 因为 ch 是带缓冲的通道, 我们可以同时发送两个数据
    // 而不用立刻需要去同步读取数据
    ch <- 1
    ch <- 2

    // 获取这两个数据
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

[!IMPORTANT]

补充:通道具有流向的特点,因此也分有普通通道、只读通道、只写通道。

// 三种不同的通道
package main

import "time"

// 只写通道作为参数
func sendData(ch chan<- int) {
    ch <- 20
}

// 只读通道作为参数
func receiveData(ch <-chan int) {
    num := <-ch
    println(num)
}

func main() {
    ch := make(chan int)
    go sendData(ch)
    go receiveData(ch)
    
    // 等待一段时间,确保数据能正常传递和接收
    time.Sleep(1 * time.Second)
}

[!WARNING]

注意:因此如果使用无缓冲的通道,在同一个协程中(比如主协程 main)中是无法同时存在发送通道和接受通道的,会发生死锁错误,无法编译。

[!WARNING]

注意:如果使用无缓冲通道,发送和接收必须是同步的,否则就会发生阻塞。如果主协程既不读取数据,其他协程又无法继续执行,就会触发 fatal error: all goroutines are asleep - deadlock! 这个错误,也就是死锁。但是如果您希望直观观察到阻塞现象,可以看下面的两个代码。

// 由于通道没有缓冲并且未写入引起的协程阻塞现象
// 一个数组经过两个协程分别计算后使用通道进行汇总
package main

import (
    "fmt"
    "time"
)

func sum(s []int, c chan int) {
    sum := 0
    for _, v := range s { // 遍历传递过来的数组进行累加
        sum += v
    }
    // c <- sum // 故意注释掉把 sum 发送到通道 c 的代码

    // 故意设置一个死循环避免出现死锁, 方便看到主协程的阻塞现象
    for true {
        fmt.Println("Wait write...")
        time.Sleep(1 * time.Second)
    }
}

func main() {
    s := []int{7, 2, 8, -9, 4, 0}

    c := make(chan int)
    go sum(s[:len(s)/2], c)
    go sum(s[len(s)/2:], c)
    x, y := <-c, <-c // 从通道 c 中接收

    // 这里往后的代码一直无法被执行, 因为通道没有缓存处于同步的状态
    fmt.Println("Run here")
    fmt.Println(x, y, x+y)
}

// 由于通道没有缓冲并且未读出引起的协程阻塞现象
// 一个数组经过两个协程分别计算后使用通道进行汇总
package main

import (
    "fmt"
    "time"
)

func sum(s []int, c chan int) {
    sum := 0
    for _, v := range s { // 遍历传递过来的数组进行累加
        sum += v
    }
    c <- sum

    // 这里往后的代码一直无法被执行, 因为通道没有缓存处于同步的状态
    fmt.Println("Run here")
}

func main() {
    s := []int{7, 2, 8, -9, 4, 0}

    c := make(chan int)
    go sum(s[:len(s)/2], c)
    go sum(s[len(s)/2:], c)
    // x, y := <-c, <-c // 故意注释掉从通道 c 中接收的代码

    // 故意设置一个死循环避免出现死锁, 方便看到其他协程的阻塞现象
    for true {
        fmt.Println("Wait write...")
        time.Sleep(1 * time.Second)
    }

    // fmt.Println(x, y, x+y)
}

通道除了被打开后读写,还可以手动关闭,并且也允许使用关键字 range 遍历通道。

// 关闭通道
package main

import (
    "fmt"
)

func fibonacci(n int, c chan int) {
    x, y := 0, 1
    for i := 0; i < n; i++ {
        c <- x
        x, y = y, x+y
    }
    close(c)
}

func main() {
    c := make(chan int, 10)
    go fibonacci(cap(c), c)
    // range 函数遍历每个从通道接收到的数据, 因为 c 在发送完 10 个
    // 数据之后就关闭了通道, 所以这里我们 range 函数在接收到 10 个数据之后就结束了
    // 如果上面的 c 通道不关闭, 那么 range 函数就不会结束
    // 从而在接收第 11 个数据的时候就阻塞了
    for i := range c {
        fmt.Println(i)
    }
}

select 语句使得一个协程可以等待多个通信操作,select 会阻塞,直到其中的某个 case 可以继续执行。

// 使用 select 语句
package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(2 * time.Second)
        ch1 <- "消息来自 ch1"
    }()

    go func() {
        time.Sleep(1 * time.Second)
        ch2 <- "消息来自 ch2"
    }()

    for i := 0; i < 2; i++ { // 需要接收两次
        select {
        case msg1 := <-ch1:
            fmt.Println("接收到:", msg1)
        case msg2 := <-ch2:
            fmt.Println("接收到:", msg2)
        }
    }
}

[!NOTE]

吐槽:这不就是封装版的多路转接 select() 么,哪一个通道可用就用哪一个、手动循环、阻塞等待…

另外如果协程进入阻塞,需要手动等待,否则有可能无法通过编译得到死锁错误。

// 等待被阻塞的所有协程
package main

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

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done() // wg.Done() 用于减去计数器, 这里的 defer 可以延迟执行该函数, 确保 worker 函数调用结束后执行 Done()
    fmt.Printf("Worker %d starting\n", id)
    time.Sleep(time.Second) // 等待 1 s
    fmt.Printf("Worker %d done\n", id)
}

func main() {
    var wg sync.WaitGroup // 内部维护了一个计数器
    numWorkers := 3
    wg.Add(numWorkers) // 设置计数器的初始值, 代表需要启动的协程个数

    for i := 1; i <= numWorkers; i++ {
        go worker(i, &wg)
    }

    wg.Wait() // 等待所有协程执行完毕
    fmt.Println("All workers have finished")
}

[!IMPORTANT]

补充:如果不使用 defer,而是将 wg.Done() 放在 worker 函数的末尾,那么在函数执行过程中如果发生 panicwg.Done() 就不会被执行,这会导致 sync.WaitGroup 的计数器无法正确递减,进而使 wg.Wait() 一直阻塞,程序无法正常结束。

[!NOTE]

吐槽:C 语言使用 Linux 的系统调用实现各种并发场景,而 Go 简洁的并发语法内置让 Go 成为高级的 C。说实在的,以后需要我手写什么线程池的场景,我会更加偏向使用 Go

7.3.3.协程控制

目前我们对协程都是让 Go 自动处理,这和操作系统的线程很类似,但是我们希望更加对协程进行控制。

  • context.WithCancel 会返回一个可取消的上下文 ctx 和一个取消函数 cancel。调用 cancel 函数可以取消该上下文,通知所有基于此上下文启动的 goroutine 停止工作。

    // 使用 context.WithCancel()
    package main
    
    import (
        "context"
        "fmt"
        "time"
    )
    
    func worker(ctx context.Context, id int) {
        for {
            select {
            case <-ctx.Done():
                fmt.Printf("Worker %d received cancel signal and stopped.\n", id)
                return
            default:
                fmt.Printf("Worker %d is working...\n", id)
                time.Sleep(500 * time.Millisecond)
            }
        }
    }
    
    func main() {
        // 创建一个可取消的上下文
        ctx, cancel := context.WithCancel(context.Background())
    
        // 启动一个 goroutine 作为工作协程
        go worker(ctx, 1)
    
        // 模拟工作一段时间后取消上下文
        time.Sleep(2 * time.Second)
        cancel()
    
        // 等待一段时间,确保工作协程有足够的时间响应取消信号
        time.Sleep(1 * time.Second)
        fmt.Println("Main function exiting.")
    }
    
    
  • context.WithTimeout 会返回一个带有超时时间的上下文 ctx 和一个取消函数 cancel。当超过指定的超时时间后,上下文会自动取消。

    // 使用 context.WithTimeout()
    package main
    
    import (
        "context"
        "fmt"
        "time"
    )
    
    func worker(ctx context.Context, id int) {
        for {
            select {
            case <-ctx.Done():
                fmt.Printf("Worker %d received cancel signal and stopped due to timeout.\n", id)
                return
            default:
                fmt.Printf("Worker %d is working...\n", id)
                time.Sleep(500 * time.Millisecond)
            }
        }
    }
    
    func main() {
        // 创建一个带有 1.5 秒超时时间的上下文
        ctx, cancel := context.WithTimeout(context.Background(), 1500*time.Millisecond)
        defer cancel() // 确保在函数结束时取消上下文,避免资源泄漏
    
        // 启动一个 goroutine 作为工作协程
        go worker(ctx, 1)
    
        // 等待工作协程响应取消信号
        time.Sleep(2 * time.Second)
        fmt.Println("Main function exiting.")
    }
    
    
7.3.4.协程互斥

Go 语言里,sync.Mutex 是用于实现互斥锁的结构体,借助互斥锁可以保证在同一时刻只有一个协程能够访问共享资源,从而避免多个协程同时操作共享资源而引发的数据竞争问题。

// 使用互斥锁
package main

import (
    "fmt"
    "sync"
)

// 定义一个全局变量作为共享资源
var sharedResource int

// 定义一个互斥锁
var mutex sync.Mutex

// 定义一个函数用于增加共享资源的值
func increment() {
    // 加锁,确保同一时刻只有一个 goroutine 可以访问共享资源
    mutex.Lock()
    // 使用 defer 确保在函数返回时解锁
    defer mutex.Unlock()
    sharedResource++
    fmt.Printf("Incremented sharedResource to %d\n", sharedResource)
}

func main() {
    var wg sync.WaitGroup
    // 启动 10 个 goroutine 来并发地增加共享资源的值
    numGoroutines := 10
    wg.Add(numGoroutines)

    for i := 0; i < numGoroutines; i++ {
        go func() { // 这里的 () 可以定义参数
            // 当 goroutine 完成任务时通知 WaitGroup
            defer wg.Done()
            increment()
        }() // 这里的 () 可以设置参数
    }

    // 等待所有 goroutine 完成任务
    wg.Wait()
    fmt.Printf("Final value of sharedResource: %d\n", sharedResource)
}

8.错误处理

Go 语言的错误处理采用显式返回错误的方式,而非传统的异常处理机制。这种设计使代码逻辑更清晰,便于开发者在编译时或运行时明确处理错误。结合 Go 可以在函数返回多个返回值以及并行赋值的特性,可以使用 errors 包中的 errors.New("xxx") 来创建一个错误类型。

// 实现内部
func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return 0, errors.New("math: square root of negative number")
    }
    // 实现
}

// 使用函数
result, err:= Sqrt(-1)

if err != nil {
   fmt.Println(err)
}

Go 中,错误通常作为函数的返回值返回,开发者需要显式检查并处理,因此如果没有处理错误就会报错。

package main

import (
    "errors"
    "fmt"
)

func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

func main() {
    result := divide(10, 0) // 由于只接受了一个值, 没有处理错误值导致编译出现问题
    fmt.Println(result)
}

Go 标准库定义了一个 error 接口,表示一个错误的抽象。

// error 接口
type error interface {
    Error() string
}

  • 任何实现了 Error() 方法的类型都可以作为错误
  • Error() 方法返回一个描述错误的字符串
// 使用 error 接口
package main

import (
    "fmt"
)

type DivideError struct {
    Dividend int
    Divisor  int
}

func (e *DivideError) Error() string {
    return fmt.Sprintf("cannot divide %d by %d", e.Dividend, e.Divisor)
}

func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, &DivideError{Dividend: a, Divisor: b}
    }
    return a / b, nil
}

func main() {
    _, err := divide(10, 0)
    if err != nil {
        fmt.Println(err) // 输出 cannot divide 10 by 0
    }
}

9.集合工具

Go 没有类似 JavaCpp 等语言有专门的集合工具,但是和 Python 一样,其本身具备的数组、切片、字符串、键值对等就已经满足大部分的需要。

10.包管理器

Go 自带包管理,其实就是 go 命令行工具。

  • **初始化项目为 Go 模块。**确保您的项目已经初始化为一个 Go 模块。在项目根目录下执行 go mod init <module-path> 通常是项目的代码仓库地址,例如:go mod init github.com/yourusername/yourproject。执行该命令后,项目根目录下会生成一个 go.mod 文件,用于记录项目的依赖信息和模块路径。

  • **编写代码并确保代码结构合理。**组织好项目的包结构,每个包包含相关的 Go 源文件,且同一目录下的源文件开头使用相同的 package 声明。如果希望其他开发者能够使用您的函数、类型等,需要将它们导出(标识符首字母大写)。

    // yourpackage/yourfile.go
    package yourpackage
    
    // PublicFunction 是一个导出的函数
    func PublicFunction() {
        // 函数实现
    }
    
    
  • **添加文档注释。**为导出的标识符添加详细的文档注释,方便其他开发者理解和使用。

    // yourpackage/yourfile.go
    package yourpackage
    
    // PublicFunction 执行特定的操作。
    // 该函数没有参数,也没有返回值。
    func PublicFunction() {
        // 函数实现
    }
    
    
  • **进行测试(可选但推荐)。**编写测试代码来确保您的模块功能正确。测试文件以 _test.go 结尾,放在对应的包目录下,使用 go test <path> 命令运行测试。

    // yourpackage/yourfile_test.go
    package yourpackage
    
    import "testing"
    
    func TestPublicFunction(t *testing.T) {
        PublicFunction()
        // 可以添加更多的测试逻辑
    }
    
    
  • **将项目上传到代码托管平台。**将您的项目上传到代码托管平台,如 GitHub、GitLab 等。确保代码仓库是公开的(如果希望其他人可以自由使用),或者根据需要设置合适的访问权限。

  • **告知他人如何使用您的模块。**其他开发者可以在他们的项目中使用 go get 命令下载您的模块。例如,如果您的模块路径是 github.com/yourusername/yourproject,版本是 v1.2.3,可以执行 go get github.com/yourusername/yourproject@v1.2.3

  • 导入和使用:在他们的代码中导入您的模块并使用导出的标识符。

    // 其他人使用您的模块
    package main
    
    import (
        "fmt"
        "github.com/yourusername/yourproject/yourpackage"
    )
    
    func main() {
        yourpackage.PublicFunction()
        fmt.Println("Module function called.")
    }
    
    

http://www.kler.cn/a/535500.html

相关文章:

  • java时间相关类
  • Text2Sql:开启自然语言与数据库交互新时代(3030)
  • ORACLE11g如何查询用户权限
  • AspectJ 中通知方法参数绑定
  • 【C++】STL——stack的底层实现
  • 【prompt实战】AI +OCR技术结合ChatGPT能力项目实践(BOL提单识别提取专家)
  • 主动管理的基本概念
  • el-table中的某个字段最多显示两行,超出部分显示“...详情”,怎么办
  • Tomcat Request Cookie 丢失问题
  • [论文笔记] Deepseek-R1R1-zero技术报告阅读
  • Java全栈项目-在线实验报告系统开发实践
  • Git仓库托管基本使用_01
  • MybatisPlus较全常用复杂查询引例(limit、orderby、groupby、having、like...)
  • C++:内存泄漏
  • MyBatis一条语句(PostgresSql)实现批量新增更新操作ON CONFLICT
  • 2024最新版Node.js详细安装教程(含npm配置淘宝最新镜像地址)
  • CTF SQL注入学习笔记
  • 第七天 开始学习ArkTS基础,理解声明式UI编程思想
  • vue3-响应式 shallowRef
  • 网络安全 | 零信任架构:重构安全防线的未来趋势
  • 【2025最新计算机毕业设计】基于SSM健身俱乐部管理系统【提供源码+答辩PPT+文档+项目部署】
  • 【Vitest】单元测试
  • 【STM32】蓝牙模块数据包解析
  • 【华为OD-E卷 - 108 最大矩阵和 100分(python、java、c++、js、c)】
  • crewai框架第三方API使用官方RAG工具(pdf,csv,json)
  • 高斯溅射和GIS融合之路- 将splat文件切片成3dtiles