快速入门
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 2月 3 23:24 .
drwxrwxr-x 15 ljp ljp 4096 1月 27 23:44 ..
-rw-rw-r-- 1 ljp ljp 32 2月 3 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 2月 3 23:21 .
drwxrwxr-x 3 ljp ljp 4.0K 2月 3 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 2月 4 00:13 .
drwxrwxr-x 3 ljp ljp 4096 2月 3 23:49 ..
-rw-rw-r-- 1 ljp ljp 32 2月 4 00:12 go.mod
-rwxrwxr-x 1 ljp ljp 2130759 2月 4 00:13 hello.exe
-rw-rw-r-- 1 ljp ljp 77 2月 4 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 2月 3 23:42 .
drwxrwxr-x 15 ljp ljp 4.0K 1月 27 23:44 ..
-rw-rw-r-- 1 ljp ljp 175 2月 3 23:42 go.mod
-rw-rw-r-- 1 ljp ljp 499 2月 3 23:42 go.sum # 多出该文件
-rwxrwxr-x 1 ljp ljp 2.1M 2月 3 23:36 hello.exe
-rw-rw-r-- 1 ljp ljp 95 2月 3 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 := 50
、b := false
这些形式,等价于var a = 50
、var b = false
。
[!IMPORTANT]
补充:
Go
允许像Python
在一行定义或赋值多个变量(并行赋值)。因此如果您想要快速交换两个变量的值,则可以简单地使用a, b = b, a
,但是两个变量的类型必须是相同。并行赋值也被用于当一个函数返回多个返回值时,比如val, err = Func(var)
。
[!IMPORTANT]
补充:空白标识符
_
也被用于抛弃值,如在_, b = 5, 7
中5
被抛弃。_
实际上是一个只写变量,您不能得到它的值。有时您会遇到并不需要使用从一个函数得到的所有返回值的情况,这个时候_
会非常有用。
[!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
是一个特殊的常量,可以认为是一个可以被编译器修改的常量。iota
在 const
关键字出现时将被重置为 0
,const
中每新增一行常量声明将使 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
语言类似,不过有一些简化和加强。首先 Go
和 Python
类似去掉了冗余的 ()
来放置布尔表达式,但仍需要 {}
,可以用一个代码来解释清楚。
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
也继承了C
的break、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
的模块中有专门的函数可以静态计算一个变量的内存占用(计算出字节的个数,类似C
的sizeof()
)。
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
语言支持并发,通过 goroutines
和 channels
提供了一种简洁且高效的方式来实现并发。
Goroutines
:Go
中的并发执行单位,类似于轻量级的线程Goroutine
的调度由Go
运行时管理,用户无需手动分配线程- 使用
go
关键字启动Goroutine
Goroutine
是非阻塞的,可以高效地运行成千上万个Goroutine
Channel
:Go
中用于在Goroutine
之间通信的机制- 支持同步和数据共享,避免了显式的锁机制
- 使用
chan
关键字创建,通过<-
操作符发送和接收数据
Scheduler
:Go
的调度器基于GMP
模型- G:内置协程(
Goroutine
) - M:系统线程(
Machine
) - P:逻辑处理器(
Processor
)
- G:内置协程(
- 协程会先进入到
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
函数的末尾,那么在函数执行过程中如果发生panic
,wg.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
没有类似 Java
和 Cpp
等语言有专门的集合工具,但是和 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.") }