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的并发的主要内容,如果未能解决你的问题,请参考以下文章

[Go] 并发和并行的区别

golang goroutine例子[golang并发代码片段]

解决go: go.mod file not found in current directory or any parent directory; see ‘go help modules‘(代码片段

几段 Go 并发代码

你知道的Go切片扩容机制可能是错的

如何从设置中获取数据并发送到此片段