Go的并发
Posted Harris-H
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go的并发相关的知识,希望对你有一定的参考价值。
Go的并发
1.并发
https://docs.microsoft.com/zh-cn/learn/modules/go-concurrency/1-goroutines
Goroutine
goroutine 是轻量线程中的并发活动,而不是在操作系统中进行的传统活动。 假设你有一个写入输出的程序和另一个计算两个数字相加的函数。 一个并发程序可以有数个 goroutine 同时调用这两个函数。
我们可以说,程序执行的第一个 goroutine 是 main()
函数。 如果要创建其他 goroutine,则必须在调用该函数之前使用 go
关键字,如下所示:
func main()
login()
go launch()
你还会发现,许多程序喜欢使用匿名函数来创建 goroutine,如下所示:
func main()
login()
go func()
launch()
()
将 channel 用作通信机制
Go 中的 channel 是 goroutine 之间的通信机制。 这就是为什么我们之前说过 Go 实现并发的方式是:“不是通过共享内存通信,而是通过通信共享内存。” 当你需要将值从一个 goroutine 发送到另一个 goroutine 时,将使用 channel。 让我们看看它们的工作原理,以及如何开始使用它们来编写并发 Go 程序。
由于 channel 是发送和接收数据的通信机制,因此它也有类型之分。 这意味着你只能发送 channel 支持的数据类型。 除使用关键字 chan
作为 channel 的数据类型外,还需指定将通过 channel 传递的数据类型,如 int
类型。
每次声明一个 channel 或希望在函数中指定一个 channel 作为参数时,都需要使用 chan <type>
,如 chan int
。 要创建 channel,需使用内置的 make()
函数,如下所示:
ch := make(chan int)
一个 channel 可以执行两项操作:发送数据和接收数据。 若要指定 channel 具有的操作类型,需要使用 channel 运算符 <-
。 此外,在 channel 中发送数据和接收数据属于阻止操作。 你一会儿就会明白为何如此。
如果希望 channel 仅发送数据,则必须在 channel 之后使用 <-
运算符。 如果希望 channel 接收数据,则必须在 channel 之前使用 <-
运算符,如下所示:
ch <- x // sends (or write) x through channel ch
x = <-ch // x receives (or reads) data sent to the channel ch
<-ch // receives data, but the result is discarded
可在 channel 中执行的另一项操作是关闭 channel。 若要关闭 channel,需使用内置的 close()
函数,如下所示:
close(ch)
关闭 channel 时,你希望数据将不再在该 channel 中发送。 如果试图将数据发送到已关闭的 channel,则程序将发生严重错误。 如果试图通过已关闭的 channel 接收数据,则可以读取发送的所有数据。 随后的每次“读取”都将返回一个零值。
让我们回到之前创建的程序,然后使用 channel 来删除睡眠功能并稍做清理。 首先,让我们在 main
函数中创建一个字符串 channel,如下所示:
ch := make(chan string)
它在运行,但程序未完成。 最后一个打印行阻止了程序,因为它需要接收数据。 必须使用类似 Ctrl+C
的命令关闭程序。
这只是证明了读取数据和接收数据都属于阻止操作。 要解决此问题,只需更改循环的代码,然后只接收确定要发送的数据,如下所示:
for i := 0; i < len(apis); i++
fmt.Print(<-ch)
程序最终版本
package main
import (
"fmt"
"net/http"
"time"
)
func main()
start := time.Now()
apis := []string
"https://management.azure.com",
"https://dev.azure.com",
"https://api.github.com",
"https://outlook.office.com/",
"https://api.somewhereintheinternet.com/",
"https://graph.microsoft.com",
ch := make(chan string)
for _, api := range apis
go checkAPI(api, ch)
for i := 0; i < len(apis); i++
fmt.Print(<-ch)
elapsed := time.Since(start)
fmt.Printf("Done! It took %v seconds!\\n", elapsed.Seconds())
func checkAPI(api string, ch chan string)
_, err := http.Get(api)
if err != nil
ch <- fmt.Sprintf("ERROR: %s is down!\\n", api)
return
ch <- fmt.Sprintf("SUCCESS: %s is up and running!\\n", api)
Channel 方向
Go 中 channel 的一个有趣特性是,在使用 channel 作为函数的参数时,可以指定 channel 是要发送数据还是接收数据。 随着程序的增长,可能会使用大量的函数,这时候,最好记录每个 channel 的意图,以便正确使用它们。 或者,你要编写一个库,并希望将 channel 公开为只读,以保持数据一致性。
要定义 channel 的方向,可以使用与读取或接收数据时类似的方式进行定义。 但是你在函数参数中声明 channel 时执行此操作。 将 channel 类型定义为函数中的参数的语法如下所示:
chan<- int // it's a channel to only send data
<-chan int // it's a channel to only receive data
通过仅接收的 channel 发送数据时,在编译程序时会出现错误。
让我们使用以下程序作为两个函数的示例,一个函数用于读取数据,另一个函数用于发送数据:
package main
import "fmt"
func send(ch chan<- string, message string)
fmt.Printf("Sending: %#v\\n", message)
ch <- message
func read(ch <-chan string)
fmt.Printf("Receiving: %#v\\n", <-ch)
func main()
ch := make(chan string, 1)
send(ch, "Hello World!")
read(ch)
总结
如你所见,Go 实现并发的方法不同于其他编程语言。 Go 的标语概括了此方法:“不是通过共享内存通信,而是通过通信共享内存。”
这个简单的句子改变了一切。 你已经看到,通过使用 goroutine 和 channel,可以编写运行速度更快且易于理解的并发程序(至少当你了解了 Go 中某些功能的工作原理后)。
我们只是介绍了 Go 的并发方法的基础知识。 但至少你已经进行了一些实践,尤其是针对这个挑战的实践。
我们强烈建议你再次访问本模块,以确保了解基础知识。 然后,就可以开始更深入的探索了。
确保理解为什么需要使用 channel 在 goroutine 中进行通信,以及无缓冲 channel 和有缓冲 channel 之间的区别,尤其是使用时的区别。 关于并发的知识先介绍到这里,下一模块再见。
项目代码
文件结构
/src
/bankapi
go.mod
main.go
/bankcore
bank.go
bank_test.go
go.mod
go.mod
module bankapi
require (
github.com/msft/bank v0.0.1
)
replace (
github.com/msft/bank => ../bankcore
)
go 1.17
main.go
package main
import (
"encoding/json"
"fmt"
"github.com/msft/bank"
"log"
"net/http"
"strconv"
)
var accounts = map[float64]*CustomAccount
func deposit(w http.ResponseWriter, req *http.Request)
numberqs := req.URL.Query().Get("number")
amountqs := req.URL.Query().Get("amount")
if numberqs == ""
fmt.Fprintf(w, "Account number is missing!")
return
if number, err := strconv.ParseFloat(numberqs, 64); err != nil
fmt.Fprintf(w, "Invalid account number!")
else if amount, err := strconv.ParseFloat(amountqs, 64); err != nil
fmt.Fprintf(w, "Invalid amount number!")
else
account, ok := accounts[number]
if !ok
fmt.Fprintf(w, "Account with number %v can't be found!", number)
else
err := account.Deposit(amount)
if err != nil
fmt.Fprintf(w, "%v", err)
else
fmt.Fprintf(w, account.Statement())
func withdraw(w http.ResponseWriter, req *http.Request)
numberqs := req.URL.Query().Get("number")
amountqs := req.URL.Query().Get("amount")
if numberqs == ""
fmt.Fprintf(w, "Account number is missing!")
return
if number, err := strconv.ParseFloat(numberqs, 64); err != nil
fmt.Fprintf(w, "Invalid account number!")
else if amount, err := strconv.ParseFloat(amountqs, 64); err != nil
fmt.Fprintf(w, "Invalid amount number!")
else
account, ok := accounts[number]
if !ok
fmt.Fprintf(w, "Account with number %v can't be found!", number)
else
err := account.Withdraw(amount)
if err != nil
fmt.Fprintf(w, "%v", err)
else
fmt.Fprintf(w, account.Statement())
// CustomAccount ...
type CustomAccount struct
*bank.Account
// Statement ...
func (c *CustomAccount) Statement() string
json, err := json.Marshal(c)
if err != nil
return err.Error()
return string(json)
func main()
accounts[1001] = &CustomAccount
Account: &bank.Account
Customer: bank.Customer
Name: "John",
Address: "Los Angeles, California",
Phone: "(213) 555 0147",
,
Number: 1001,
,
accounts[1002] = &CustomAccount
Account: &bank.Account
Customer: bank.Customer
Name: "Mark",
Address: "Irvine, California",
Phone: "(949) 555 0198",
,
Number: 1002,
,
http.HandleFunc("/statement", statement)
http.HandleFunc("/deposit", deposit)
http.HandleFunc("/withdraw", withdraw)
http.HandleFunc("/transfer", transfer)
log.Fatal(http.ListenAndServe("localhost:8000", nil))
func transfer(w http.ResponseWriter, req *http.Request)
numberqs := req.URL.Query().Get("number")
amountqs := req.URL.Query().Get("amount")
destqs := req.URL.Query().Get("dest")
if numberqs == ""
fmt.Fprintf(w, "Account number is missing!")
return
if number, err := strconv.ParseFloat(numberqs, 64); err != nil
fmt.Fprintf(w, "Invalid account number!")
else if amount, err := strconv.ParseFloat(amountqs, 64); err != nil
fmt.Fprintf(w, "Invalid amount number!")
else if dest, err := strconv.ParseFloat(destqs, 64); err != nil
fmt.Fprintf(w, "Invalid account destination number!")
else
if accountA, ok := accounts[number]; !ok
fmt.Fprintf(w, "Account with number %v can't be found!", number)
else if accountB, ok := accounts[dest]; !ok
fmt.Fprintf(w, "Account with number %v can't be found!", dest)
else
err := accountA.Transfer(amount, accountB.Account)
if err != nil
fmt.Fprintf(w, "%v", err)
else
fmt.Fprintf(w, accountA.Statement())
func statement(w http.ResponseWriter, req *http.Request)
numberqs := req.URL.Query().Get("number")
if numberqs == ""
fmt.Fprintf(w, "Account number is missing!")
return
number, err := strconv.ParseFloat(numberqs, 64)
if err != nil
fmt.Fprintf(w, "Invalid account number!")
else
account, ok := accounts[number]
if !ok
fmt.Fprintf(w, "Account with number %v can't be found!", number)
else
json.NewEncoder(w).Encode(bank.Statement(account))
bank.go
package bank
import (
"errors"
"fmt"
)
//Customer
type Customer struct
Name,Address,Phone string
// Account
type Account struct
Customer
Number int32
Balance float64
//Deposit
func (a *Account) Deposit(amount float64) error
if amount <= 0
return errors.New("the amount to deposit should be greater than zero")
a.Balance += amount
return nil
// Withdraw ...
func (a *Account) Withdraw(amount float64) error
if amount <= 0
return errors.New("the amount to withdraw should be greater than zero")
if a.Balance < amount
return errors.New("the amount to withdraw should be greater than the account's balance")
a.Balance -= amount
return nil
// Statement ...
func (a *Account) Statement() string
return fmt.Sprintf("%v - %v - %v", a.Number, a.Name, a.Balance)
// Transfer function
func (a *Account) Transfer(amount float64, dest *Account) error
if amount <= 0
return errors.New("the amount to transfer should be greater than zero")
if a.Balance < amount
return errors.New("the amount to transfer should be greater than the account's balance")
a.Withdraw(amount)
dest.Deposit(amount)
return nil
// Bank ...
type Bank interface
Statement() string
// Statement ...
func Statement(b Bank) string
return b.Statement()
bank_test.go
package bank
import "testing"
func TestAccount(t *testing.T)
account := Account
Customer: Customer
Name: "John",
Address: "Los Angeles, California",
Phone: "(213) 555 0147",
,
Number: 1001,
Balance: 0,
if account.Name == ""
t.Error("can't create an Account object")
func TestDeposit(t *testing.T)
account := Account
Customer: Customer
Name: "John",
Address: "Los Angeles, California",
Phone: "(213) 555 0147",
,
Number: 1001,
Balance: 0,
account.Deposit(10)
if account.Balance != 10
t.Error("balance is not being updated after a deposit")
func TestDepositInvalid(t *testing.T)
account := Account
Customer: Customer
Name: "John",
Address: "Los Angeles, California",
Phone: "(213) 555 0147",
,
Number: 1001,
Balance: 0,
if err := account.Deposit(-10); err == nil
t.Error("only positive numbers should be allowed to deposit")
func TestWithdraw(t 以上是关于Go的并发的主要内容,如果未能解决你的问题,请参考以下文章
golang goroutine例子[golang并发代码片段]
解决go: go.mod file not found in current directory or any parent directory; see ‘go help modules‘(代码片段