这是一篇实践文章。在本篇文章中,将会介绍Go语言的常用特性解析和部分常用基础语法。
1. 基础语法
因为部分基础语法已在之前的笔记文章中有过叙述,在此不做过多介绍。再补充为介绍到的部分基础语法。
1.1 Package
一般情况下,一个文件夹可以作为一个 package,同一个 package 内部的变量、类型、方法等可以相互看到。
Go 语言也有 Public
和 Private
的概念,粒度是包。
如果类型
/接口
/方法
/函数
/字段
的首字母大写
,则是 Public
的,对其他 package 可见
,如果首字母小写
,则是 Private
的,对其他 package 不可见
。
1.2 Modules
Go Modules
是 Go 1.11
版本之后引入,之前使用 $GOPATH
机制。
Go Modules 是较为完善的包管理工具。同时支持代理,可以配置高速的第三方镜像包下载服务。
通过 go mod init '文件名'
初始化Module。 在当前文件夹下生成了go.mod
,这个文件是用来记录当前模块的模块名以及所有依赖包的版本。
若是在import()里添加外部依赖,运行 go run.
会自动触发依赖包的下载,并将具体的版本信息记录在go.mod
里。
若是在包中新增了子包,只需要在需要引入子包内容的文件import中添加模块名/子目录名即可。
1.3 字符串操作
- 字符串长度: 使用
len()
函数可以获取字符串的长度,即字符串中字符的个数。
go
str := "Hello, World!"
length := len(str)
fmt.Println(length) // 输出:13
- 字符串拼接: 使用
+
操作符可以将两个字符串进行拼接。
go
str1 := "Hello"
str2 := "World"
result := str1 + " " + str2
fmt.Println(result) // 输出:Hello World
- 字符串截取: 使用切片操作可以截取字符串的一部分。
go
str := "Hello, World!"
substring := str[0:5]
fmt.Println(substring) // 输出:Hello
- 字符串查找: 使用strings包中的
Contains
函数可以在字符串中查找特定的子串。
go
str := "Hello, World!"
contains := strings.Contains(str, "World")
fmt.Println(contains) // 输出:true
- 字符串替换: 使用strings包中的
Replace
函数可以将字符串中的某个子串替换为另一个子串。
go
str := "Hello, World!"
newStr := strings.Replace(str, "World", "Golang", -1)
fmt.Println(newStr) // 输出:Hello, Golang!
- 字符串分割: 使用strings包中的
Split
函数可以将字符串按照指定的分隔符进行分割。
go
str := "Hello, World!"
parts := strings.Split(str, ", ")
fmt.Println(parts) // 输出:[Hello World!]
- 字符串转换: 使用strconv包中的
Atoi
函数可以将字符串转换为其他类型的值,如整数、浮点数等。
go
str := "100"
num, _ := strconv.Atoi(str)
fmt.Println(num) // 输出:100
- 字符串大小写转换: 使用strings包中的
ToUpper
,ToLower
函数可以将字符串中的字母转换为大写或小写。
go
str := "Hello, World!"
upper := strings.ToUpper(str)
lower := strings.ToLower(str)
fmt.Println(upper) // 输出:HELLO, WORLD!
fmt.Println(lower) // 输出:hello, world!
- 字符串统计: 使用strings包中的
Count
函数用于统计一个子串在字符串中出现的次数。
go
str := "Hello, Hello, Hello"
count := strings.Count(str, "Hello")
fmt.Println(count) // 输出:3
- 字符串前缀判断: 使用strings包中的
HasPrefix
函数用于判断字符串是否以指定的前缀开头。
go
str := "Hello, World!"
hasPrefix := strings.HasPrefix(str, "Hello")
fmt.Println(hasPrefix) // 输出:true
- 字符串后缀判断: 使用strings包中的
HasSuffix
函数用于判断字符串是否以指定的后缀结尾。
go
str := "Hello, World!"
hasSuffix := strings.HasSuffix(str, "World!")
fmt.Println(hasSuffix) // 输出:true
- 字符串字串首位判断: 使用strings包中的
Index
函数用于查找子串在字符串中第一次出现的位置。
go
str := "Hello, World!"
index := strings.Index(str, "World")
fmt.Println(index) // 输出:7
- 字符串切片连接: 使用strings包中的
Join
函数用于将字符串切片按照指定的分隔符连接成一个字符串。
go
strs := []string{"Hello", "World", "Golang"}
result := strings.Join(strs, "-")
fmt.Println(result) // 输出:Hello-World-Golang
- 字符串重复: 使用strings包中的
Repeat
函数用于将一个字符串重复多次。
go
str := "Hello"
result := strings.Repeat(str, 3)
fmt.Println(result) // 输出:HelloHelloHello
2. 常用特性解析
2.1 并发编程 goroutine
Go语言应该是实现并发最简易的语言。Go 语言提供了 sync 和 channel 两种方式支持协程(goroutine)的并发。
- goroutine(协程): goroutine是Go语言中的轻量级线程,可以在并发编程中实现并行执行。通过关键字
go
可以创建一个新的goroutine,它会在一个独立的线程中运行。与传统的线程相比,goroutine的创建和销毁开销很小,可以高效地创建大量的goroutine。 - channel(通道): channel是用于在不同goroutine之间进行通信和同步的机制。它可以用于在goroutine之间传递数据,实现数据的安全共享。channel提供了阻塞和非阻塞的发送和接收操作,可以确保数据的同步和顺序。
sync
包是一个非常重要的包,它提供了一些用于同步和保护共享资源的机制
- mutex(互斥锁): mutex是一种用于保护共享资源的锁机制。通过使用mutex,可以确保在同一时间只有一个goroutine可以访问共享资源,从而避免了竞态条件和数据竞争。
- WaitGroup(等待组): WaitGroup用于
等待一组goroutine的执行完成
。通过Add、Done和Wait方法,可以控制并发执行的goroutine数量,并在所有goroutine执行完成后进行等待
。 接下来是一个使用sync.WaitGroup实现并发下载N个资源的示例代码:
go
package main
import (
"fmt"
"net/http"
"sync"
)
// 创建一个WaitGroup
var wg sync.WaitGroup
func main() {
// 定义下载的资源数量
resourceCount := 5
// 设置WaitGroup的计数器
wg.Add(resourceCount)
// 并发下载资源
for i := 0; i < resourceCount; i++ {
go func(index int) {
// 在goroutine执行完成后,调用Done方法减少WaitGroup的计数器
defer wg.Done()
// 构造要下载的资源URL
url := fmt.Sprintf("https://example.com/resource-%d", index)
// 发起HTTP请求下载资源
resp, err := http.Get(url)
if err != nil {
fmt.Printf("Error downloading resource %d: %v\n", index, err)
return
}
defer resp.Body.Close()
fmt.Printf("Resource %d downloaded\n", index)
}(i)
}
// 等待所有并发协程执行结束
wg.Wait()
fmt.Println("All resources downloaded")
}
首先定义了要下载的资源数量resourceCount,并创建了一个WaitGroup。然后,使用for循环并发地下载资源。每个goroutine执行完成后,都会调用WaitGroup的Done方法来减少计数器。最后,调用Wait方法来阻塞主goroutine,直到所有的goroutine都执行完成。
运行测试后并发执行与非并发执行速度大大提升。
下面是通过模拟生产者和消费者的场景来使用channel:
go
package main
import (
"fmt"
"time"
)
func producer(ch chan<- int) {
for i := 0; i < 5; i++ {
fmt.Println("Producer sending:", i)
ch <- i // 发送数据到channel
time.Sleep(time.Second)
}
close(ch) // 关闭channel
}
func consumer(ch <-chan int) {
for num := range ch {
// 从channel接收数据,直到channel关闭
fmt.Println("Consumer received:", num)
time.Sleep(2 * time.Second)
}
}
func main() {
ch := make(chan int) // 创建一个无缓冲的channel
go producer(ch) // 启动生产者goroutine
go consumer(ch) // 启动消费者goroutine
time.Sleep(10 * time.Second) // 等待一段时间,让生产者和消费者完成
}
首先创建了一个无缓冲的channel,用于在生产者和消费者之间传递数据。生产者goroutine通过for循环向channel发送数据,并使用time.Sleep模拟一段耗时的生产过程。消费者goroutine通过range循环从channel接收数据,并使用time.Sleep模拟一段耗时的消费过程。
在main函数中,启动生产者和消费者的goroutine,并通过time.Sleep等待一段时间,完成生产和消费操作。
在生产者函数中使用ch <- i
语句将数据发送到channel,而在消费者函数中使用num := <-ch
语句从channel接收数据。在接收数据时,使用range循环来遍历channel中的所有数据。
生产者函数中使用了close(ch)
语句来关闭channel。关闭channel后,接收方仍然可以从channel中接收数据,但是不能再向channel发送数据。在消费者函数中,当channel关闭后,for循环会自动结束。
channel的用法:通过ch <- data
向channel发送数据,通过data := <-ch
从channel接收数据,使用close(ch)
关闭channel。channel可以实现不同goroutine之间的数据传递和同步。
2.2 单元测试 unit test
- 所有测试文件以 test.go 结尾
- func TestXxx(*testing.T)
- 初始化逻辑放到 TestMain 中
单元测试-覆盖率
- 一般覆盖率:50%~60%,较高覆盖率80%+
- 测试分支相互独立、全面覆盖
- 测试单元粒度足够小,函数单一职责
下面是一个Go语言单元测试示例:
go
package main
import (
"testing"
)
// 计算两个整数的和
func Add(a, b int) int {
return a + b
}
// 测试函数
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d; expected %d", result, expected)
}
}
定义了一个函数Add,用于计算两个整数的和。然后,编写TestAdd的测试函数,用于测试Add函数的功能。
在测试函数中,调用Add函数并将结果与预期值进行比较。如果结果与预期值不相等,使用t.Errorf函数来输出错误信息。
运行单元测试,在命令行中使用go test命令:
go
$ go test
运行测试时,Go语言会自动查找当前目录及其子目录中的_test.go文件,并执行其中的测试函数。
2.3 基准测试
Go语言提供了基准测试框架,基准测试是指测试一段程序的运行性能及耗费 CPU 的程度,而我们在实际项目开发中,经常会遇到代码性能瓶颈,为了定位问题经常要对代码做性能分析,这就用到了基准测试。使用方法类似于单元测试。
下面是一个简单的Go语言基准测试示例:
go
package main
import (
"testing"
)
// 计算斐波那契数列的第n个数
func Fibonacci(n int) int {
if n <= 1 {
return n
}
return Fibonacci(n-1) + Fibonacci(n-2)
}
// 基准测试函数
func BenchmarkFibonacci(b *testing.B) {
for i := 0; i < b.N; i++ {
Fibonacci(20)
}
}
定义了一个函数Fibonacci,用于计算斐波那契数列的第n个数。然后,编写了一个名为BenchmarkFibonacci的基准测试函数,用于测试Fibonacci函数的性能。
在基准测试函数中,使用b.N来指定要运行的迭代次数。在每次迭代中,调用Fibonacci函数来计算斐波那契数列的第20个数。
运行基准测试,在命令行中使用go test命令,并指定-bench参数:
go
$ go test -bench=.
运行基准测试时,Go语言会自动查找当前目录及其子目录中的_test.go文件,并执行其中的基准测试函数。通过-bench参数,可以指定要运行的基准测试函数。
基准测试会输出每次迭代的执行时间和每秒执行的迭代次数,以及其他相关信息。
3. 常用库
3.1 Fmt常用库
输出 fmt.Print
三个函数 Print, Println, Printf最终都调用Fpintf。
- 格式化占位符之通用占位符
占位符 | 说明 |
---|---|
%v | 值的默认格式表示 |
%+v | 类似%v,但输出结构体时会添加字段名 |
%#v | 值的Go语法表示 |
%T | 打印值的类型 |
%% | 百分号 |
- 格式化占位符之布尔型
占位符 | 说明 |
---|---|
%t | true或false |
- 格式化占位符之整型
占位符 | 说明 |
---|---|
%b | 表示为二进制 |
%c | 该值对应的unicode码值 |
%d | 表示为十进制 |
%o | 表示为八进制 |
%x | 表示为十六进制,使用a-f |
%X | 表示为十六进制,使用A-F |
%U | 表示为Unicode 格式:U+1234,等价于"U+%04X" |
%q | 该值对应的单引号括起来的go语法字符字面值,必要时会采用安全的转义表示 |
- 格式化占位符之复数
- 格式化占位符之字符串和[]byte
占位符 | 说明 |
---|---|
%s | 直接输出字符串或者[]byte |
%q | 该值对应的双引号括起来的go语法字符串字面值,必要时会采用安全的转义表示 |
%x | 每个字节用两字符十六进制数表示(使用a-f) |
%X | 每个字节用两字符十六进制数表示(使用A-F) |
- 格式化占位符之指针
- 格式化占位符之宽度标识符
- 格式化占位符之其他特殊符
- Fprint。将内容输出到一个io.Writer接口类型的变量w中。一般用在写文件中。
- Sprint。把传入的数据生成并返回一个字符串。
- Errorf。根据format参数生成格式化字符串并返回一个包含该字符串的错误。
fmt.Scan
- 从标准输入扫描文本,读取由空白符分隔的值保存到传递给本函数的参数中,换行符视为空白符
- 本函数返回成功扫描的数据个数(n)和遇到的任何错误(error)
fmt.Scanf 实际使用的是Fscanf
,以format定义的格式来进行输入。
fmt.Scanln
Scanln
函数会一次性读取多个输入值,并将它们存储到传入的参数中。使用Scanln
函数时,需要提供与输入值对应的变量作为参数。
fmt.Fsanf 将内容从一个io.Reader接口类型的变量r中读取出来,将连续的以空格分隔的值存储到由格式确定的连续的参数中。
3.2 Json处理
对于一个已有的结构体,每个字段的第一字母是大写,也就是公开字段,那这个结构体就用JSON.Marshal去序列化,变成一个JSON 的字符串。 序列化之后的字符串也能够用JSON.Unmarshal去反序列化到一个空的变量里面。 默认序列化出来的字符串,风格是大写字母开头,而不是下划线。 可以在后面用json tag 等语法来去修改输出 JSON 结果里面的字段名。
3.3 时间处理
-
time
包:time
包是Go语言中用于处理时间的核心包。提供了用于表示和操作时间的类型和函数。time.Now()
函数:用于获取当前的时间。time.Parse(layout, value)
函数:用于将字符串解析为时间类型。time.Format(layout, t)
函数:用于将时间格式化为字符串。time.Duration
类型:表示时间间隔,可以用于计算时间差等操作。
-
time.Time
类型:time.Time
类型表示一个具体的时间点。包含了年、月、日、时、分、秒、纳秒等信息,可以进行各种时间操作。t.Year()
、t.Month()
、t.Day()
等方法:用于获取时间的年、月、日等信息。t.Hour()
、t.Minute()
、t.Second()
等方法:用于获取时间的小时、分钟、秒等信息。t.Add(duration)
方法:用于在时间上增加一个时间间隔。t.Sub(u)
方法:用于计算两个时间之间的时间间隔。
-
time.Duration
类型:time.Duration
类型表示一个时间间隔。可以用于表示一段时间的长度,例如1小时、30分钟等。time.Second
、time.Minute
、time.Hour
等常量:表示不同单位的时间间隔。duration.Hours()
、duration.Minutes()
、duration.Seconds()
等方法:用于获取时间间隔的小时、分钟、秒等信息。
-
time.Timer
类型:time.Timer
类型表示一个定时器,用于在指定的时间后触发一个事件。time.NewTimer(duration)
函数:创建一个定时器,指定触发事件的时间间隔。timer.Reset(duration)
方法:重置定时器的触发时间。<-timer.C
表达式:在定时器的通道上阻塞,直到定时器触发。
-
time.Tick(duration)
函数:创建一个定时器的通道,每隔指定的时间间隔就会向通道发送一个时间值。 -
time.Sleep(duration)
函数:使当前的goroutine休眠指定的时间间隔。 -
time.LoadLocation(name)
函数:根据时区名字加载对应的时区。
3.4 数字解析
-
strconv
包:strconv
包是Go语言中用于字符串和基本数据类型之间转换的核心包。提供了用于数字解析的函数。strconv.Atoi(s)
函数:将字符串解析为int
类型的整数。如果解析失败,会返回错误。strconv.ParseInt(s, base, bitSize)
函数:将字符串解析为指定进制和位数的整数。base
表示进制,可以是2、8、10、16;bitSize
表示整数的位数,可以是0、8、16、32、64。如果解析失败,会返回错误。strconv.ParseFloat(s, bitSize)
函数:将字符串解析为指定位数的浮点数。bitSize
表示浮点数的位数,可以是32、64。如果解析失败,会返回错误。
4. 实践结语
在完成了Go语言入门指南的实践之后,我对这门语言有了更深的了解和体会。Go语言以其简洁、高效和并发安全的特性,成为了现代编程领域中备受关注的语言之一。
首先,Go语言的基础语法非常简洁明了,易于学习和理解。它摒弃了一些复杂的语法和概念,使得代码更加清晰和易读。同时,Go语言还提供了丰富的内置类型和函数,方便开发者进行各种操作和处理。
其次,Go语言的并发特性给我留下了深刻的印象。通过goroutine和channel的机制,可以方便地实现并发编程,充分利用多核处理器的性能。这种并发模型的设计使得编写高效且可靠的并发程序变得更加容易。
此外,Go语言还具有良好的性能和可扩展性。它的编译器能够将代码快速编译成机器码,执行速度较快。同时,Go语言还提供了丰富的标准库和第三方库,可以满足各种需求,从而提高开发效率。
在这次实践中,我深刻感受到了Go语言的简洁和高效,它让我在编写代码时更加专注于问题的解决,而不是纠结于语法和细节。同时,Go语言的并发特性也让我对并发编程有了更深入的理解和实践。
Go语言是一门非常优秀的编程语言,它的简洁、高效和并发安全的特性使得它在现代编程领域中有着广泛的应用。通过这次实践,我对Go语言的学习和使用充满了信心,我相信它将成为我未来开发的重要工具之一。