Go语言中的并发与数据IO
1. Go语言并发编程
在Go语言并发编程中,有几个重要的概念和技术值得深入探讨。
1.1 通道选择与超时
在Go语言中,
select
语句常被用于处理多个通道操作。在之前的代码片段中,
words
生成器函数会选择第一个成功的通信操作:
out <- word
或
<-stopCh
。只要
main()
函数中的消费者代码继续从
out
通道接收数据,发送操作就会首先成功。不过,当
main()
函数遇到第三个 “the” 实例时,它会关闭
stopCh
通道,此时
select
语句中的接收操作会优先执行,从而使 goroutine 返回。
通道超时是Go并发编程中常见的一种模式,可通过
select
语句结合
time
包的API来实现。以下代码展示了一个单词直方图示例,如果程序统计和打印单词的时间超过200微秒,就会超时:
func main() {
data := []string{...}
histogram := make(map[string]int)
done := make(chan struct{})
go func() {
defer close(done)
words := words(data) // returns handle to channel
for word := range words {
histogram[word]++
}
for k, v := range histogram {
fmt.Printf("%s\t(%d)\n", k, v)
}
}()
select {
case <-done:
fmt.Println("Done counting words!!!!")
case <-time.After(200 * time.Microsecond):
fmt.Println("Sorry, took too long to count.")
}
}
func words(data []string) <-chan string {...}
这个示例引入了
done
通道,用于在处理完成时发出信号。在
select
语句中,
<-done
操作会阻塞,直到 goroutine 关闭
done
通道。
time.After()
函数返回一个通道,该通道会在指定的持续时间后关闭。如果在
done
通道关闭之前200微秒过去了,
time.After()
返回的通道会先关闭,从而使超时情况优先发生。
1.2 sync包的使用
在某些情况下,使用传统方法访问共享值比使用通道更简单和合适。
sync
包提供了几个同步原语,包括互斥锁(mutex)和同步屏障,用于安全地访问共享值。
-
互斥锁同步
:互斥锁允许对共享资源进行串行访问,通过使 goroutine 阻塞并等待锁释放来实现。以下是一个典型的代码场景,
Service类型必须在启动后才能使用,启动后会更新一个内部的bool变量started来存储其当前状态:
type Service struct {
started bool
stpCh chan struct{}
mutex sync.Mutex
}
func (s *Service) Start() {
s.stpCh = make(chan struct{})
go func() {
s.mutex.Lock()
s.started = true
s.mutex.Unlock()
<-s.stpCh // wait to be closed.
}()
}
func (s *Service) Stop() {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.started {
s.started = false
close(s.stpCh)
}
}
func main() {
s := &Service{}
s.Start()
time.Sleep(time.Second) // do some work
s.Stop()
}
上述代码使用
sync.Mutex
类型的
mutex
变量来同步对共享变量
started
的访问。为了有效工作,所有更新
started
变量的竞争区域都必须使用相同的锁,并依次调用
mutex.Lock()
和
mutex.Unlock()
。
还可以将
sync.Mutex
类型直接嵌入结构体中,这样可以将
Lock()
和
Unlock()
方法提升为结构体本身的方法:
type Service struct {
...
sync.Mutex
}
func (s *Service) Start() {
s.stpCh = make(chan struct{})
go func() {
s.Lock()
s.started = true
s.Unlock()
<-s.stpCh // wait to be closed.
}()
}
func (s *Service) Stop() {
s.Lock()
defer s.Unlock()
...
}
-
读写锁(RWMutex)
:
sync包还提供了RWMutex(读写锁),适用于有一个写入者更新共享资源,而可能有多个读取者的情况。写入者使用完全锁更新资源,而读取者使用RLock()/RUnlock()方法对来应用只读锁。以下是一个使用RWMutex同步访问复合值的示例:
type Service struct {
started bool
stpCh chan struct{}
mutex sync.RWMutex
cache map[int]string
}
func (s *Service) Start() {
...
go func() {
s.mutex.Lock()
s.started = true
s.cache[1] = "Hello World"
...
s.mutex.Unlock()
<-s.stpCh // wait to be closed.
}()
}
...
func (s *Service) Serve(id int) {
s.mutex.RLock()
msg := s.cache[id]
s.mutex.RUnlock()
if msg != "" {
fmt.Println(msg)
} else {
fmt.Println("Hello, goodbye!")
}
}
该代码使用
RWMutex
变量来管理对
cache
变量的锁。更新
cache
变量时使用
mutex.Lock()
和
mutex.Unlock()
,读取时使用
mutex.RLock()
和
mutex.RUnlock()
以提供并发安全性。
-
使用
sync.WaitGroup创建并发屏障 :sync.WaitGroup类型用于创建同步屏障,允许等待所有运行的 goroutine 完成后再继续执行。使用WaitGroup需要三个步骤:
1. 通过Add方法设置组中的参与者数量。
2. 每个 goroutine 调用Done方法来信号完成。
3. 使用Wait方法阻塞,直到所有 goroutine 完成。
以下代码展示了如何使用
WaitGroup
来计算3和5的倍数之和:
const MAX = 1000
func main() {
values := make(chan int, MAX)
result := make(chan int, 2)
var wg sync.WaitGroup
wg.Add(2)
go func() { // gen multiple of 3 & 5 values
for i := 1; i < MAX; i++ {
if (i%3) == 0 || (i%5) == 0 {
values <- i // push downstream
}
}
close(values)
}()
work := func() { // work unit, calc partial result
defer wg.Done()
r := 0
for i := range values {
r += i
}
result <- r
}
// distribute work to two goroutines
go work()
go work()
wg.Wait() // wait for both groutines
total := <-result + <-result // gather partial results
fmt.Println("Total:", total)
}
在上述代码中,
wg.Add(2)
配置了
WaitGroup
变量
wg
,因为工作分配给了两个 goroutine。
work
函数调用
defer wg.Done()
在每次完成时将
WaitGroup
计数器减一。最后,
wg.Wait()
方法调用会阻塞,直到其内部计数器达到零。
-
检测竞态条件
:调试具有竞态条件的并发代码可能非常耗时和令人沮丧。幸运的是,从Go 1.1版本开始,Go在其命令行工具链中包含了一个竞态检测器。在构建、测试、安装或运行Go源代码时,只需添加
-race命令标志即可启用竞态检测器。例如,运行带有竞态条件的代码:
$> go run -race sync1.go
编译器的输出将显示导致竞态条件的违规 goroutine 位置。
- Go中的并行性 :Go运行时调度器会自动在可用的操作系统管理的线程上多路复用和调度 goroutine。这意味着可以并行化的并发程序能够在几乎无需配置的情况下利用底层处理器核心。以下代码通过启动多个 goroutine 来计算3和5的倍数之和:
const MAX = 1000
const workers = 2
func main() {
values := make(chan int)
result := make(chan int, workers)
var wg sync.WaitGroup
go func() { // gen multiple of 3 & 5 values
for i := 1; i < MAX; i++ {
if (i%3) == 0 || (i%5) == 0 {
values <- i // push downstream
}
}
close(values)
}()
work := func() { // work unit, calc partial result
defer wg.Done()
r := 0
for i := range values {
r += i
}
result <- r
}
//launch workers
wg.Add(workers)
for i := 0; i < workers; i++ {
go work()
}
wg.Wait() // wait for all groutines
close(result)
total := 0
// gather partial results
for pr := range result {
total += pr
}
fmt.Println("Total:", total)
}
在多核机器上执行时,每个 goroutine 会自动并行启动。Go运行时调度器默认会创建与CPU核心数相等的操作系统支持的线程用于调度,这个数量由
GOMAXPROCS
运行时值标识。可以通过命令行环境变量或
runtime
包中的
GOMAXPROCS()
函数来显式更改
GOMAXPROCS
值,以微调参与调度 goroutine 的线程数量。
2. Go语言中的数据IO
Go语言将数据输入和输出建模为从源到目标的数据流。
io
包提供了
io.Reader
和
io.Writer
接口,用于实现数据的读取和写入操作。
2.1
io.Reader
接口
io.Reader
接口非常简单,只包含一个
Read([]byte)(int, error)
方法,用于将数据从源读取并传输到提供的字节切片中。以下是一个简单的实现,
alphaReader
类型用于过滤非字母字符:
type alphaReader string
func (a alphaReader) Read(p []byte) (int, error) {
count := 0
for i := 0; i < len(a); i++ {
if (a[i] >= 'A' && a[i] <= 'Z') ||
(a[i] >= 'a' && a[i] <= 'z') {
p[i] = a[i]
}
count++
}
return count, io.EOF
}
func main() {
str := alphaReader("Hello! Where is the sun?")
io.Copy(os.Stdout, &str)
fmt.Println()
}
由于
alphaReader
类型实现了
io.Reader
接口,因此可以在任何需要读者的地方使用它。
还可以通过包装现有读者来创建新的读者。以下是更新后的
alphaReader
,它接受一个
io.Reader
作为源:
type alphaReader struct {
src io.Reader
}
func NewAlphaReader(source io.Reader) *alphaReader {
return &alphaReader{source}
}
func (a *alphaReader) Read(p []byte) (int, error) {
if len(p) == 0 {
return 0, nil
}
count, err := a.src.Read(p) // p has now source data
if err != nil {
return count, err
}
for i := 0; i < len(p); i++ {
if (p[i] >= 'A' && p[i] <= 'Z') ||
(p[i] >= 'a' && p[i] <= 'z') {
continue
} else {
p[i] = 0
}
}
return count, io.EOF
}
func main() {
str := strings.NewReader("Hello! Where is the sun?")
alpha := NewAlphaReader(str)
io.Copy(os.Stdout, alpha)
fmt.Println()
}
这种方法的优点是
alphaReader
可以从任何实现了
io.Reader
接口的源读取数据。
2.2
io.Writer
接口
io.Writer
接口同样简单,只包含一个
Write(p []byte)(n int, err error)
方法,用于将数据从提供的字节流写入目标资源。以下是
channelWriter
类型的实现,它将数据流分解并序列化到Go通道中:
type channelWriter struct {
Channel chan byte
}
func NewChannelWriter() *channelWriter {
return &channelWriter{
Channel: make(chan byte, 1024),
}
}
func (c *channelWriter) Write(p []byte) (int, error) {
if len(p) == 0 {
return 0, nil
}
go func() {
defer close(c.Channel) // when done
for _, b := range p {
c.Channel <- b
}
}()
return len(p), nil
}
使用
channelWriter
类型很简单,可以直接调用
Write()
方法,也可以与其他IO原语一起使用。以下是使用
fmt.Fprint
函数将字符串序列化到通道的示例:
func main() {
cw := NewChannelWriter()
go func() {
fmt.Fprint(cw, "Stream me!")
}()
for c := range cw.Channel {
fmt.Printf("%c\n", c)
}
}
也可以使用
io.Copy
函数将文件内容序列化到通道中:
func main() {
cw := NewChannelWriter()
file, err := os.Open("./writer2.go")
if err != nil {
fmt.Println("Error reading file:", err)
os.Exit(1)
}
_, err = io.Copy(cw, file)
if err != nil {
fmt.Println("Error copying:", err)
os.Exit(1)
}
// consume channel
for c := range cw.Channel {
fmt.Printf("%c\n", c)
}
}
通过这些接口和实现,Go语言提供了一种灵活且可互换的方式来处理数据的输入和输出。
总结
Go语言的并发编程和数据IO提供了强大而灵活的工具,帮助开发者编写高效、安全的程序。在并发编程中,通道、
sync
包的同步原语以及竞态检测器等工具使得处理并发问题更加容易。在数据IO方面,
io.Reader
和
io.Writer
接口为数据的读取和写入提供了统一的抽象,使得不同的数据源和目标可以方便地进行交互。掌握这些概念和技术,将有助于开发者更好地利用Go语言的特性。
以下是一个简单的流程图,展示了使用
io.Reader
和
io.Writer
进行数据处理的基本流程:
graph LR
A[数据源] --> B[io.Reader]
B --> C[处理数据]
C --> D[io.Writer]
D --> E[数据目标]
通过这个流程,我们可以清晰地看到数据从源到目标的流动过程。在实际应用中,可以根据具体需求选择不同的
io.Reader
和
io.Writer
实现,以满足各种数据处理场景。
3. 格式化IO与缓冲IO
在Go语言的数据IO操作中,格式化IO和缓冲IO也是非常重要的部分。
3.1 格式化IO
格式化IO主要使用
fmt
包来实现,它提供了一系列的函数,如
fmt.Printf
、
fmt.Fprintf
、
fmt.Sprintf
等,用于格式化输出数据。
-
fmt.Printf:用于将格式化的字符串输出到标准输出。例如:
package main
import "fmt"
func main() {
name := "John"
age := 30
fmt.Printf("My name is %s and I'm %d years old.\n", name, age)
}
-
fmt.Fprintf:用于将格式化的字符串输出到指定的io.Writer。例如:
package main
import (
"fmt"
"os"
)
func main() {
file, err := os.Create("output.txt")
if err != nil {
fmt.Println("Error creating file:", err)
return
}
defer file.Close()
name := "John"
age := 30
fmt.Fprintf(file, "My name is %s and I'm %d years old.\n", name, age)
}
-
fmt.Sprintf:用于返回格式化后的字符串,而不进行输出。例如:
package main
import (
"fmt"
)
func main() {
name := "John"
age := 30
message := fmt.Sprintf("My name is %s and I'm %d years old.", name, age)
fmt.Println(message)
}
3.2 缓冲IO
缓冲IO可以提高IO操作的效率,特别是在频繁进行小数据量的读写操作时。Go语言提供了
bufio
包来实现缓冲IO。
-
bufio.Reader:用于缓冲读取操作。例如:
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
file, err := os.Open("input.txt")
if err != nil {
fmt.Println("Error opening file:", err)
return
}
defer file.Close()
reader := bufio.NewReader(file)
line, err := reader.ReadString('\n')
if err != nil {
fmt.Println("Error reading line:", err)
return
}
fmt.Println("Read line:", line)
}
-
bufio.Writer:用于缓冲写入操作。例如:
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
file, err := os.Create("output.txt")
if err != nil {
fmt.Println("Error creating file:", err)
return
}
defer file.Close()
writer := bufio.NewWriter(file)
message := "Hello, World!\n"
_, err = writer.WriteString(message)
if err != nil {
fmt.Println("Error writing to buffer:", err)
return
}
err = writer.Flush()
if err != nil {
fmt.Println("Error flushing buffer:", err)
return
}
fmt.Println("Data written successfully.")
}
4. 内存中的IO
在Go语言中,还可以进行内存中的IO操作,主要使用
bytes
和
strings
包。
4.1
bytes.Buffer
bytes.Buffer
是一个可变大小的字节缓冲区,可以作为
io.Reader
和
io.Writer
使用。例如:
package main
import (
"bytes"
"fmt"
)
func main() {
var buffer bytes.Buffer
buffer.WriteString("Hello, ")
buffer.WriteString("World!")
fmt.Println(buffer.String())
reader := bytes.NewReader(buffer.Bytes())
data := make([]byte, buffer.Len())
n, err := reader.Read(data)
if err != nil {
fmt.Println("Error reading from buffer:", err)
return
}
fmt.Printf("Read %d bytes: %s\n", n, string(data))
}
4.2
strings.Reader
strings.Reader
用于从字符串中读取数据,实现了
io.Reader
接口。例如:
package main
import (
"fmt"
"strings"
)
func main() {
str := "Hello, World!"
reader := strings.NewReader(str)
data := make([]byte, len(str))
n, err := reader.Read(data)
if err != nil {
fmt.Println("Error reading from string:", err)
return
}
fmt.Printf("Read %d bytes: %s\n", n, string(data))
}
5. 数据编码与解码
在数据传输和存储过程中,常常需要对数据进行编码和解码。Go语言提供了多个包来支持不同的编码格式,如
json
、
xml
、
gob
等。
5.1 JSON编码与解码
json
包用于处理JSON格式的数据。以下是一个简单的JSON编码和解码示例:
package main
import (
"encoding/json"
"fmt"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
// 编码
p := Person{Name: "John", Age: 30}
data, err := json.Marshal(p)
if err != nil {
fmt.Println("Error encoding JSON:", err)
return
}
fmt.Println("Encoded JSON:", string(data))
// 解码
var decoded Person
err = json.Unmarshal(data, &decoded)
if err != nil {
fmt.Println("Error decoding JSON:", err)
return
}
fmt.Printf("Decoded Person: %+v\n", decoded)
}
5.2 XML编码与解码
xml
包用于处理XML格式的数据。以下是一个简单的XML编码和解码示例:
package main
import (
"encoding/xml"
"fmt"
)
type Person struct {
XMLName xml.Name `xml:"person"`
Name string `xml:"name"`
Age int `xml:"age"`
}
func main() {
// 编码
p := Person{Name: "John", Age: 30}
data, err := xml.MarshalIndent(p, "", " ")
if err != nil {
fmt.Println("Error encoding XML:", err)
return
}
fmt.Println("Encoded XML:", string(data))
// 解码
var decoded Person
err = xml.Unmarshal(data, &decoded)
if err != nil {
fmt.Println("Error decoding XML:", err)
return
}
fmt.Printf("Decoded Person: %+v\n", decoded)
}
总结
Go语言在数据IO方面提供了丰富的功能和工具,涵盖了从基本的读取和写入操作,到格式化IO、缓冲IO、内存中的IO以及数据编码与解码等多个方面。通过合理运用这些功能,可以高效地处理各种数据输入和输出场景。
以下是一个总结表格,展示了Go语言数据IO相关的主要包和功能:
| 包名 | 主要功能 | 示例函数 |
| — | — | — |
|
io
| 提供
io.Reader
和
io.Writer
接口,用于基本的读写操作 |
io.Copy
|
|
fmt
| 提供格式化IO功能 |
fmt.Printf
、
fmt.Fprintf
、
fmt.Sprintf
|
|
bufio
| 提供缓冲IO功能 |
bufio.Reader
、
bufio.Writer
|
|
bytes
| 用于内存中的字节缓冲区操作 |
bytes.Buffer
|
|
strings
| 用于从字符串中读取数据 |
strings.Reader
|
|
encoding/json
| 处理JSON格式的数据编码和解码 |
json.Marshal
、
json.Unmarshal
|
|
encoding/xml
| 处理XML格式的数据编码和解码 |
xml.Marshal
、
xml.Unmarshal
|
同时,下面的流程图展示了一个完整的数据处理流程,包含读取、处理、编码、写入等步骤:
graph LR
A[数据源] --> B[io.Reader]
B --> C[数据处理]
C --> D[数据编码]
D --> E[io.Writer]
E --> F[数据目标]
通过对这些概念和技术的掌握,开发者可以更好地利用Go语言进行高效、灵活的数据IO操作,满足不同的业务需求。
超级会员免费看

1146

被折叠的 条评论
为什么被折叠?



