Go 语言入门教程:从零开始掌握这门高效编程语言
欢迎来到 Go 语言的世界!如果你正在寻找一门现代、高效、易于学习且拥有强大并发能力的编程语言,那么 Go 语言(通常称为 Golang)绝对值得你的关注。由 Google 的 Robert Griesemer, Rob Pike, 和 Ken Thompson 设计开发,Go 语言自2009年发布以来,凭借其简洁的语法、强大的标准库以及内置的并发支持,迅速在云计算、网络服务、微服务等领域流行起来。
本教程将带你从零开始,一步步了解 Go 语言的基础知识,包括安装、基本语法、数据类型、控制结构、函数、切片、映射、结构体、方法、接口,以及 Go 语言最引人注目的特性之一——并发(goroutines 和 channels)。无论你是否有其他编程语言经验,本教程都力求用最清晰的方式为你展现 Go 语言的魅力。
文章目录
- 为什么选择 Go 语言?
- Go 语言的安装与环境配置
- 下载与安装
- 验证安装
- 设置 GOPATH(现代 Go 开发中已较少使用,但了解其历史有益)
- Go Modules(现代依赖管理)
- 你的第一个 Go 程序:Hello, World!
- 编写代码
- 运行代码
- 代码解析
- Go 语言基础
- 注释
- 包(Packages)
- 导入(Importing Packages)
- 函数(Functions)
main
函数- 函数定义与调用
- 多返回值
- 命名返回值
- 可变参数函数
- 匿名函数与闭包
defer
语句
- 变量
- 声明变量
- 零值(Zero Values)
- 类型推断(短声明
:=
) - 批量声明
- 常量
- 基本数据类型
- 布尔型(
bool
) - 数值类型(整型、浮点型、复数型)
- 字符串(
string
)
- 布尔型(
- 类型转换
- 运算符
- 控制流程
- 条件语句 (
if
,else if
,else
) - 循环 (
for
语言的唯一循环结构)- 经典
for
循环 while
风格的for
循环- 无限循环
for range
循环(用于遍历数组、切片、映射、字符串、通道)
- 经典
- 分支语句 (
switch
)- 基本
switch
- 无表达式
switch
fallthrough
关键字
- 基本
- 条件语句 (
- 复杂数据类型
- 数组(Arrays)
- 切片(Slices)
- 什么是切片?为什么使用切片?
- 创建切片 (
make
) - 切片的长度(
len
)与容量(cap
) - 切片表达式
append
函数copy
函数
- 映射(Maps)
- 什么是映射?
- 创建映射 (
make
) - 添加/访问/修改元素
- 删除元素 (
delete
) - 检查键是否存在
- 结构体(Structs)
- 定义结构体
- 创建结构体实例
- 访问结构体字段
- 匿名结构体
- 结构体嵌套(嵌入)
- 方法与接口
- 方法(Methods)
- 带接收者(Receiver)的函数
- 值接收者与指针接收者
- 接口(Interfaces)
- 定义接口
- 实现接口(隐式实现)
- 接口作为类型
- 空接口 (
interface{}
或any
) - 类型断言
- 类型选择 (
switch
on type)
- 方法(Methods)
- 指针(Pointers)
- 什么是指针?
- 获取地址 (
&
) - 通过指针访问值 (
*
) - 为什么 Go 语言需要指针?
- 并发(Concurrency)
- goroutines(轻量级线程)
- 使用
go
关键字
- 使用
- Channels(通道)
- 什么是通道?为什么使用通道?
- 创建通道 (
make
) - 发送和接收数据 (
<-
) - 无缓冲通道与有缓冲通道
- 关闭通道 (
close
) - 使用
range
遍历通道 select
语句(多路复用)
- Sync 包(简要提及)
- goroutines(轻量级线程)
- 错误处理
- Go 语言的错误处理哲学
error
接口- 返回错误
- 处理错误 (
if err != nil
) - 创建自定义错误
- 包与模块化
- 标准库(Standard Library)
- 组织代码到不同的包
- 可见性规则(大写开头 vs 小写开头)
- Go Modules 工作原理简介
- 测试
- Go 语言的测试框架
- 编写测试函数
- 运行测试 (
go test
)
- 总结与下一步
1. 为什么选择 Go 语言?
Go 语言在现代软件开发中有许多吸引人的特性:
- 简洁易学: Go 的语法设计简洁,关键字少,没有类继承、泛型(1.18版本前)、异常处理等复杂概念,上手难度低。
- 高性能: Go 是编译型语言,直接编译成机器码,性能接近 C/C++。其垃圾回收机制也非常高效。
- 强大的并发支持: Go 在语言层面内置了并发支持(goroutines 和 channels),使得编写高并发程序变得异常简单和高效。这是 Go 语言最引人注目的特性之一。
- 快速编译: Go 编译器速度非常快,大大提高了开发效率。
- 静态链接: Go 编译器可以生成不依赖任何外部库的独立可执行文件,部署非常方便,只需拷贝一个文件即可。
- 丰富的标准库: Go 拥有一个庞大而强大的标准库,涵盖了网络(HTTP, TCP/IP)、文件操作、数据结构、加密、编码等众多领域,很多常见任务无需依赖第三方库即可完成。
- 优秀的工具链: Go 提供了内置的工具,如格式化代码 (
go fmt
)、构建 (go build
)、运行 (go run
)、测试 (go test
)、依赖管理 (go mod
) 等,极大地提升了开发体验。 - 活跃的社区: Go 语言拥有一个庞大且活跃的开发者社区,文档丰富,遇到问题容易找到解决方案。
这些特性使得 Go 语言成为构建网络服务、分布式系统、DevOps 工具、命令行工具等的理想选择。
2. Go 语言的安装与环境配置
开始学习 Go 之前,你需要先在你的电脑上安装 Go 环境。
下载与安装:
访问 Go 官方下载页面:https://go.dev/dl/。
根据你的操作系统(Windows, macOS, Linux)下载对应的安装包。
- Windows: 下载
.msi
安装包,双击运行,按照提示一步步安装即可。安装程序通常会自动配置环境变量。 - macOS: 下载
.pkg
安装包,双击运行,按照提示安装。 -
Linux: 下载
.tar.gz
压缩包,解压到/usr/local
目录(或你喜欢的任何位置),然后配置环境变量。“`bash
假设下载的文件是 go1.21.5.linux-amd64.tar.gz
sudo rm -rf /usr/local/go # 如果之前安装过,先删除旧版本
sudo tar -C /usr/local -xzf go1.21.5.linux-amd64.tar.gz
然后需要将 Go 的 `bin` 目录添加到你的 `PATH` 环境变量中。编辑你的 shell 配置文件(如 `~/.bashrc`, `~/.zshrc`, `~/.profile` 等),添加以下行:
bash
export PATH=$PATH:/usr/local/go/bin
``
source ~/.bashrc` (或你的配置文件) 使其生效。
保存文件,然后运行
验证安装:
打开终端或命令行提示符,运行以下命令:
bash
go version
如果安装成功,你应该会看到 Go 语言的版本信息,例如:
go version go1.21.5 linux/amd64
这表明 Go 已经成功安装并在你的 PATH
中。
设置 GOPATH (了解即可,现代开发主要使用 Go Modules):
在 Go Modules 出现之前 (Go 1.11+),GOPATH
是一个非常重要的概念,它指定了 Go 的工作空间,所有 Go 项目、依赖库、编译后的可执行文件默认都会放在这个目录结构下 (src
, pkg
, bin
)。
虽然现在 Go Modules 已经成为主流,项目可以放在任意位置,不再强制依赖 GOPATH
,但了解这个概念有助于理解一些老项目或遗留文档。
如果你确实需要设置 GOPATH
(例如在 Go Modules 关闭的情况下),你可以设置一个环境变量:
“`bash
在你的 shell 配置文件中添加
export GOPATH=$HOME/go # 或者你想要的任何目录
export PATH=$PATH:$GOPATH/bin # 将GOPATH下的bin目录添加到PATH中
``
source` 命令使配置生效。
同样,运行
Go Modules (现代依赖管理):
从 Go 1.11 开始,Go Modules 成为官方推荐的依赖管理方式。它允许你将项目放在文件系统的 任何 位置,而不再局限于 GOPATH
。Go Modules 通过项目根目录下的 go.mod
文件来管理依赖。
当你使用 go get
, go build
, go run
等命令在一个没有 go.mod
文件的目录中时,Go 会提示你初始化一个 module:
bash
go mod init your_module_name # your_module_name 通常是你的仓库地址,如 github.com/yourname/yourproject
初始化后会生成 go.mod
文件。后续使用 go get
获取依赖时,它们会被下载到全局的 Go Module 缓存中,而不是你的项目目录或 GOPATH
下。
对于初学者来说,最简单的方式是忽略 GOPATH,直接在任意目录创建项目,然后使用 go mod init
初始化一个模块即可。 本教程后续的示例也默认在 Go Modules 环境下运行。
3. 你的第一个 Go 程序:Hello, World!
按照惯例,我们的第一个程序总是打印 “Hello, World!”。
编写代码:
创建一个新目录,比如 hello_world
。在这个目录中创建一个名为 main.go
的文件。
“`go
package main
import “fmt”
func main() {
fmt.Println(“Hello, World!”)
}
“`
运行代码:
打开终端,切换到 hello_world
目录,运行:
bash
go run main.go
你应该会在终端看到输出:
Hello, World!
或者你也可以先编译再运行:
bash
go build main.go # 这会在当前目录生成一个可执行文件 (main 或 main.exe)
./main # 运行生成的可执行文件 (Windows下可能是 main.exe)
输出同样是 “Hello, World!”。
代码解析:
go
package main
每个 Go 文件都属于一个包。package main
定义了一个名为 main
的包。main
包是一个特殊的包,它表示一个可执行程序,而不是一个库。可执行程序的入口点必须是 main
包下的 main
函数。
go
import "fmt"
import
关键字用于导入其他包。这里我们导入了标准库中的 fmt
包,它提供了格式化输入输出的功能,比如打印到控制台。
go
func main() {
// 函数体
}
func
关键字用于声明一个函数。main
函数是可执行程序的入口点,Go 程序从 main
函数开始执行。花括号 {}
包围着函数体。
go
fmt.Println("Hello, World!")
这行代码调用了 fmt
包中的 Println
函数。Println
函数的作用是打印字符串到标准输出(通常是控制台),并在末尾添加一个换行符。注意 Println
是以大写字母开头的,这在 Go 语言中有特殊的含义:以大写字母开头的函数、变量、类型等是可导出的(Public),可以在其他包中访问;以小写字母开头的则是包私有的(Private),只能在当前包中使用。 这是 Go 语言实现封装的一种方式。
4. Go 语言基础
掌握了如何运行 Go 程序后,我们来深入了解一些基本语法和概念。
注释
Go 语言支持两种注释:
- 单行注释: 以
//
开头,直到行尾。
go
// 这是一个单行注释 - 多行注释: 以
/*
开头,以*/
结尾。常用于函数或包的文档注释。
go
/*
这是一个多行注释,
可以跨越多行。
*/
包(Packages)
包是 Go 语言组织代码的方式。相关的功能通常放在同一个包中。我们在上面已经看到了 package main
和 import "fmt"
。
package 包名
: 声明当前文件所属的包。- 同一个目录下的
.go
文件通常属于同一个包(除了_test.go
文件)。 - 包名通常使用小写字母。
main
包是特殊的,包含可执行程序的入口main
函数。- 其他包是库,提供可重用的功能。
导入(Importing Packages)
使用 import
关键字导入需要的包。可以导入单个包,也可以使用括号导入多个包:
“`go
import “fmt”
import “math”
// 或者
import (
“fmt”
“math”
“net/http” // 导入标准库的http包
)
“`
导入后,可以使用 包名.标识符
的方式访问包中的导出(大写开头)元素。
函数(Functions)
函数是 Go 语言组织代码块的基本单元。
main
函数:
go
func main() {
// 程序从这里开始执行
}
每个可执行程序必须有一个 main
包和一个 main
函数。
函数定义与调用:
函数定义的语法如下:
go
func 函数名(参数列表) (返回值列表) {
// 函数体
// return 返回值
}
func
关键字。- 函数名。
- 参数列表:零个或多个参数,每个参数格式为
参数名 参数类型
,多个参数用逗号隔开。如果多个参数类型相同,可以省略前面参数的类型,只保留最后一个。 - 返回值列表:零个或多个返回值,格式与参数列表类似。如果只有一个返回值,可以省略括号。如果没有返回值,可以省略返回值列表。
示例:
“`go
func add(x int, y int) int { // x, y 都是 int 类型
return x + y
}
func multiply(x, y int) int { // 简写形式
return x * y
}
func greet(name string) { // 没有返回值
fmt.Println(“Hello,”, name)
}
func main() {
sum := add(5, 3)
fmt.Println(sum) // 输出 8
product := multiply(4, 6)
fmt.Println(product) // 输出 24
greet("Alice") // 输出 Hello, Alice
}
“`
多返回值:
Go 函数可以返回多个值,这常用于同时返回结果和错误信息。
“`go
func swap(x, y string) (string, string) {
return y, x
}
func main() {
a, b := swap(“hello”, “world”)
fmt.Println(a, b) // 输出 world hello
}
“`
命名返回值:
可以为返回值指定名称,这样函数体内可以直接使用这些名称,并且会在函数结束时自动返回。
“`go
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum – x
// 可以直接写 return,因为它会返回 x 和 y 的当前值
return
}
func main() {
fmt.Println(split(17)) // 输出 7 10
}
“`
使用命名返回值可以让代码更清晰,尤其是有多个返回值时。
可变参数函数:
函数可以接受不定数量的同类型参数,使用 ...
语法。这些参数在函数体内会被当作一个切片处理。
“`go
func sumAll(numbers …int) int {
total := 0
for _, num := range numbers {
total += num
}
return total
}
func main() {
fmt.Println(sumAll(1, 2, 3)) // 输出 6
fmt.Println(sumAll(10, 20, 30, 40)) // 输出 100
}
“`
匿名函数与闭包:
Go 支持匿名函数,即没有函数名的函数。匿名函数可以赋值给变量,也可以直接调用。闭包是指匿名函数引用了其声明范围外的变量。
“`go
func main() {
// 匿名函数赋值给变量
add := func(a, b int) int {
return a + b
}
fmt.Println(add(10, 5)) // 输出 15
// 直接调用匿名函数
func(message string) {
fmt.Println(message)
}("Hello from anonymous function!") // 输出 Hello from anonymous function!
// 闭包示例
counter := createCounter()
fmt.Println(counter()) // 输出 1
fmt.Println(counter()) // 输出 2
}
// 闭包函数:返回一个函数,该函数”记住”了外部变量 i
func createCounter() func() int {
i := 0 // 外部变量
return func() int { // 匿名函数
i++ // 访问并修改外部变量 i
return i
}
}
“`
defer
语句:
defer
语句用于延迟函数的执行,直到包含它的函数执行完毕(无论是正常返回还是发生 panic)。defer
常用于资源清理,如关闭文件、释放锁等。如果同一个函数中有多个 defer
语句,它们会按照后进先出的顺序执行。
“`go
func readFile(filename string) {
fmt.Println(“打开文件:”, filename)
// 假设这里是打开文件的操作,并返回一个文件对象
// file, err := os.Open(filename)
// if err != nil { … handle error … }
// defer 确保文件在函数结束时关闭
// defer file.Close() // 真实的场景会是这样
// 模拟其他操作
fmt.Println("正在读取文件内容...")
// defer 语句会在函数即将返回前执行
defer fmt.Println("defer 1: 文件读取完毕")
defer fmt.Println("defer 2: 准备关闭文件") // 这个会先执行
fmt.Println("文件处理完成")
}
func main() {
readFile(“example.txt”)
}
输出会是:
打开文件: example.txt
正在读取文件内容…
文件处理完成
defer 2: 准备关闭文件
defer 1: 文件读取完毕
“`
变量
变量用于存储数据。Go 语言是静态类型语言,但支持类型推断。
声明变量:
使用 var
关键字声明变量:
“`go
var name string // 声明一个字符串变量 name
var age int // 声明一个整型变量 age
var isStudent bool // 声明一个布尔型变量 isStudent
var price float64 // 声明一个浮点型变量 price
// 声明变量并初始化
var city string = “New York”
var count int = 100
// 声明多个变量
var (
country string = “USA”
zipCode int = 10001
)
func main() {
fmt.Println(name, age, isStudent, price) // 输出变量的零值
fmt.Println(city, count)
fmt.Println(country, zipCode)
}
“`
零值(Zero Values):
当声明变量但未显式初始化时,变量会被自动赋予其类型的零值:
- 数值类型 (int, float, etc.):
0
- 布尔型 (
bool
):false
- 字符串 (
string
):""
(空字符串) - 切片 (
slice
):nil
- 映射 (
map
):nil
- 通道 (
channel
):nil
- 接口 (
interface
):nil
- 指针 (
pointer
):nil
- 结构体 (
struct
):各字段的零值
类型推断(短声明 :=
):
在函数内部,可以使用短声明 :=
来声明并初始化变量。Go 会根据初始值自动推断变量的类型。这是 Go 语言中最常用的变量声明方式。
“`go
func main() {
message := “Hello, Go!” // 自动推断为 string 类型
number := 42 // 自动推断为 int 类型
pi := 3.14 // 自动推断为 float64 类型
isValid := true // 自动推断为 bool 类型
fmt.Println(message, number, pi, isValid)
// 注意::= 只能用于新声明的变量。如果变量已经声明过,再使用 := 会导致编译错误。
// 可以使用 = 来为已声明的变量赋值。
message = "Welcome!"
fmt.Println(message)
}
``
:=` 只能在函数内部使用,不能用于包级别的变量声明。
常量
常量用于存储不会改变的值。使用 const
关键字声明。常量可以在包级别或函数内部声明。
“`go
const Pi = 3.14159 // 声明一个浮点型常量
const Greeting = “Hello” // 声明一个字符串常量
const (
StatusOK = 200
StatusError = 500
)
func main() {
const MaxInt = 1<<63 – 1 // 可以在函数内部声明常量
fmt.Println(Pi, Greeting)
fmt.Println(StatusOK, StatusError)
fmt.Println(MaxInt)
// 常量不能被修改
// Pi = 3.0 // 编译错误!
}
“`
常量可以是数值、布尔值或字符串。常量的值必须在编译时确定。
基本数据类型
Go 语言提供了丰富的内置数据类型:
- 布尔型 (
bool
):true
或false
。 - 数值类型:
- 整型:
- 有符号整型:
int
,int8
,int16
,int32
,int64
(int
的大小取决于操作系统,32位或64位)。 - 无符号整型:
uint
,uint8
(byte),uint16
,uint32
,uint64
,uintptr
(uint
的大小与int
相同,uintptr
用于存储指针值)。 byte
是uint8
的别名。rune
是int32
的别名,用于表示 Unicode 码点。
- 有符号整型:
- 浮点型:
float32
,float64
(float64
是默认的浮点类型)。 - 复数型:
complex64
(实部和虚部都是float32
), complex128
(实部和虚部都是float64
)。
- 整型:
- 字符串 (
string
): 一系列不可变的字节序列。可以使用双引号""
或反引号`
来定义字符串。反引号字符串是原始字符串,支持多行,不转义特殊字符。
“`go
func main() {
var b bool = true
var i int = 10
var f float64 = 3.14
var s string = “你好 Go!”
var r rune = ‘界’ // rune 类型表示一个Unicode字符
var by byte = ‘A’ // byte 类型表示一个ASCII字符或原始字节
fmt.Println(b, i, f, s, r, by)
rawString := `这是一个原始字符串,
它可以跨越多行,
并且不会解释转义字符,如 \n。`
fmt.Println(rawString)
}
“`
类型转换
Go 语言不支持隐式类型转换,必须进行显式类型转换。
“`go
func main() {
var i int = 42
var f float64 = float64(i) // 将 int 转换为 float64
var u uint = uint(i) // 将 int 转换为 uint
fmt.Println(i, f, u)
var s string = "123"
// 要将字符串转换为数字或反之,需要使用标准库的strconv包
// num, err := strconv.Atoi(s) // 字符串转 int
// 注意:不同数值类型之间转换可能会丢失精度或范围溢出
var largeInt int64 = 10000000000
var smallInt int8 = int8(largeInt) // 可能溢出
fmt.Println(smallInt) // 输出 -128 (或其他依赖于溢出行为的值)
}
“`
运算符
Go 支持常见的算术、比较、逻辑、位、赋值运算符。
- 算术运算符:
+
,-
,*
,/
,%
- 比较运算符:
==
,!=
,<
,<=
,>
,>=
- 逻辑运算符:
&&
(与),||
(或),!
(非) - 位运算符:
&
,|
,^
,<<
,>>
,&^
(位清除) - 赋值运算符:
=
,+=
,-=
,*=
,/=
,%=
,&=
,|=
,^=
,<<=
,>>=
“`go
func main() {
a := 10
b := 3
fmt.Println("a + b =", a+b)
fmt.Println("a - b =", a-b)
fmt.Println("a * b =", a*b)
fmt.Println("a / b =", a/b) // 整型除法,结果是整型
fmt.Println("a % b =", a%b)
fmt.Println("a > b =", a > b)
fmt.Println("a == b =", a == b)
x := true
y := false
fmt.Println("x && y =", x && y)
fmt.Println("x || y =", x || y)
fmt.Println("!x =", !x)
a += 5 // 等同于 a = a + 5
fmt.Println("a after += 5:", a)
}
“`
5. 控制流程
控制流程语句决定了程序执行的顺序。
条件语句 (if
, else if
, else
)
Go 语言的 if
语句不需要用括号包围条件,但是 {}
是必须的。if
语句可以带一个可选的初始化语句,该语句在条件判断之前执行,声明的变量作用域仅限于 if
及与其相关的 else if
, else
块。
“`go
func main() {
score := 75
if score >= 90 {
fmt.Println("优秀")
} else if score >= 60 { // else if 写在同一行
fmt.Println("及格")
} else {
fmt.Println("不及格")
}
// 带初始化语句的 if
if num := 10; num%2 == 0 { // num 变量的作用域仅在此 if/else 块内
fmt.Println(num, "是偶数")
} else {
fmt.Println(num, "是奇数")
}
// fmt.Println(num) // 这里会报错,因为 num 不在此作用域
}
“`
循环 (for
语言的唯一循环结构)
Go 语言只有 for
一种循环关键字,但它可以通过不同的形式实现类似其他语言的 while
或无限循环。
经典 for
循环:
类似 C/C++/Java 中的 for 循环,包含初始化、条件、后置语句。括号非必需,但 {}
必须有。
go
func main() {
for i := 0; i < 5; i++ {
fmt.Println("计数:", i)
}
}
while
风格的 for
循环:
省略初始化和后置语句,只保留条件。
go
func main() {
sum := 1
for sum < 100 { // 相当于 while(sum < 100)
sum += sum
fmt.Println("sum:", sum)
}
}
无限循环:
省略所有部分。
go
func main() {
// 注意:运行这个会无限循环,需要按 Ctrl+C 终止
/*
for {
fmt.Println("无限循环...")
}
*/
}
for range
循环:
用于遍历数组、切片、映射、字符串、通道。返回两个值:索引/键 和 对应的值。
“`go
func main() {
// 遍历切片或数组:返回索引和元素值
numbers := []int{10, 20, 30, 40}
for index, value := range numbers {
fmt.Printf(“索引: %d, 值: %d\n”, index, value)
}
// 只获取值,忽略索引(使用 _ 忽略不需要的返回值)
for _, value := range numbers {
fmt.Println("值:", value)
}
// 只获取索引,忽略值
for index := range numbers {
fmt.Println("索引:", index)
}
// 遍历字符串:返回字符的起始字节索引和 rune (Unicode 码点)
str := "你好 Go"
for index, r := range str {
fmt.Printf("索引: %d, 字符: %c, Unicode 码点: %U\n", index, r, r)
}
// 遍历映射:返回键和值
ages := map[string]int{"Alice": 30, "Bob": 25}
for name, age := range ages {
fmt.Printf("姓名: %s, 年龄: %d\n", name, age)
}
// 遍历通道 (通道关闭后循环结束)
// ch := make(chan int, 2)
// ch <- 1
// ch <- 2
// close(ch)
// for val := range ch {
// fmt.Println("从通道接收:", val)
// }
}
“`
可以使用 break
跳出循环,continue
跳过当前循环迭代。
分支语句 (switch
)
switch
语句用于根据表达式的值执行不同的代码块。Go 的 switch
语句非常灵活:
case
后面不需要break
,执行完匹配的case
后会自动退出switch
。case
后面可以跟多个值,用逗号隔开。case
后面也可以跟表达式,而不仅仅是常量或字面量。- 可以省略
switch
后面的表达式,此时switch
语句等同于一系列if-else if-else
。
基本 switch
:
“`go
func main() {
day := “Monday”
switch day {
case "Monday":
fmt.Println("工作日第一天")
case "Friday":
fmt.Println("快到周末了!")
case "Saturday", "Sunday": // case 可以跟多个值
fmt.Println("周末!")
default: // 没有匹配的 case 时执行 default
fmt.Println("普通工作日")
}
}
“`
无表达式 switch
:
常用于更灵活的条件判断。
“`go
func main() {
temperature := 15
switch { // 没有表达式
case temperature < 0:
fmt.Println("极寒")
case temperature >= 0 && temperature < 10: // case 可以是表达式
fmt.Println("寒冷")
case temperature >= 10 && temperature < 20:
fmt.Println("凉爽")
default:
fmt.Println("温暖或炎热")
}
}
“`
fallthrough
关键字:
如果需要强制执行下一个 case
块,可以使用 fallthrough
。这比较少用。
go
func main() {
i := 1
switch i {
case 1:
fmt.Println("case 1")
fallthrough // 继续执行下一个 case
case 2:
fmt.Println("case 2") // 这个也会被执行
case 3:
fmt.Println("case 3") // 如果没有 fallthrough,这个不会执行
}
// 输出:
// case 1
// case 2
}
6. 复杂数据类型
Go 语言提供了几种内置的复杂数据类型来组织数据集合。
数组(Arrays)
数组是固定长度的同类型元素序列。数组的长度是其类型的一部分。
“`go
func main() {
var a [5]int // 声明一个包含 5 个整型元素的数组,元素会被初始化为零值
fmt.Println(a) // 输出 [0 0 0 0 0]
a[0] = 100 // 访问和修改元素
a[4] = 99
fmt.Println(a) // 输出 [100 0 0 0 99]
fmt.Println(a[0]) // 输出 100
fmt.Println(len(a)) // 获取数组长度,输出 5
// 声明并初始化数组
b := [3]string{"apple", "banana", "cherry"}
fmt.Println(b) // 输出 [apple banana cherry]
// 使用 ... 让编译器自动计算数组长度
c := [...]int{1, 2, 3, 4, 5, 6}
fmt.Println(c) // 输出 [1 2 3 4 5 6]
fmt.Println(len(c)) // 输出 6
}
“`
数组的长度是固定的,这限制了其灵活性,因此在 Go 中,切片(Slices)更常用。
切片(Slices)
切片是 Go 语言中对数组的抽象,它提供了更强大、更灵活的功能。切片是一个动态大小的序列。
什么是切片?为什么使用切片?
切片并不是真正拥有数据,它只是一个指向底层数组的视图。切片包含三个组件:
- 指针 (Pointer): 指向底层数组的起始元素位置。
- 长度 (Length): 切片包含的元素数量。
- 容量 (Capacity): 从切片起始元素到底层数组末尾的元素数量。
因为长度可变且基于底层数组,切片比数组更常用。
“`go
func main() {
// 从数组创建切片
primes := [6]int{2, 3, 5, 7, 11, 13}
var s []int = primes[1:4] // 创建一个切片,包含 primes[1], primes[2], primes[3]
fmt.Println(s) // 输出 [3 5 7]
// 切片字面量:直接创建切片(实际上会先创建一个匿名数组,然后返回其切片)
t := []int{10, 20, 30}
fmt.Println(t) // 输出 [10 20 30]
// 零值切片是 nil
var nilSlice []int
fmt.Println(nilSlice, len(nilSlice), cap(nilSlice)) // 输出 [] 0 0
if nilSlice == nil {
fmt.Println("nilSlice 是 nil")
}
}
“`
创建切片 (make
):
除了从现有数组或切片字面量创建,还可以使用内置的 make
函数创建切片。make
分配一个底层数组,并返回指向该数组的切片。
“`go
func main() {
// 创建一个长度为 5,容量为 5 的整型切片
a := make([]int, 5)
fmt.Println(a, len(a), cap(a)) // 输出 [0 0 0 0 0] 5 5
// 创建一个长度为 0,容量为 5 的整型切片
b := make([]int, 0, 5)
fmt.Println(b, len(b), cap(b)) // 输出 [] 0 5
// 从 b 创建一个切片,长度为 2
c := b[:2]
fmt.Println(c, len(c), cap(c)) // 输出 [0 0] 2 5
// 从 b 创建一个切片,长度为 3,容量为 3 (从索引 2 开始,容量到原容量末尾)
d := b[2:5]
fmt.Println(d, len(d), cap(d)) // 输出 [0 0 0] 3 3
}
“`
切片的长度(len
)与容量(cap
):
len(slice)
: 返回切片的元素数量。cap(slice)
: 返回从切片起始元素到其底层数组末尾的元素数量。容量反映了切片可以增长而无需重新分配底层数组的大小。
切片表达式:
可以使用 [low:high]
语法创建子切片。这会得到一个新的切片,它指向原切片(或数组)的同一个底层数组。
slice[low:high]
: 包含从索引low
到high-1
的元素。slice[low:]
: 包含从索引low
到末尾的元素。slice[:high]
: 包含从开头到索引high-1
的元素。slice[:]
: 包含原切片的所有元素,创建一个副本(但仍指向同一底层数组)。
省略 low
默认为 0,省略 high
默认为切片的长度。
append
函数:
内置函数 append
用于向切片末尾添加元素。如果底层数组容量不足,append
会创建一个新的、更大的底层数组,并将原切片的数据复制过去,然后返回新的切片。
“`go
func main() {
s := []int{1, 2, 3}
fmt.Println(s, len(s), cap(s)) // 输出 [1 2 3] 3 3
s = append(s, 4) // 添加单个元素
fmt.Println(s, len(s), cap(s)) // 输出 [1 2 3 4] 4 6 (容量可能翻倍)
s = append(s, 5, 6, 7) // 添加多个元素
fmt.Println(s, len(s), cap(s)) // 输出 [1 2 3 4 5 6 7] 7 12 (容量再次增加)
// 添加另一个切片的所有元素
s2 := []int{8, 9}
s = append(s, s2...) // 注意这里的 ...
fmt.Println(s, len(s), cap(s)) // 输出 [1 2 3 4 5 6 7 8 9] 9 12
}
``
append
因为可能返回一个新的切片(指向新的底层数组),所以通常需要将
append` 的结果重新赋值给原切片变量。
copy
函数:
内置函数 copy(dst, src)
用于将源切片 src
的元素复制到目标切片 dst
。复制的数量是 len(dst)
和 len(src)
中的较小值。
“`go
func main() {
src := []int{1, 2, 3, 4, 5}
dst := make([]int, 3) // 目标切片长度为 3
n := copy(dst, src) // 复制 src 的前 3 个元素到 dst
fmt.Println(dst) // 输出 [1 2 3]
fmt.Println("复制了", n, "个元素") // 输出 复制了 3 个元素
}
“`
映射(Maps)
映射(Map)是一种无序的键值对集合,键必须是唯一的。映射是 Go 语言内置的关联容器。
什么是映射?
映射将键映射到值。键的类型必须是可比较的(如基本类型、结构体(如果字段都可比较)、数组(如果元素可比较)),切片、映射、函数等不可比较的类型不能作为键。
创建映射 (make
):
使用 make
函数创建映射,零值映射是 nil
。
“`go
func main() {
// 创建一个键为 string,值为 int 的映射
var m map[string]int // 零值映射,是 nil
fmt.Println(m == nil) // 输出 true
// 使用 make 创建映射
m = make(map[string]int) // 创建一个空的映射
fmt.Println(m == nil) // 输出 false
// 创建映射并指定初始容量(可选,有助于性能优化)
scores := make(map[string]int, 10) // 容量为 10 的映射
// 映射字面量:创建并初始化映射
capitals := map[string]string{
"France": "Paris",
"Italy": "Rome",
"Japan": "Tokyo",
}
fmt.Println(capitals)
}
“`
添加/访问/修改元素:
使用 map[key]
语法。
“`go
func main() {
m := make(map[string]int)
m["Apple"] = 10 // 添加元素
m["Banana"] = 5 // 添加元素
fmt.Println(m) // 输出 map[Apple:10 Banana:5] (顺序不保证)
appleCount := m["Apple"] // 访问元素
fmt.Println("Apple 的数量:", appleCount) // 输出 Apple 的数量: 10
m["Apple"] = 12 // 修改元素
fmt.Println(m) // 输出 map[Apple:12 Banana:5]
// 访问不存在的键会返回该值类型的零值
orangeCount := m["Orange"]
fmt.Println("Orange 的数量:", orangeCount) // 输出 Orange 的数量: 0 (int 的零值)
}
“`
删除元素 (delete
):
使用内置函数 delete(map, key)
删除指定键的元素。删除不存在的键不会报错。
“`go
func main() {
m := map[string]int{“a”: 1, “b”: 2, “c”: 3}
fmt.Println(“原始映射:”, m) // 输出 原始映射: map[a:1 b:2 c:3]
delete(m, "b") // 删除键 "b"
fmt.Println("删除 b 后:", m) // 输出 删除 b 后: map[a:1 c:3]
delete(m, "d") // 删除不存在的键 "d",不会报错
fmt.Println("删除 d 后:", m) // 输出 删除 d 后: map[a:1 c:3]
}
“`
检查键是否存在:
访问映射时,可以使用双返回值语法来同时获取值和键是否存在的信息。
“`go
func main() {
m := map[string]int{“Apple”: 10, “Banana”: 5}
value, ok := m["Apple"] // 键存在
fmt.Println("Apple:", value, "存在吗?", ok) // 输出 Apple: 10 存在吗? true
value, ok = m["Orange"] // 键不存在
fmt.Println("Orange:", value, "存在吗?", ok) // 输出 Orange: 0 存在吗? false
}
``
ok是一个布尔值,如果键存在则为
true,否则为
false`。这是判断键是否存在以及区分零值和实际存储的零值的常用方式。
结构体(Structs)
结构体是一种聚合数据类型,它将不同类型的字段组合在一起。
定义结构体:
使用 type
和 struct
关键字。
“`go
// 定义一个 Person 结构体
type Person struct {
Name string
Age int
City string
}
func main() {
// … 使用结构体
}
“`
创建结构体实例:
可以通过多种方式创建结构体实例。
“`go
func main() {
// 方式 1: 声明变量,字段会被初始化为零值
var p1 Person
fmt.Println(p1) // 输出 { 0 } (string 的零值 “”, int 的零值 0)
// 方式 2: 使用字段名和对应的值初始化
p2 := Person{Name: "Alice", Age: 30, City: "New York"}
fmt.Println(p2) // 输出 {Alice 30 New York}
// 方式 3: 按照字段顺序初始化(不推荐,如果字段顺序改变代码会出错)
p3 := Person{"Bob", 25, "London"}
fmt.Println(p3) // 输出 {Bob 25 London}
// 方式 4: 使用 new 关键字,返回结构体指针
p4 := new(Person) // 返回 *Person 类型
fmt.Println(p4) // 输出 &{ 0 } (指针)
// 通过指针访问字段
fmt.Println(p4.Name) // 输出 ""
// *p4 是 Person 类型
}
“`
访问结构体字段:
使用点号 .
访问结构体的字段。如果变量是结构体指针,也可以直接使用点号访问字段,Go 会自动解引用。
“`go
func main() {
p := Person{Name: “Alice”, Age: 30}
fmt.Println(p.Name) // 输出 Alice
ptrP := &p // 获取结构体指针
fmt.Println(ptrP.Age) // 直接使用 . 访问字段,Go 会自动解引用 (*ptrP).Age
ptrP.City = "Paris" // 修改字段值
fmt.Println(p) // 输出 {Alice 30 Paris} (原结构体也改变了,因为 ptrP 指向 p)
}
“`
匿名结构体:
可以定义没有名字的结构体类型,常用于一次性的结构体。
go
func main() {
point := struct {
X int
Y int
}{10, 20}
fmt.Println(point.X, point.Y)
}
结构体嵌套(嵌入):
可以将一个结构体嵌入到另一个结构体中,只写类型名,不写字段名。被嵌入的结构体的字段和方法会被提升到外部结构体中,可以直接访问。
“`go
type Address struct {
Street string
City string
}
type Employee struct {
Person // 嵌入 Person 结构体
Address // 嵌入 Address 结构体
ID string
}
func main() {
e := Employee{
Person: Person{Name: “Charlie”, Age: 35},
Address: Address{Street: “123 Main St”, City: “Anytown”},
ID: “EMP001”,
}
fmt.Println(e.Name) // 直接访问 Person 的字段 Name
fmt.Println(e.City) // 直接访问 Address 的字段 City (注意:如果 Person 和 Address 都有同名字段,需要通过嵌入的结构体名访问 e.Person.City 或 e.Address.City)
fmt.Println(e.ID) // 访问 Employee 自己的字段 ID
fmt.Println(e.Person.Age) // 也可以通过嵌入的结构体名访问
}
“`
7. 方法与接口
Go 语言没有类,但可以通过方法和接口实现面向对象的编程风格。
方法(Methods)
方法是绑定到特定类型(称为接收者,Receiver)的函数。
“`go
// 定义一个 Rectangle 结构体
type Rectangle struct {
Width float64
Height float64
}
// 定义一个方法,接收者是 Rectangle 类型 (值接收者)
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
// 定义另一个方法,接收者是 Rectangle 的指针类型 (指针接收者)
func (r *Rectangle) Scale(factor float64) {
r.Width = r.Width * factor
r.Height = r.Height * factor
}
func main() {
rect := Rectangle{Width: 10, Height: 5}
fmt.Println(“面积:”, rect.Area()) // 调用方法
// 值类型调用指针接收者方法
rect.Scale(2) // Go 会自动取地址 &rect
fmt.Println("缩放后面积:", rect.Area()) // 输出 面积: 200
// 指针类型调用值接收者方法
ptrRect := &rect
fmt.Println("通过指针计算面积:", ptrRect.Area()) // Go 会自动解引用 *ptrRect
// 指针类型调用指针接收者方法
ptrRect.Scale(0.5)
fmt.Println("再次缩放后面积:", rect.Area()) // 输出 面积: 100
}
“`
值接收者 vs 指针接收者:
- 值接收者: 方法接收的是类型的一个副本。对接收者的修改不会影响原始值。适用于方法只需要读取接收者的值,或者接收者是小对象。
- 指针接收者: 方法接收的是类型的一个指针。对接收者(通过指针)的修改会影响原始值。适用于方法需要修改接收者的状态,或者接收者是大对象(避免复制开销)。
选择哪种接收者取决于你的方法是否需要修改接收者的状态。通常,如果结构体较大或者方法需要修改字段,使用指针接收者;否则,使用值接收者。
接口(Interfaces)
接口是一组方法签名的集合。一个类型只要实现了接口中定义的所有方法,就被认为实现了该接口。Go 语言的接口实现是隐式的,不需要使用关键字声明。
“`go
// 定义一个 Shape 接口
type Shape interface {
Area() float64 // 要求实现一个 Area() 方法,返回 float64
Perimeter() float64 // 要求实现一个 Perimeter() 方法,返回 float64
}
// 实现 Shape 接口的 Rectangle 类型
type Rectangle struct {
Width float64
Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
// 实现 Shape 接口的 Circle 类型
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return 3.14159 * c.Radius * c.Radius // 简单用圆周率近似值
}
func (c Circle) Perimeter() float64 {
return 2 * 3.14159 * c.Radius
}
func main() {
// Shape 接口类型的变量可以持有任何实现了 Shape 接口的值
var s Shape
rect := Rectangle{Width: 10, Height: 5}
s = rect // Rectangle 实现了 Shape 接口
fmt.Println("矩形面积:", s.Area()) // 调用 Shape 接口方法
fmt.Println("矩形周长:", s.Perimeter())
circle := Circle{Radius: 3}
s = circle // Circle 也实现了 Shape 接口
fmt.Println("圆形面积:", s.Area()) // 调用 Shape 接口方法
fmt.Println("圆形周长:", s.Perimeter())
}
“`
接口实现了多态性:接口类型的变量可以存储任何实现了该接口的具体类型的值。这使得代码更加灵活和通用。
空接口 (interface{}
或 any
):
空接口没有方法签名,因此任何类型都实现了空接口。空接口可以用来存储任意类型的值。从 Go 1.18 开始,可以使用 any
作为 interface{}
的别名,更易读。
“`go
func describe(i any) { // 参数类型是 any (interface{})
fmt.Printf(“类型: %T, 值: %v\n”, i, i)
}
func main() {
describe(10) // int 类型
describe(“hello”) // string 类型
describe(true) // bool 类型
describe([]int{1,2}) // []int 类型
}
“`
空接口常用于需要处理不确定类型的情况,但使用时需要进行类型断言或类型选择来获取其底层具体值。
类型断言:
用于从接口值中提取其底层具体值。
“`go
func main() {
var i any = “hello”
s, ok := i.(string) // 断言 i 的底层类型是否是 string
if ok {
fmt.Printf("i 是字符串,值为: %q\n", s)
} else {
fmt.Println("i 不是字符串")
}
f, ok := i.(float64) // 断言 i 的底层类型是否是 float64
if ok {
fmt.Printf("i 是浮点数,值为: %f\n", f)
} else {
fmt.Println("i 不是浮点数") // 这个分支会被执行
}
}
``
value, ok := i.(Type)
双返回值语法是安全的断言方式,如果断言失败,
ok为
false,
value为该类型的零值,不会发生 panic。单返回值
value := i.(Type)` 如果断言失败会导致 panic。
类型选择 (switch
on type):
用于判断接口值的底层类型,并根据不同类型执行相应的代码块。
“`go
func do(i any) {
switch v := i.(type) { // v 在每个 case 中是对应类型的变量
case int:
fmt.Printf(“是整型 %v 的两倍是 %v\n”, v, v*2)
case string:
fmt.Printf(“是字符串 %q,长度是 %v\n”, v, len(v))
default:
fmt.Printf(“未知类型 %T 值为 %v\n”, v, v)
}
}
func main() {
do(21)
do(“hello”)
do(true)
}
“`
8. 指针(Pointers)
Go 语言有指针,但没有指针算术(不能直接对指针进行加减操作),这使得指针的使用更安全。指针用于存储变量的内存地址。
什么是指针?
一个指针变量指向一个变量的内存地址。
“`go
func main() {
i := 42 // 一个 int 变量
p := &i // p 是一个指向 i 的指针,& 操作符用于获取变量的地址
fmt.Println(i) // 输出 i 的值: 42
fmt.Println(p) // 输出 i 的内存地址 (例如: 0xc000014080)
fmt.Println(*p) // * 操作符用于读取指针指向地址的值 (解引用),输出 42
*p = 21 // 通过指针修改 i 的值
fmt.Println(i) // 输出 i 的新值: 21
}
“`
获取地址 (&
):
&
操作符返回其操作数的内存地址。
通过指针访问值 (*
):
*
操作符用于访问指针指向的内存地址上的值。这称为解引用或间接引用。
为什么 Go 语言需要指针?
- 修改函数参数的原值: Go 函数参数传递默认是值传递。如果想在函数内部修改传入参数的原值,需要传递指针。
- 避免大对象复制: 对于较大的结构体等,通过传递指针而不是复制整个值可以提高效率。
“`go
func zeroValue(x int) { // 值传递
x = 0 // 只改变了副本
}
func zeroPointer(x int) { // 指针传递
x = 0 // 改变了原值
}
func main() {
i := 10
zeroValue(i)
fmt.Println(“zeroValue 后:”, i) // 输出 10
j := 10
zeroPointer(&j) // 传递 j 的地址
fmt.Println("zeroPointer 后:", j) // 输出 0
}
“`
结构体方法中的指针接收者也是基于指针的原理。
9. 并发(Concurrency)
并发是 Go 语言的一大亮点。Go 的并发模型基于 CSP (Communicating Sequential Processes),通过 goroutines 和 channels 实现。
并发 vs 并行:
- 并发 (Concurrency): 两个或多个任务逻辑上同时进行,但在单核 CPU 上可能通过时间片轮转实现交替执行。
- 并行 (Parallelism): 两个或多个任务物理上同时执行,需要多核 CPU。
Go 的并发特性使得编写易于理解的并发代码变得容易,而 Go 运行时可以利用多核 CPU 实现并行。
goroutines(轻量级线程)
goroutine 是 Go 语言中轻量级的执行单元,可以看作是由 Go 运行时管理的线程。创建一个 goroutine 的开销非常小。
使用 go
关键字即可启动一个 goroutine。
“`go
import (
“fmt”
“time”
)
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond) // 暂停 100 毫秒
fmt.Println(s)
}
}
func main() {
go say(“world”) // 启动一个新的 goroutine
say(“hello”) // 在主 goroutine 中执行
// 主 goroutine 执行完毕后,程序会退出,即使其他 goroutine 还没执行完。
// 为了看到 "world" 的输出,需要主 goroutine 等待一下。
// time.Sleep(time.Second) // 临时等待,非推荐方式
}
``
say(“hello”)` 后如果没有其他需要执行的代码,程序会立即退出,因此上面的代码可能看不到完整的 “world” 输出。实际开发中,我们会使用 channels 或 sync 包来协调 goroutines。
运行上述代码,你会看到 "hello" 和 "world" 的输出是交织在一起的,说明它们在并发执行。主 goroutine 执行完
Channels(通道)
Channels 是用于 goroutines 之间通信的管道。通过通道发送和接收数据是 Go 语言推荐的并发同步方式,它遵循 “不要通过共享内存来通信,而是通过通信来共享内存” 的原则。
什么是通道?为什么使用通道?
通道是一种类型化的管道,可以通过它发送和接收指定类型的值。使用通道的目的是安全地在 goroutines 之间传递数据,避免直接操作共享内存可能导致的竞态条件。
创建通道 (make
):
使用 make
函数创建通道。
“`go
// 创建一个无缓冲的整型通道
ch := make(chan int)
// 创建一个有缓冲的整型通道,容量为 10
bufferedCh := make(chan int, 10)
“`
发送和接收数据 (<-
):
channel <- value
: 将value
发送到通道channel
。value := <-channel
: 从通道channel
接收一个值,并赋给value
。
“`go
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // 将计算结果发送到通道 c
}
func main() {
a := []int{7, 2, 8, -9, 4, 0}
// 创建一个通道
c := make(chan int)
// 将数组分成两半,分别用两个 goroutine 计算和
go sum(a[:len(a)/2], c) // 计算前半部分
go sum(a[len(a)/2:], c) // 计算后半部分
// 从通道接收结果。接收操作是阻塞的,直到有数据可读。
x := <-c // 接收第一个 goroutine 的结果
y := <-c // 接收第二个 goroutine 的结果
fmt.Println(x, y, x+y) // 输出两个部分的和以及总和
}
“`
无缓冲通道与有缓冲通道:
- 无缓冲通道 (unbuffered channel):
make(chan Type)
. 发送和接收操作是阻塞的。发送方会阻塞直到有接收方接收数据,接收方会阻塞直到有发送方发送数据。 - 有缓冲通道 (buffered channel):
make(chan Type, capacity)
. 容量指定了通道可以存储多少个元素而无需阻塞。发送到有缓冲通道时,如果容量未满则不会阻塞;如果满了则阻塞。从有缓冲通道接收时,如果通道非空则不会阻塞;如果为空则阻塞。
“`go
func main() {
ch := make(chan int, 2) // 创建一个容量为 2 的有缓冲通道
ch <- 1 // 发送 1 (不阻塞,因为容量未满)
ch <- 2 // 发送 2 (不阻塞,因为容量未满)
// ch <- 3 // 发送 3 会阻塞,因为容量已满
fmt.Println(<-ch) // 接收 1
fmt.Println(<-ch) // 接收 2
}
“`
关闭通道 (close
):
发送方可以关闭一个通道,表明不再有值发送。接收方可以通过接收操作的第二个返回值判断通道是否已关闭且没有更多数据。
“`go
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
close(ch) // 关闭通道
// 从已关闭的通道接收数据,会立即返回通道中剩余的数据,
// 然后再接收会返回零值和 false
v, ok := <-ch
fmt.Println(v, ok) // 输出 1 true
v, ok = <-ch
fmt.Println(v, ok) // 输出 2 true
v, ok = <-ch // 通道已关闭且无数据
fmt.Println(v, ok) // 输出 0 false (int 的零值)
// 注意:向已关闭的通道发送数据会导致 panic。
// close(ch) // 再次关闭已关闭的通道也会导致 panic。
}
“`
使用 range
遍历通道:
可以使用 for range
循环从通道接收数据,直到通道被关闭且没有更多数据。
“`go
func producer(ch chan int) {
for i := 0; i < 5; i++ {
ch <- i // 发送数据
}
close(ch) // 数据发送完毕,关闭通道
}
func consumer(ch chan int) {
// 使用 range 遍历通道,直到通道关闭且无数据
for value := range ch {
fmt.Println(“接收到:”, value)
}
fmt.Println(“通道已关闭,消费者退出”)
}
func main() {
ch := make(chan int) // 无缓冲通道
go producer(ch) // 启动生产者 goroutine
consumer(ch) // 在主 goroutine 中作为消费者
}
“`
select
语句(多路复用):
select
语句用于同时等待多个通道操作。它会阻塞直到其中一个通信操作准备就绪。
“`go
func main() {
c1 := make(chan string)
c2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
c1 <- "one"
}()
go func() {
time.Sleep(2 * time.Second)
c2 <- "two"
}()
for i := 0; i < 2; i++ { // 接收两次
select { // 监听多个通道
case msg1 := <-c1: // 如果 c1 可读
fmt.Println("received", msg1)
case msg2 := <-c2: // 如果 c2 可读
fmt.Println("received", msg2)
// default: // 可选的 default case,如果没有通道准备好,则执行 default (非阻塞)
// fmt.Println("no activity")
// time.Sleep(50 * time.Millisecond)
}
}
}
``
case
如果多个都准备就绪,
select会随机选择一个执行。
default子句使得
select` 成为非阻塞的。
Sync 包(简要提及)
虽然 Go 鼓励使用通道进行并发同步,但标准库的 sync
包也提供了一些传统的并发原语,如互斥锁 (sync.Mutex
)、读写锁 (sync.RWMutex
)、条件变量 (sync.Cond
)、原子操作 (sync/atomic
)、等待组 (sync.WaitGroup
) 等,用于更细粒度的控制共享内存访问。对于初学者,掌握 goroutines 和 channels 是关键。
10. 错误处理
Go 语言没有传统的异常处理机制(如 try-catch
)。它采用的是更简洁、更强制的方式:函数通过返回一个特殊的 error
类型值来表示是否发生了错误。
Go 语言的错误处理哲学:
- 错误是预期的、常见的情况,应该像处理普通返回值一样处理它们。
- 错误处理应该显式化,让开发者清楚地知道哪些操作可能失败,并强制他们处理这些错误。
error
接口:
error
是一个内置接口,定义了一个 Error()
方法,返回一个字符串,描述错误信息。
go
type error interface {
Error() string
}
返回错误:
通常,可能出错的函数会返回多个值,其中最后一个是 error
类型。如果操作成功,error
返回值为 nil
;如果失败,返回一个非 nil
的错误值。
“`go
import (
“errors”
“fmt”
)
// 一个可能返回错误的函数
func Divide(a, b float64) (float64, error) {
if b == 0 {
// 返回零值和错误
return 0, errors.New(“除数不能为0”) // errors.New 创建一个简单的错误
}
// 返回结果和 nil (表示无错误)
return a / b, nil
}
func main() {
result, err := Divide(10, 2) // 调用函数并接收返回值和错误
if err != nil { // 检查错误是否为 nil
fmt.Println(“发生错误:”, err)
} else {
fmt.Println(“结果:”, result) // 输出 结果: 5
}
result, err = Divide(10, 0)
if err != nil { // 发生错误
fmt.Println("发生错误:", err) // 输出 发生错误: 除数不能为0
} else {
fmt.Println("结果:", result)
}
}
“`
处理错误 (if err != nil
):
这是 Go 语言中最常见的错误处理模式。每次调用可能返回错误的函数后,都应该立即检查 err
是否为 nil
。
go
// 这是Go中最常见的错误处理模式
res, err := someFunctionThatMightFail()
if err != nil {
// 错误发生了,在这里处理错误
// 例如:打印错误,返回错误,记录日志,重试等等
fmt.Println("Error:", err)
return // 或者其他错误处理逻辑
}
// 如果 error 是 nil,继续执行成功后的逻辑
fmt.Println("Success:", res)
创建自定义错误:
除了使用 errors.New
创建简单错误,还可以创建实现 error
接口的自定义错误类型,以携带更多错误信息。
“`go
// 定义一个自定义错误类型
type DivideByZeroError struct {
Message string
Code int
}
// 实现 error 接口的 Error() 方法
func (e *DivideByZeroError) Error() string {
return fmt.Sprintf(“错误码 %d: %s”, e.Code, e.Message)
}
func DivideCustom(a, b float64) (float64, error) {
if b == 0 {
return 0, &DivideByZeroError{ // 返回自定义错误类型的指针
Message: “除数不能为0”,
Code: 1001,
}
}
return a / b, nil
}
func main() {
_, err := DivideCustom(10, 0)
if err != nil {
fmt.Println(“发生错误:”, err) // 输出 发生错误: 错误码 1001: 除数不能为0
// 可以使用类型断言检查错误是否是特定类型
if divErr, ok := err.(*DivideByZeroError); ok {
fmt.Println("这是一个除零错误,错误码:", divErr.Code)
}
}
}
“`
11. 包与模块化
Go 语言的代码组织基于包。Go Modules 是现代 Go 项目管理依赖的标准方式。
标准库(Standard Library):
Go 语言拥有强大的标准库,开箱即用,无需额外安装。常用的标准库包包括:
fmt
: 格式化输入输出strings
: 字符串操作bytes
: 字节切片操作io
: 输入输出原语os
: 操作系统接口(文件、进程、环境变量等)net/http
: HTTP 客户端和服务端实现encoding/json
: JSON 编码解码time
: 时间操作sync
: 基本同步原语
在代码中通过 import
导入并使用它们,例如 fmt.Println()
。
组织代码到不同的包:
将相关的功能放在同一个包中可以提高代码的可维护性和复用性。一个目录通常代表一个包。
myproject/
├── main.go // package main
├── utils/
│ └── strings.go // package utils
└── models/
└── user.go // package models
在 main.go
中,你可以导入并使用 utils
包和 models
包中的导出元素。
“`go
// myproject/main.go
package main
import (
“fmt”
“myproject/models” // 导入本地 models 包
“myproject/utils” // 导入本地 utils 包
)
func main() {
u := models.User{Name: “Alice”, Age: 30}
fmt.Println(u.Name)
greeting := utils.Capitalize("hello world")
fmt.Println(greeting)
}
“`
“`go
// myproject/models/user.go
package models
// User 结构体 (大写开头,可导出)
type User struct {
Name string // 字段大写开头,可导出
Age int // 字段大写开头,可导出
// email string // 字段小写开头,包私有
}
// GetName 方法 (大写开头,可导出)
func (u *User) GetName() string {
return u.Name
}
“`
“`go
// myproject/utils/strings.go
package utils
import “strings”
// Capitalize 函数 (大写开头,可导出)
func Capitalize(s string) string {
return strings.ToUpper(s) // 调用标准库 strings 包的 ToUpper 函数
}
// toLower 函数 (小写开头,包私有)
func toLower(s string) string {
return strings.ToLower(s)
}
“`
可见性规则:
如前所述,Go 语言通过首字母大小写控制可见性:
- 大写开头: 标识符(变量、函数、类型、方法、结构体字段)是可导出的(public),可以在包外访问。
- 小写开头: 标识符是包私有的(private),只能在当前包内访问。
Go Modules 工作原理简介:
Go Modules 是 Go 语言官方的依赖管理工具。
- 初始化模块: 在项目根目录运行
go mod init <module path>
。<module path>
通常是你托管代码仓库的路径,如github.com/yourname/yourproject
。这会创建一个go.mod
文件。 - 管理依赖: 当你使用
import
语句导入一个外部包时,go build
或go run
命令会自动下载该依赖的最新版本(或go.mod
文件中指定的版本),并记录到go.mod
文件中。go.sum
文件记录依赖的校验和,用于验证依赖的完整性。 - 获取依赖:
go get <package path>
命令用于获取或更新特定的依赖。 - 整理依赖:
go mod tidy
命令会移除不再需要的依赖,并添加项目中新增的依赖。
Go Modules 使得项目的依赖管理更加独立和可控,不再强制依赖 GOPATH
。
12. 测试
Go 语言内置了轻量级的测试框架,无需额外安装。
Go 语言的测试框架:
测试文件以 _test.go
结尾,并且通常与被测试的源文件在同一目录。测试函数以 Test
开头,接收一个 *testing.T
参数。
“`go
// calculator.go
package calculator
func Add(a, b int) int {
return a + b
}
func Subtract(a, b int) int {
return a – b
}
“`
“`go
// calculator_test.go (与 calculator.go 在同一目录下的 calculator 包中)
package calculator
import “testing” // 导入内置的 testing 包
// 测试 Add 函数
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
// t.Errorf() 会标记测试失败并打印消息,但测试会继续
t.Errorf(“Add(2, 3) 期望 %d, 实际 %d”, expected, result)
}
result = Add(-1, 1)
expected = 0
if result != expected {
t.Errorf("Add(-1, 1) 期望 %d, 实际 %d", expected, result)
}
}
// 测试 Subtract 函数
func TestSubtract(t *testing.T) {
result := Subtract(5, 2)
expected := 3
if result != expected {
// t.Fatalf() 会标记测试失败并打印消息,然后立即中止测试函数
t.Fatalf(“Subtract(5, 2) 期望 %d, 实际 %d”, expected, result)
}
}
“`
运行测试 (go test
):
在包含测试文件的目录中打开终端,运行:
bash
go test
Go 会自动找到并运行当前包下的所有 _test.go
文件中的测试函数。
输出示例(如果测试通过):
ok your_module_path/calculator 0.00x s
输出示例(如果测试失败):
--- FAIL: TestAdd (0.00s)
calculator_test.go:12: Add(-1, 1) 期望 0, 实际 -2
FAIL
exit status 1
FAIL your_module_path/calculator 0.00s
go test
还有许多选项,例如 -v
(显示详细信息)、-cover
(生成代码覆盖率报告) 等。
13. 总结与下一步
恭喜你!你已经完成了 Go 语言入门的主要内容。我们学习了 Go 语言的基本语法、数据类型、控制流程、函数、切片、映射、结构体、方法、接口、指针,以及 Go 最重要的特性——并发模型 (goroutines 和 channels) 和错误处理机制。
Go 语言以其简洁、高效、强大的并发能力在现代软件开发中占据着越来越重要的地位。掌握这些基础知识,你已经具备了编写简单的 Go 程序的能力。
接下来,你可以:
- 动手实践: 尝试编写一些小程序来巩固学到的知识。例如,编写一个简单的命令行工具,一个 TCP 服务器,或者一个处理 JSON 数据的程序。
- 深入标准库: 花时间探索 Go 的标准库,了解更多强大的内置功能,如
net/http
、os
、io
、time
、encoding/json
等。 - 学习更多高级主题: 探索更深入的 Go 特性,如 context、reflect、unsafe、cgo 等。
- 阅读优秀 Go 代码: 阅读一些流行的 Go 开源项目的代码,学习它们的结构和设计模式。
- 参与社区: 加入 Go 语言社区,提问、交流,参与开源项目。
学习编程语言是一个持续的过程,不断实践和探索是进步的关键。希望这篇教程为你打开了通往 Go 语言世界的大门!祝你在 Go 语言的学习和开发之路上一切顺利!