原文:Go Recipes
三、结构和接口
当你写程序时,你选择的语言的类型系统是非常重要的。类型允许您以结构化的方式组织应用程序数据,这些数据可以保存在各种数据存储区中。当您编写应用程序(尤其是业务应用程序)时,您使用各种类型来组织应用程序数据,并将这些类型的值保存到持久化存储中。当你用 Go 编写应用程序时,理解它的类型系统和设计理念是很重要的。Go 提供了int
、uint
、float64
、string
、bool
等多种内置类型。用于存储数组、切片和映射等值集合的数据结构被称为复合类型,因为它们由其他类型(内置类型和用户定义类型)组成。除了 Go 提供的内置类型之外,还可以通过与其他类型结合来创建自己的类型。本章包含 Go 中用户定义类型的配方。
Go 为其类型系统提供了简单性和实用性,因为该语言对各种语言规范都有很大的贡献。Go 的类型系统是为解决现实世界的问题而设计的,而不是过于依赖学术理论,当你为你的应用程序设计数据模型时,它避免了很多复杂性。Go 的面向对象方法不同于其他语言,如 C++、Java 和 C#。Go 在其类型系统中不支持继承,甚至没有一个class
关键字。Go 有一个 struct 类型,如果你想比较 Go 的类型系统和其他面向对象语言的类型系统,它类似于类。Go 中的 struct 类型是类的一个轻量级版本,它遵循一种独特的设计,这种设计更倾向于组合而不是继承。
3-1.创建用户定义的类型
问题
您希望创建用户定义的类型来组织您的应用程序数据。
解决办法
Go 有一个 struct 类型,允许您通过与其他类型组合来创建用户定义的类型。
它是如何工作的
Go struct
允许您通过组合一个或多个类型来创建自己的类型,包括内置类型和用户定义类型。结构是在 Go 中创建具体的用户定义类型的唯一方法。当您使用struct
创建自己的类型时,重要的是要理解 Go 不支持其类型系统中的继承,但是它支持类型的组合,这允许您通过组合较小的类型来创建较大的类型。Go 的设计理念是通过组合较小的和模块化的组件来创建较大的组件。如果你是一个务实的程序员,你会欣赏 Go 的设计哲学,因为它有实际的好处,所以更喜欢组合而不是继承。类型的继承有时会在可维护性方面带来实际挑战。
声明结构类型
关键字struct
用于将类型声明为 struct。清单 3-1 展示了一个表示客户信息的示例结构。
type Customer struct {
FirstName string
LastName string
Email string
Phone string
}
Listing 3-1.Declare Struct Type
声明了一个结构类型Customer
,它有四个string
类型的字段。注意,Customer
结构及其字段被导出到其他包中,因为标识符是以大写字母开头的。在 Go 中,如果名称以大写字母开头,标识符将被导出到其他包中;否则,包内的可访问性将受到限制。如果一组结构字段有一个共同的类型,你可以在一个单行语句中组织相同类型的字段,如清单 3-2 所示。
type Customer struct {
FirstName, LastName, Email, Phone string
}
Listing 3-2.Declare Struct Type
因为Customer
结构的所有字段都有string
类型,所以可以在一条语句中指定字段。
创建结构类型的实例
您可以通过声明一个struct
变量或使用 struct 文字来创建struct
类型的实例。清单 3-3 显示了通过声明一个struct
变量并将值赋给 struct 的字段来创建一个Customer
struct 实例的代码块。
var c Customer
c.FirstName = "Alex"
c.LastName = "John"
c.Email = "alex@email.com"
c.Phone = "732-757-2923"
Listing 3-3.Creating a Struct Instance and Assigning Values
创建了一个Customer
类型的实例,并将值逐个分配给结构字段。struct 文本也可以用于创建struct
类型的实例。清单 3-4 显示了代码块,该代码块通过使用一个结构文本并给该结构的字段赋值来创建一个Customer
结构的实例。
c := Customer{
FirstName: "Alex",
LastName: "John",
Email: "alex@email.com",
Phone: "732-757-2923",
}
Listing 3-4.Creating a Struct Instance Using a Struct Literal
使用 struct 文本创建一个Customer
类型的实例,并将值赋给 struct 字段。请注意,即使在 struct 的最后一个字段初始化之后,也会添加一个逗号。当使用 struct 文本创建结构的实例时,可以将值初始化为多行语句,但即使在结构字段的赋值结束后也必须加上逗号。在清单 3-4 中,您通过指定结构字段来初始化值。如果您清楚地知道字段的顺序,您可以在初始化值时省略字段标识符,如清单 3-5 所示。
c := Customer{
"Alex",
"John",
"alex@email.com",
"732-757-2923",
}
Listing 3-5.Creating a Struct Instance Using a Struct Literal
当您使用 struct 文本创建 struct 实例时,您可以向 struct 的特定字段提供值,如清单 3-6 所示。
c := Customer{
FirstName: "Alex",
Email: "alex@email.com",
}
Listing 3-6.Creating a Struct Instance Using a Struct Literal by Specifying Values to a Few Fields
使用用户定义的类型作为字段的类型
使用内置类型的字段创建了Customer
结构。您可以使用其他结构类型作为结构字段的类型。让我们扩展一下Customer
结构,添加一个新字段来保存地址信息,用一个结构作为新字段的类型。清单 3-7 显示了通过添加一个新字段来扩展的Customer
结构,该字段的类型是Address
类型的一部分。
type Address struct {
Street, City, State, Zip string
IsShippingAddress bool
}
type Customer struct {
FirstName, LastName, Email, Phone string
Addresses []Address
}
Listing 3-7.
Customer Struct with a Slice of a User-Defined Type as the Type for Field
通过添加一个新字段Addresses
扩展了Customer
结构,该字段的类型被指定为一个名为Address
的结构的一部分。使用Addresses
字段,您可以为一个客户指定多个地址。IsShippingAddress
字段用于指定默认发货地址。清单 3-8 显示了创建这个修改过的Customer
结构的实例的代码块。
c := Customer{
FirstName: "Alex",
LastName: "John",
Email: "alex@email.com",
Phone: "732-757-2923",
Addresses: []Address{
Address{
Street: "1 Mission Street",
City: "San Francisco",
State: "CA",
Zip: "94105",
IsShippingAddress: true,
},
Address{
Street: "49 Stevenson Street",
City: "San Francisco",
State: "CA",
Zip: "94105",
},
},
}
Listing 3-8.Creating an Instance of Customer Struct
通过创建一个长度为两个值的Address
类型的切片来初始化Addresses
字段。
3-2.向结构类型添加方法
问题
您希望将行为添加到struct
类型中,以提供对struct
的操作,并作为方法调用。
解决办法
Go 的类型系统允许你使用一个方法接收器向结构类型添加方法。方法接收器指定哪种类型必须将函数作为方法关联到该类型。
它是如何工作的
在 Go 中,方法是一个由接收者指定的函数。让我们给Customer
结构添加一个方法。
func (c Customer) ToString() string {
return fmt.Sprintf("Customer: %s %s, Email:%s", c.FirstName, c.LastName, c.Email)
}
方法ToString
被添加到Customer
结构中。在方法名之前使用一个额外的参数部分来指定接收方。在方法内部,您可以使用 receiver 的标识符来访问 receiver 类型的字段。ToString
方法通过访问 struct 字段以字符串形式返回客户名称和电子邮件。
return fmt.Sprintf("Customer: %s %s, Email:%s", c.FirstName, c.LastName, c.Email)
清单 3-9 展示了一个示例程序,它声明了Customer
结构并向其中添加了一些方法。
package main
import (
"fmt"
)
type Address struct {
Street, City, State, Zip string
IsShippingAddress bool
}
type Customer struct {
FirstName, LastName, Email, Phone string
Addresses []Address
}
func (c Customer) ToString() string {
return fmt.Sprintf("Customer: %s %s, Email:%s", c.FirstName, c.LastName, c.Email)
}
func (c Customer) ShippingAddress() string {
for _, v := range c.Addresses {
if v.IsShippingAddress == true {
return fmt.Sprintf("%s, %s, %s, Zip - %s", v.Street, v.City, v.State, v.Zip)
}
}
return ""
}
func main() {
c := Customer{
FirstName: "Alex",
LastName: "John",
Email: "alex@email.com",
Phone: "732-757-2923",
Addresses: []Address{
Address{
Street: "1 Mission Street",
City: "San Francisco",
State: "CA",
Zip: "94105",
IsShippingAddress: true,
},
Address{
Street: "49 Stevenson Street",
City: "San Francisco",
State: "CA",
Zip: "94105",
},
},
}
fmt.Println(c.ToString())
fmt.Println(c.ShippingAddress())
}
Listing 3-9.Struct with Methods
通过指定方法接收器,Customer
结构被附加到几个方法上。ToString
返回客户姓名和电子邮件,ShippingAddress
从存储在Addresses
字段中的地址列表中返回默认送货地址。在main
函数中,创建了一个Customer
结构的实例,并调用了它的方法。
运行该程序时,您应该会看到以下输出:
Customer: Alex John, Email:alex@email.com
1 Mission Street, San Francisco, CA, Zip - 94105
方法是带有接收器的函数。有两种类型的方法接收器:指针接收器和值接收器。清单 3-9 中的程序使用一个值接收器向Customer
结构添加方法。当用指针接收器指定方法时,用指向接收器值的指针调用该方法,当用值接收器指定方法时,使用接收器值的副本。因此,如果您想要改变接收器的状态(字段值),您必须使用指针接收器。
让我们给Customer
结构添加一个新方法(参见清单 3-9 )来探索指针接收器。首先,让我们通过指定不带指针的接收者来添加方法。
func (c Customer) ChangeEmail(newEmail string) {
c.Email = newEmail
}
新添加的ChangeEmail
方法为Email
字段分配一个新的电子邮件地址。让我们创建一个Customer
结构的实例,并通过传递一个新的电子邮件地址来调用ChangeEmail
方法。
c := Customer{
FirstName: "Alex",
LastName: "John",
Email: "alex@gmail.com",
Phone: "732-757-2923",
Addresses: []Address{
Address{
Street: "1 Mission Street",
City: "San Francisco",
State: "CA",
Zip: "94105",
IsShippingAddress: true,
},
Address{
Street: "49 Stevenson Street",
City: "San Francisco",
State: "CA",
Zip: "94105",
},
},
}
// Call ChangeEmail
c.ChangeEmail("alex.john@gmail.com")
fmt.Println(c.ToString())
运行该程序时,您应该会看到以下输出:
Customer: Alex John, Email:alex@gmail.com
您已经向ChangeEmail
方法提供了一个新的电子邮件来更改电子邮件地址,但是当您调用ToString
方法时,它并没有反映出来。您仍然会从电子邮件字段收到旧电子邮件。若要修改方法内部结构值的状态,必须用指针接收器声明方法,以便字段值的更改将反映在方法外部。清单 3-10 修改了ChangeEmail
方法,用一个指针接收器来指定,这样对 Email 字段的更改将会在ChangeEmail
方法之外得到反映。
func (c *Customer) ChangeEmail(newEmail string) {
c.Email = newEmail
}
Listing 3-10.A Method to Customer Struct with a Pointer Receiver
让我们创建一个Customer
struct 的实例,并通过传递一个新的电子邮件地址来调用ChangeEmail
方法。
c := Customer{
FirstName: "Alex",
LastName: "John",
Email: "alex@gmail.com",
Phone: "732-757-2923",
}
// Call ChangeEmail
c.ChangeEmail(alex.john@gmail.com)
fmt.Println(c.ToString())
运行该程序时,您应该会看到以下输出:
Customer: Alex John, Email:alex.john@gmail.com
1 Mission Street, San Francisco, CA, Zip - 94105
输出显示Email
字段的值已经改变。这里,Customer
类型的值用于调用用指针接收器指定的ChangeEmail
方法。
以下代码块使用类型为Customer
的指针来调用通过指针接收器指定的ChangeEmail
方法:
c := $Customer{
FirstName: "Alex",
LastName: "John",
Email: "alex@gmail.com",
Phone: "732-757-2923",
}
// Call ChangeEmail
c.ChangeEmail(alex.john@gmail.com)
值得注意的是,您可以向任何类型添加方法,包括内置类型。您可以向基元类型、复合类型和用户定义的类型添加方法。您可以为指针或值接收器类型定义方法,因此了解何时在方法上为接收器使用值或指针非常重要。简而言之,如果方法需要改变接收方的状态,接收方必须是指针。如果接收器是大型结构、数组或切片,指针接收器会更有效,因为它避免了在方法调用时复制大型数据结构的值。如果一个方法被指定了一个指针接收器,可能是为了改变接收器,那么最好在相同接收器类型的所有方法上使用指针接收器,这为用户提供了更好的可用性和可读性。
Customer
结构的ChangeEmail
方法需要改变它的接收者。因此,为了更好的可用性和清晰性,让我们修改其他方法。清单 3-11 修改了清单 3-9 的程序,所有方法都由指针接收器指定。
package main
import (
"fmt"
)
type Address struct {
Street, City, State, Zip string
IsShippingAddress bool
}
type Customer struct {
FirstName, LastName, Email, Phone string
Addresses []Address
}
func (c *Customer) ToString() string {
return fmt.Sprintf("Customer: %s %s, Email:%s", c.FirstName, c.LastName, c.Email)
}
func (c *Customer) ChangeEmail(newEmail string) {
c.Email = newEmail
}
func (c *Customer) ShippingAddress() string {
for _, v := range c.Addresses {
if v.IsShippingAddress == true {
return fmt.Sprintf("%s, %s, %s, Zip - %s", v.Street, v.City, v.State, v.Zip)
}
}
return ""
}
func main() {
c := &Customer{
FirstName: "Alex",
LastName: "John",
Email: "alex@email.com",
Phone: "732-757-2923",
Addresses: []Address{
Address{
Street: "1 Mission Street",
City: "San Francisco",
State: "CA",
Zip: "94105",
IsShippingAddress: true,
},
Address{
Street: "49 Stevenson Street",
City: "San Francisco",
State: "CA",
Zip: "94105",
},
},
}
fmt.Println(c.ToString())
c.ChangeEmail("alex.john@gmail.com")
fmt.Println("Customer after changing the Email:")
fmt.Println(c.ToString())
fmt.Println(c.ShippingAddress())
}
Listing 3-11.Struct with Pointer Receiver on Methods
因为ChangeEmail
方法需要改变接收器,所以所有方法都用指针接收器来定义。值得注意的是,您可以将方法与值和指针接收器混合使用。在前面的程序中,使用地址操作符(&
)创建了一个指针Customer
:
c := &Customer{}
Customer
指针c
用于调用Customer
结构的方法:
fmt.Println(c.ToString())
c.ChangeEmail("alex.john@gmail.com")
fmt.Println(c.ToString())
fmt.Println(c.ShippingAddress())
运行该程序时,您应该会看到以下输出:
Customer: Alex John, Email:alex@email.com
Customer after changing the Email:
Customer: Alex John, Email:alex.john@gmail.com
1 Mission Street, San Francisco, CA, Zip - 94105
3-3.使用类型嵌入合成类型
问题
您希望通过组合其他类型来创建类型。
解决办法
Go 支持将类型嵌入到其他类型中,这允许您通过组合其他类型来创建类型。
它是如何工作的
Go 的类型系统强化了组合优先于继承的设计理念,允许你通过嵌入其他类型来创建类型。通过使用通过类型嵌入实现的复合设计理念,您可以通过组合较小的类型来创建较大的类型。
让我们通过在类型中嵌入其他类型来创建类型。清单 3-12 展示了可以用来在电子商务系统中表示订单的数据模型。
type Address struct {
Street, City, State, Zip string
IsShippingAddress bool
}
type Customer struct {
FirstName, LastName, Email, Phone string
Addresses []Address
}
type Order struct {
Id int
Customer
PlacedOn time.Time
Status string
OrderItems []OrderItem
}
type OrderItem struct {
Product
Quantity int
}
type Product struct {
Code, Name, Description string
UnitPrice float64
}
Listing 3-12.Data Model for Order Entity
在清单 3-12 中,Order
结构是通过嵌入另一种类型Customer
结构来声明的。Order
结构用于为客户下订单,因此Customer
结构被嵌入到Order
结构中。要嵌入一个类型,只需指定要嵌入到另一个类型中的类型的名称。
type Order struct {
Customer
}
由于类型嵌入,Customer
结构的字段和行为在Order
结构中可用。Customer
结构将Address
结构的片用于Addresses
字段。Order
结构将OrderItem
结构的片用于OrderItems
字段。Product
结构被嵌入到OrderItem
结构中。在这里,您通过组合几个其他结构类型来创建一个更大的类型Order
结构。
让我们向为表示订单信息而声明的结构类型添加操作。清单 3-13 显示了带有各种行为的Order
的数据模型的完整版本。
package main
import (
"fmt"
"time"
)
type Address struct {
Street, City, State, Zip string
IsShippingAddress bool
}
type Customer struct {
FirstName, LastName, Email, Phone string
Addresses []Address
}
func (c Customer) ToString() string {
return fmt.Sprintf("Customer: %s %s, Email:%s", c.FirstName, c.LastName, c.Email)
}
func (c Customer) ShippingAddress() string {
for _, v := range c.Addresses {
if v.IsShippingAddress == true {
return fmt.Sprintf("%s, %s, %s, Zip - %s", v.Street, v.City, v.State, v.Zip)
}
}
return ""
}
type Order struct {
Id int
Customer
PlacedOn time.Time
Status string
OrderItems []OrderItem
}
func (o *Order) GrandTotal() float64 {
var total float64
for _, v := range o.OrderItems {
total += v.Total()
}
return total
}
func (o *Order) ToString() string {
var orderStr string
orderStr = fmt.Sprintf("Order#:%d, OrderDate:%s, Status:%s, Grand Total:%f\n", o.Id, o.PlacedOn, o.Status, o.GrandTotal())
orderStr += o.Customer.ToString()
orderStr += fmt.Sprintf("\nOrder Items:")
for _, v := range o.OrderItems {
orderStr += fmt.Sprintf("\n")
orderStr += v.ToString()
}
orderStr += fmt.Sprintf("\nShipping Address:")
orderStr += o.Customer.ShippingAddress()
return orderStr
}
func (o *Order) ChangeStatus(newStatus string) {
o.Status = newStatus
}
type OrderItem struct {
Product
Quantity int
}
func (item OrderItem) Total() float64 {
return float64(item.Quantity) * item.Product.UnitPrice
}
func (item OrderItem) ToString() string {
itemStr := fmt.Sprintf("Code:%s, Product:%s -- %s, UnitPrice:%f, Quantity:%d, Total:%f",
item.Product.Code, item.Product.Name, item.Product.Description, item.Product.UnitPrice, item.Quantity, item.Total())
return itemStr
}
type Product struct {
Code, Name, Description string
UnitPrice float64
}
Listing 3-13.Data Model for Order Entity with Operations in models.go
Order
结构的ToString
方法返回一个提供订单所有信息的string
值。ToString
调用其嵌入类型Customer
的ToString
和ShippingAddress
方法。ToString
方法还通过迭代OrderItems
字段来调用OrderItem
结构的ToString
方法,该字段是OrderItem
的一部分。
orderStr += o.Customer.ToString()
orderStr += fmt.Sprintf("\nOrder Items:")
for _, v := range o.OrderItems {
orderStr += fmt.Sprintf("\n")
orderStr += v.ToString()
}
orderStr += fmt.Sprintf("\nShipping Address:")
orderStr += o.Customer.ShippingAddress()
Order
结构的GrandTotal
方法返回订单的总计值,它调用OrderItem
结构的Total
方法来确定每个订单项的总值。
func (o *Order) GrandTotal() float64 {
var total float64
for _, v := range o.OrderItems {
total += v.Total()
}
return total
}
注意,Order
结构的ChangeStatus
方法改变了Status
字段的状态,因此该方法使用了指针接收器。
func (o *Order) ChangeStatus(newStatus string) {
o.Status = newStatus
}
因为ChangeStatus
方法需要一个指针接收器,所以Order
结构的所有其他方法都是用指针接收器定义的。
清单 3-14 显示了main
函数,该函数用于创建Order
结构的一个实例,并调用其ToString
方法来获取订单信息。
package main
import (
"fmt"
"time"
)
func main() {
order := &Order{
Id: 1001,
Customer: Customer{
FirstName: "Alex",
LastName: "John",
Email: "alex@email.com",
Phone: "732-757-2923",
Addresses: []Address{
Address{
Street: "1 Mission Street",
City: "San Francisco",
State: "CA",
Zip: "94105",
IsShippingAddress: true,
},
Address{
Street: "49 Stevenson Street",
City: "San Francisco",
State: "CA",
Zip: "94105",
},
},
},
Status: "Placed",
PlacedOn: time.Date(2016, time.April, 10, 0, 0, 0, 0, time.UTC),
OrderItems: []OrderItem{
OrderItem{
Product: Product{
Code: "knd100",
Name: "Kindle Voyage",
Description: "Kindle Voyage Wifi, 6 High-Resolution Display",
UnitPrice: 220,
},
Quantity: 1,
},
OrderItem{
Product: Product{
Code: "fint101",
Name: "Kindle Case",
Description: "Fintie Kindle Voyage SmartShell Case",
UnitPrice: 10,
},
Quantity: 2,
},
},
}
fmt.Println(order.ToString())
// Change Order status
order.ChangeStatus("Processing")
fmt.Println("\n")
fmt.Println(order.ToString())
}
Listing 3-14.Entry Point of the Program That Creates an Instance of the Order struct in main.go
通过为字段提供值来创建Order
结构的实例,包括嵌入的类型。这里使用了一个指针变量来调用Order
结构的方法。ToString
方法提供了客户所下订单的所有信息。ChangeStatus
方法用于改变订单的状态,从而改变Status
字段的值。嵌入类型时,可以提供类似于结构的普通字段的值。
运行该程序时,您应该会看到以下输出:
Order#:1001, OrderDate:2016-04-10 00:00:00 +0000 UTC, Status:Placed, Grand Total:240.000000
Customer: Alex John, Email:alex@email.com
Order Items:
Code:knd100, Product:Kindle Voyage -- Kindle Voyage Wifi, 6 High-Resolution Display, UnitPrice:220.000000, Quantity:1, Total:220.000000
Code:fint101, Product:Kindle Case -- Fintie Kindle Voyage SmartShell Case, UnitPrice:10.000000, Quantity:2, Total:20.000000
Shipping Address:1 Mission Street, San Francisco, CA, Zip - 94105
Order#:1001, OrderDate:2016-04-10 00:00:00 +0000 UTC, Status:Processing, Grand Total:240.000000
Customer: Alex John, Email:alex@email.com
Order Items:
Code:knd100, Product:Kindle Voyage -- Kindle Voyage Wifi, 6 High-Resolution Display, UnitPrice:220.000000, Quantity:1, Total:220.000000
Code:fint101, Product:Kindle Case -- Fintie Kindle Voyage SmartShell Case, UnitPrice:10.000000, Quantity:2, Total:20.000000
Shipping Address:1 Mission Street, San Francisco, CA, Zip - 94105
该输出显示订单信息,包括总计,这是通过调用类型的相应方法计算的。
3-4.使用界面
问题
您希望创建一个接口类型,将其作为其他类型的协定提供。
解决办法
Go 有一个用户定义的接口类型,可以作为具体类型的契约。Go 的接口类型为您的 Go 应用程序提供了大量的可扩展性和可组合性。用关键字interface
定义接口类型。
它是如何工作的
Go 的interface
类型为您的 Go 应用程序提供了大量的可扩展性和可组合性。像 C#和 Java 这样的编程语言都支持接口类型,但是 Go 的interface
类型在设计理念上是独一无二的。
声明接口类型
与 C#和 Java 不同,在 Go 中,你不需要通过指定任何关键字来显式地将一个interface
实现到一个具体的类型中。要将一个interface
实现为一个具体的类型,只需提供与在interface
类型中定义的相同签名的方法。清单 3-15 显示了一种interface
类型。
type TeamMember interface {
PrintName()
PrintDetails()
}
Listing 3-15.Interface Type TeamMember
interface
类型TeamMember
是在团队中创建各种员工类型的契约。TeamMember
接口在其契约中提供了两个行为:PrintName
和PrintDetails.
将接口实现为具体类型
让我们通过实现接口的两个行为PrintName
和PrintDetails
来创建一个具体类型的TeamMember
接口。清单 3-16 显示了一个具体的TeamMember
类型,它实现了在interface
类型中定义的方法。
type Employee struct {
FirstName, LastName string
Dob time.Time
JobTitle, Location string
}
func (e Employee) PrintName() {
fmt.Printf("\n%s %s\n", e.FirstName, e.LastName)
}
func (e Employee) PrintDetails() {
fmt.Printf("Date of Birth: %s, Job: %s, Location: %s\n", e.Dob.String(), e.JobTitle, e.Location)
}
Listing 3-16.Concrete Type of TeamMember
一个 struct Employee
用保存其状态的字段和基于在TeamMember
接口中定义的行为实现的方法来声明。您不需要使用任何语法来将interface
实现到类型中。相反,只需提供在接口中定义了相同签名的方法,就像您为实现TeamMember
接口的Employee
类型所做的那样。
一个interface
类型的最大好处是它允许你为同一个interface
类型创建不同的实现,这支持了更高层次的可扩展性。
清单 3-17 显示了通过嵌入Employee
类型创建的TeamMember
接口的实现,它是TeamMember
接口的实现。
type Developer struct {
Employee //type embedding for composition
Skills []string
}
Listing 3-17.Type Developer Implements TeamMember Interface
声明了一个结构Developer
,其中嵌入了类型Employee
。在这里你创建了更多具体类型的TeamMember
接口。因为类型Employee
是TeamMember
接口的实现,所以类型Developer
也是TeamMember
接口的实现。类型Employee
中定义的所有字段和方法在Developer
类型中也可用。除了Employee
的嵌入类型外,Developer
结构还提供了一个Skill
字段来表示Developer
类型的技能。
清单 3-18 显示了创建一个Developer
实例并通过嵌入类型Employee
调用可用方法的代码块。
d := Developer{
Employee{
"Steve",
"John",
time.Date(1990, time.February, 17, 0, 0, 0, 0, time.UTC),
"Software Engineer",
"San Francisco",
},
[]string{"Go", "Docker", "Kubernetes"},
}
d.PrintName()
d.PrintDetails()
Listing 3-18.Create an Instance of Developer Type and Call Methods
运行该程序时,您应该会看到以下输出:
Steve John
Date of Birth: 1990-02-17 00:00:00 +0000 UTC, Job: Software Engineer, Location: San Francisco
输出显示了在 Employee 结构中定义的方法可以通过 Developer 结构的实例进行访问。
与Employee
类型相比,Developer
结构更像是TeamMember
接口的具体实现。Employee
类型是为类型嵌入而定义的,用于更具体地实现TeamMember
接口,比如Developer
结构。此时,Developer
结构使用在Employee
结构中定义的方法。因为Developer
结构更像是一个具体的实现,它可能有自己的方法实现。这里的Developer
结构可能需要覆盖Employee
结构中定义的方法来提供额外的功能。清单 3-19 显示了覆盖Developer
结构的方法PrintDetails
的代码块。
// Overrides the PrintDetails
func (d Developer) PrintDetails() {
// Call Employee PrintDetails
d.Employee.PrintDetails()
fmt.Println("Technical Skills:")
for _, v := range d.Skills {
fmt.Println(v)
}
}
Listing 3-19.Overrides for the PrintDetails Method for the Developer struct
这里你调用了Employee
的PrintDetails
方法,并为Developer
结构提供了一个额外的功能。
让我们创建另一个struct
类型来提供TeamMember
接口的不同实现。清单 3-20 显示了一个名为Manager
的结构,它通过嵌入Employee
类型和覆盖PrintDetails
方法来实现TeamMember
接口。
type Manager struct {
Employee //type embedding for composition
Projects []string
Locations []string
}
// Overrides the PrintDetails
func (m Manager) PrintDetails() {
// Call Employee PrintDetails
m.Employee.PrintDetails()
fmt.Println("Projects:")
for _, v := range m.Projects {
fmt.Println(v)
}
fmt.Println("Managing teams for the locations:")
for _, v := range m.Locations {
fmt.Println(v)
}
}
Listing 3-20.Type Manager Implements the TeamMember Interface
除了Employee
的嵌入类型之外,Manager
结构还提供了Projects
和Locations
字段来表示经理管理的项目和位置。
到目前为止,您已经创建了一个名为TeamMember
的接口类型,以及实现TeamMember
接口的三个具体类型:Employee
、Developer
和Manager
。让我们创建一个示例程序来探索这些类型并演示interface
类型。清单 3-21 显示了一个示例程序,它通过使用我们在本节中讨论过的类型来演示interface
。
package main
import (
"fmt"
"time"
)
type TeamMember interface {
PrintName()
PrintDetails()
}
type Employee struct {
FirstName, LastName string
Dob time.Time
JobTitle, Location string
}
func (e Employee) PrintName() {
fmt.Printf("\n%s %s\n", e.FirstName, e.LastName)
}
func (e Employee) PrintDetails() {
fmt.Printf("Date of Birth: %s, Job: %s, Location: %s\n", e.Dob.String(), e.JobTitle, e.Location)
}
type Developer struct {
Employee //type embedding for composition
Skills []string
}
// Overrides the PrintDetails
func (d Developer) PrintDetails() {
// Call Employee PrintDetails
d.Employee.PrintDetails()
fmt.Println("Technical Skills:")
for _, v := range d.Skills {
fmt.Println(v)
}
}
type Manager struct {
Employee //type embedding for composition
Projects []string
Locations []string
}
// Overrides the PrintDetails
func (m Manager) PrintDetails() {
// Call Employee PrintDetails
m.Employee.PrintDetails()
fmt.Println("Projects:")
for _, v := range m.Projects {
fmt.Println(v)
}
fmt.Println("Managing teams for the locations:")
for _, v := range m.Locations {
fmt.Println(v)
}
}
type Team struct {
Name, Description string
TeamMembers []TeamMember
}
func (t Team) PrintTeamDetails() {
fmt.Printf("Team: %s - %s\n", t.Name, t.Description)
fmt.Println("Details of the team members:")
for _, v := range t.TeamMembers {
v.PrintName()
v.PrintDetails()
}
}
func main() {
steve := Developer{
Employee{
"Steve",
"John",
time.Date(1990, time.February, 17, 0, 0, 0, 0, time.UTC),
"Software Engineer",
"San Francisco",
},
[]string{"Go", "Docker", "Kubernetes"},
}
irene := Developer{
Employee{
"Irene",
"Rose",
time.Date(1991, time.January, 13, 0, 0, 0, 0, time.UTC),
"Software Engineer",
"Santa Clara",
},
[]string{"Go", "MongoDB"},
}
alex := Manager{
Employee{
"Alex",
"Williams",
time.Date(1979, time.February, 17, 0, 0, 0, 0, time.UTC),
"Program Manger",
"Santa Clara",
},
[]string{"CRM", "e-Commerce"},
[]string{"San Francisco", "Santa Clara"},
}
// Create team
team := Team{
"Go",
"Golang Engineering Team",
[]TeamMember{steve, irene, alex},
}
// Get details of Team
team.PrintTeamDetails()
}
Listing 3-21.Example Program Demonstrates Interface with Type Embedding and Method Overriding
一个名为Team
的结构被声明为代表一个雇员团队,团队成员的雇员由字段TeamMembers
组织,字段的类型为TeamMember
接口的切片。因为TeamMembers
字段的类型使用了TeamMember
接口的一部分,所以您可以提供TeamMember
接口的任何实现作为值。类型Employee
仅用于嵌入到Developer
和Manager
结构中,这些结构更多的是作为团队成员的雇员的具体实现。
type Team struct {
Name, Description string
TeamMembers []TeamMember
}
Team
的PrintTeamDetails
方法打印一个Team
对象的信息。在PrintTeamDetails
方法中,它遍历TeamMembers
集合的元素,并调用PrintName
和PrintDetails
方法来获取每个团队成员的信息。
func (t Team) PrintTeamDetails() {
fmt.Printf("Team: %s - %s\n", t.Name, t.Description)
fmt.Println("Details of the team members:")
for _, v := range t.TeamMembers {
v.PrintName()
v.PrintDetails()
}
}
在main
函数内部,通过提供实现了TeamMember
接口的三个对象的值,创建了一个 team struct 实例。在三个TeamMember
类型的对象中,两个是用Developer
类型创建的,另一个是用Manager
类型创建的。TeamMembers
字段的值包含不同类型的值;所有对象的连接因素是TeamMember
接口。您只需提供TeamMember
接口的不同实现。最后调用Team
结构的PrintTeamDetails
方法来获取关于Team
类型的值的信息。
func main() {
steve := Developer{
Employee{
"Steve",
"John",
time.Date(1990, time.February, 17, 0, 0, 0, 0, time.UTC),
"Software Engineer",
"San Francisco",
},
[]string{"Go", "Docker", "Kubernetes"},
}
irene := Developer{
Employee{
"Irene",
"Rose",
time.Date(1991, time.January, 13, 0, 0, 0, 0, time.UTC),
"Software Engineer",
"Santa Clara",
},
[]string{"Go", "MongoDB"},
}
alex := Manager{
Employee{
"Alex",
"Williams",
time.Date(1979, time.February, 17, 0, 0, 0, 0, time.UTC),
"Program Manger",
"Santa Clara",
},
[]string{"CRM", "e-Commerce"},
[]string{"San Francisco", "Santa Clara"},
}
// Create team
team := Team{
"Go",
"Golang Engineering Team",
[]TeamMember{steve, irene, alex},
}
// Get details of Team
team.PrintTeamDetails()
}
运行该程序时,您应该会看到以下输出:
Team: Go - Golang Engineering Team
Details of the team members:
Steve John
Date of Birth: 1990-02-17 00:00:00 +0000 UTC, Job: Software Engineer, Location: San Francisco
Technical Skills:
Go
Docker
Kubernetes
Irene Rose
Date of Birth: 1991-01-13 00:00:00 +0000 UTC, Job: Software Engineer, Location: Santa Clara
Technical Skills:
Go
MongoDB
Alex Williams
Date of Birth: 1979-02-17 00:00:00 +0000 UTC, Job: Program Manger, Location: Santa Clara
Projects:
CRM
e-Commerce
Managing teams for the locations:
San Francisco
Santa Clara
四、并发
我们生活在云计算时代,在这个时代,您可以在高性能服务器中快速配置虚拟机。尽管我们的现代计算机发展到现在有了更多的 CPU 内核,但当我们运行应用程序时,我们仍然不能充分利用现代服务器的全部能力。有时我们的应用程序运行缓慢,但当我们查看 CPU 利用率时,它可能没有得到充分利用。问题是我们仍然在使用一些为单核机器时代设计的工具。我们可以通过编写并发程序来提高许多应用程序的性能,并发程序允许您将程序编写为几个自治活动的组合。我们现有的一些编程语言通过使用框架或库来提供对并发性的支持,但不是核心语言的内置特性。
Go 对并发的支持是其主要卖点之一。并发是 Go 的一个内置特性,Go 运行时对使用其并发特性运行的程序有很大的控制力。Go 通过两种范例提供并发性:goroutine 和 channel。Goroutines
让您运行相互独立的功能。Go 中并发执行的函数称为 goroutine,每个函数都被视为执行特定任务的工作单元。您可以通过组合这些自治任务来编写并发程序。除了运行彼此独立的功能之外,Go 还具有通过使用通道在 Go routine 之间发送和接收数据来同步 Go routine 的能力。通道是在 goroutines 之间发送和接收数据的通信机制。
4-1.编写并发程序
问题
您希望通过将函数作为自主活动运行来编写并发程序。
解决办法
Go 能够通过作为goroutine
运行来并发运行功能。Goroutines 是通过调用go
语句创建的,后面跟着您希望作为自治活动运行的函数或方法。
它是如何工作的
在前几章的例子中,所有的程序都是顺序程序。这意味着,在程序中,您按顺序调用函数:每个函数调用都会阻止程序完成该函数的执行,然后调用下一个函数。比如说你写一个程序,需要从main
函数中调用两个函数。这里你可能需要调用第一个函数,然后调用下一个函数。第二个函数的执行将发生在第一个函数执行之后。使用 Go 提供的并发功能,通过 goroutines,您可以同时执行这两个功能,彼此独立
要将一个函数作为goroutine
运行,调用带有go
语句前缀的函数。下面是示例代码块:
f() // A normal function call that executes f synchronously and waits for completing it
go f() // A goroutine that executes f asynchronously and doesn't wait for completing it
普通函数调用和goroutine
的唯一区别是goroutine
是用go
语句创建的。一个可执行的 Go 程序确实至少有一个goroutine
;调用main
函数的goroutine
被称为main goroutine
。清单 4-1 显示了一个示例程序,它创建了两个 goroutines 来打印一个加法表和一个乘法表。这个程序在执行 goroutines 时也使用sync.WaitGroup
同步执行;这里,函数main
正在等待使用sync.WaitGroup
完成 goroutines 的执行。
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
// WaitGroup is used to wait for the program to finish goroutines.
var wg sync.WaitGroup
func main() {
// Add a count of two, one for each goroutine.
wg.Add(2)
fmt.Println("Start Goroutines")
// Launch functions as goroutines
go addTable()
go multiTable()
// Wait for the goroutines to finish.
fmt.Println("Waiting To Finish")
wg.Wait()
fmt.Println("\nTerminating Program")
}
func addTable() {
// Schedule the call to WaitGroup's Done to tell goroutine is completed.
defer wg.Done()
for i := 1; i <= 10; i++ {
sleep := rand.Int63n(1000)
time.Sleep(time.Duration(sleep) * time.Millisecond)
fmt.Println("Addition Table for:", i)
for j := 1; j <= 10; j++ {
fmt.Printf("%d+%d=%d\t", i, j, i+j)
}
fmt.Println("\n")
}
}
func multiTable() {
// Schedule the call to WaitGroup's Done to tell goroutine is completed.
defer wg.Done()
for i := 1; i <= 10; i++ {
sleep := rand.Int63n(1000)
time.Sleep(time.Duration(sleep) * time.Millisecond)
fmt.Println("Multiplication Table for:", i)
for j := 1; j <= 10; j++ {
//res = i + j
fmt.Printf("%d*%d=%d\t", i, j, i*j)
}
fmt.Println("\n")
}
}
Listing 4-1.Example Program Demonstrates how to Create Goroutines
该程序创建了两个 goroutines:一个函数用于打印加法表,另一个函数用于打印乘法表。因为这两个函数同时运行,所以都将输出打印到控制台窗口中。go
语句用于启动函数作为 goroutines。
go addTable()
go multiTable()
程序使用WaitGroup
类型的sync
包,用于等待程序完成从main
功能启动的所有 goroutines。否则,goroutines 将从main
功能启动,然后在 goroutines 执行完成之前终止程序。WaitGroup
类型的Wait
方法等待程序完成所有 goroutines。WaitGroup
类型使用一个counter
来指定 goroutines 的数量,而Wait
阻塞程序的执行,直到WaitGroup counter
为零。
var wg sync.WaitGroup
wg.Add(2)
Add
方法用于给WaitGroup
增加一个计数器,这样对Wait
方法的调用就会阻塞执行,直到WaitGroup
计数器为零。在这里,两个计数器被添加到WaitGroup
中,每个 goroutine 一个计数器。在作为 goroutines 启动的addTable
和multiTable
函数中,WaitGroup
的Done
方法被调度使用defer
语句来递减WaitGroup
计数器。因此,在执行每个 goroutine 后,WaitGroup
计数器减 1。
func addTable() {
// Schedule the call to WaitGroup's Done to tell goroutine is completed.
defer wg.Done()
for i := 1; i <= 10; i++ {
sleep := rand.Int63n(1000)
time.Sleep(time.Duration(sleep) * time.Millisecond)
fmt.Println("Addition Table for:", i)
for j := 1; j <= 10; j++ {
//res = i + j
fmt.Printf("%d+%d=%d\t", i, j, i+j)
}
fmt.Println("\n")
}
}
当在main
函数中调用Wait
方法时,它会阻止执行,直到WaitGroup
计数器达到零值,并确保所有的 goroutines 都被执行。
func main() {
// Add a count of two, one for each goroutine.
wg.Add(2)
fmt.Println("Start Goroutines")
// Launch functions as goroutines
go addTable()
go multiTable()
// Wait for the goroutines to finish.
fmt.Println("Waiting To Finish")
wg.Wait()
fmt.Println("\nTerminating Program")
}
您应该会看到类似如下的输出:
Start Goroutines
Waiting To Finish
Addition Table for: 1
1+1=2 1+2=3 1+3=4 1+4=5 1+5=6 1+6=7 1+7=8 1+8=9 1+9=10 1+10=11
Multiplication Table for: 1
1*1=1 1*2=2 1*3=3 1*4=4 1*5=5 1*6=6 1*7=7 1*8=8 1*9=9 1*10=10
Multiplication Table for: 2
2*1=2 2*2=4 2*3=6 2*4=8 2*5=10 2*6=12 2*7=14 2*8=16 2*9=18 2*10=20
Addition Table for: 2
2+1=3 2+2=4 2+3=5 2+4=6 2+5=7 2+6=8 2+7=9 2+8=10 2+9=11 2+10=12
Multiplication Table for: 3
3*1=3 3*2=6 3*3=9 3*4=12 3*5=15 3*6=18 3*7=21 3*8=24 3*9=27 3*10=30
Addition Table for: 3
3+1=4 3+2=5 3+3=6 3+4=7 3+5=8 3+6=9 3+7=10 3+8=11 3+9=12 3+10=13
Addition Table for: 4
4+1=5 4+2=6 4+3=7 4+4=8 4+5=9 4+6=10 4+7=11 4+8=12 4+9=13 4+10=14
Addition Table for: 5
5+1=6 5+2=7 5+3=8 5+4=9 5+5=10 5+6=11 5+7=12 5+8=13 5+9=14 5+10=15
Multiplication Table for: 4
4*1=4 4*2=8 4*3=12 4*4=16 4*5=20 4*6=24 4*7=28 4*8=32 4*9=36 4*10=40
Addition Table for: 6
6+1=7 6+2=8 6+3=9 6+4=10 6+5=11 6+6=12 6+7=13 6+8=14 6+9=15 6+10=16
Multiplication Table for: 5
5*1=5 5*2=10 5*3=15 5*4=20 5*5=25 5*6=30 5*7=35 5*8=40 5*9=45 5*10=50
Addition Table for: 7
7+1=8 7+2=9 7+3=10 7+4=11 7+5=12 7+6=13 7+7=14 7+8=15 7+9=16 7+10=17
Multiplication Table for: 6
6*1=6 6*2=12 6*3=18 6*4=24 6*5=30 6*6=36 6*7=42 6*8=48 6*9=54 6*10=60
Multiplication Table for: 7
7*1=7 7*2=14 7*3=21 7*4=28 7*5=35 7*6=42 7*7=49 7*8=56 7*9=63 7*10=70
Addition Table for: 8
8+1=9 8+2=10 8+3=11 8+4=12 8+5=13 8+6=14 8+7=15 8+8=16 8+9=17 8+10=18
Multiplication Table for: 8
8*1=8 8*2=16 8*3=24 8*4=32 8*5=40 8*6=48 8*7=56 8*8=64 8*9=72 8*10=80
Multiplication Table for: 9
9*1=9 9*2=18 9*3=27 9*4=36 9*5=45 9*6=54 9*7=63 9*8=72 9*9=81 9*10=90
Addition Table for: 9
9+1=10 9+2=11 9+3=12 9+4=13 9+5=14 9+6=15 9+7=16 9+8=17 9+9=18 9+10=19
Addition Table for: 10
10+1=11 10+2=12 10+3=13 10+4=14 10+5=15 10+6=16 10+7=17 10+8=18 10+9=19 10+10=20
Multiplication Table for: 10
10*1=10 10*2=20 10*3=30 10*4=40 10*5=50 10*6=60 10*7=70 10*8=80 10*9=90 10*10=100
Terminating Program
您可以看到,addTable
和multiTable
函数同时在控制台窗口中生成输出,因为它们是并发执行的。在addTable
和multiTable
函数中,为了演示起见,执行会延迟一段随机生成的时间。当您运行程序时,输出的顺序每次都会不同,因为函数内部的执行是随机延迟的。
4-2.管理并发的 CPU 数量
问题
您希望管理用于在 Go 运行时执行 goroutines 的 CPU 数量,以便管理并发编程的行为。
解决办法
运行时包的GOMAXPROCS
函数用于改变用于运行并发程序的 CPU 数量。
它是如何工作的
Go 运行时提供了一个调度器,在执行期间管理 goroutines。调度程序与操作系统紧密合作,并在一个 goroutine 的执行过程中控制一切。它调度所有 goroutines 在逻辑处理器上运行,其中每个逻辑处理器都绑定了一个在物理处理器上运行的操作系统线程。简而言之,Go runtime scheduler 针对一个逻辑处理器运行 goroutines,这个逻辑处理器与一个可用的物理处理器中的操作系统线程绑定在一起。请记住,带有操作系统线程的单个逻辑处理器可以同时执行数万个 goroutines。
在执行程序时,Go runtime scheduler 采用GOMAXPROCS
设置的值来找出有多少操作系统线程将试图同时执行代码。比如说,如果GOMAXPROCS
的值是 8,那么程序一次只会在 8 个操作系统线程上执行 goroutines。从 Go 1.5 开始,GOMAXPROCS
的默认值是可用的 CPU 数量,由runtime
包的NumCPU
函数决定。NumCPU
函数返回当前进程可用的逻辑 CPU 数量。在 Go 1.5 之前,GOMAXPROCS
的默认值是 1。使用GOMAXPROCS
环境变量或从程序内调用runtime
包的GOMAXPROCS
函数可以修改GOMAXPROCS
的值。下面的代码块将GOMAXPROCS
的值设置为 1,这样程序将一次在一个操作系统线程上执行 goroutines:
import "runtime"
// Sets the value of GOMAXPROCS
runtime.GOMAXPROCS(1)
4-3.创建频道
问题
您希望在 goroutine 之间发送和接收数据,以便一个 go routine 可以与其他 go routine 通信。
解决办法
Go 提供了一种称为通道的机制,用于在 goroutines 之间共享数据。基于它们的行为,有两种类型的通道:无缓冲通道和缓冲通道。无缓冲信道用于执行 goroutines 之间的同步通信;缓冲通道用于执行异步通信。
它是如何工作的
Goroutines 是在并发编程中用来执行并发活动的一种很好的机制。当您作为一个 goroutine 执行并发活动时,您可能需要将数据从一个 goroutine 发送到另一个 go routine。通道通过充当 goroutines 之间的管道来处理这种通信。根据数据交换的行为,通道分为无缓冲通道和缓冲通道。无缓冲通道用于执行数据的同步交换。另一方面,缓冲通道用于异步执行数据交换。
创建频道
通道由make
函数创建,它指定了chan
关键字和通道的元素类型。下面是创建无缓冲通道的代码块:
// Unbuffered channel of integer type
counter := make(chan int)
使用内置函数make
创建一个integer
类型的无缓冲通道。渠道counter
可以充当integer
类型的价值观的管道。您可以使用内置类型和用户定义类型作为通道元素的类型。
通过指定缓冲通道的容量来创建缓冲通道。下面是声明缓冲通道的代码块:
// Buffered channel of integer type buffering up to 3 values
nums := make(chan int,3)
创建一个integer
类型的缓冲通道,其capacity
为 3。通道nums
能够缓冲多达三个元素的integer
值。
渠道沟通
一个通道有三种操作:send
、receive
和close
。send
操作向通道发送一个值或指针,当执行相应的receive
操作时,该值或指针从通道中读取。通信操作符<-
用于send
和receive
操作:
counter <- 10
前面的语句显示了一个向名为counter
的通道发送值的send
操作。当你写一个值或指针到一个通道时,操作符<-
被放在通道变量的右边。
num = <- counter
前面的语句显示了一个从名为counter
的通道接收值的receive
操作。当你从一个通道接收一个值或指针时,操作符<-
被放在通道变量的左边。
通道有一个关闭通道的close
操作,因此通道上的send
操作不能发生。在封闭通道上的send
操作将导致panic
。在关闭的通道上的receive
操作返回在通道关闭前已经发送到通道中的值;之后,receive
语句返回通道元素类型的零值。
清单 4-2 显示了一个用无缓冲和缓冲通道发送和接收的示例程序。
package main
import (
"fmt"
)
func main() {
// Declare a unbuffered channel
counter := make(chan int)
// Declare a buffered channel with capacity of 3
nums := make(chan int, 3)
go func() {
// Send value to the unbuffered channel
counter <- 1
close(counter) // Closes the channel
}()
go func() {
// Send values to the buffered channel
nums <- 10
nums <- 30
nums <- 50
}()
// Read the value from unbuffered channel
fmt.Println(<-counter)
val, ok := <-counter // Trying to read from closed channel
if ok {
fmt.Println(val) // This won't execute
}
// Read the 3 buffered values from the buffered channel
fmt.Println(<-nums)
fmt.Println(<-nums)
fmt.Println(<-nums)
close(nums) // Closes the channel
}
Listing 4-2.Send and Receive Values with Unbuffered and Buffered Channels
名为counter
的无缓冲通道是用元素类型integer
创建的。名为nums
的缓冲通道也是用元素类型integer
和capacity
3 创建的,这意味着它最多可以缓冲三个值。从main
函数启动一个匿名函数作为 goroutine,并向其写入一个值。通道counter
在写入一个值后关闭。请注意,无缓冲通道上的send
操作会阻止该通道上的执行,直到执行相应的receive
操作,因此该通道将等待另一个 goroutine 的receive
操作。这里receive
操作从main
goroutine 执行。
go func() {
// Send value to the unbuffered channel
counter <- 1
close(counter) // Closes the channel
}()
另一个匿名函数作为 goroutine 启动,将值写入缓冲通道。与无缓冲通道不同,缓冲通道上的send
操作不会阻止执行,您可以缓冲最高达其capacity
的值,此处为 3。
go func() {
// Send values to the buffered channel
nums <- 10
nums <- 30
nums <- 50
}()
该程序从无缓冲通道产生值。在关闭通道counter
之前,一个值被发送到其中,因此程序可以执行一个receive
操作。此后,信道将是空的。
// Read the value from unbuffered channel
fmt.Println(<-counter)
通道上的receive
操作可以识别通道是否为空。下面的代码块检查通道是否为空。
val, ok := <-counter // Trying to read from closed channel
if ok {
fmt.Println(val) // This won't execute
}
receive
操作可以返回两个值。它返回一个额外的boolean
值,指示通信是否成功。在前面的代码块中,如果成功的send
操作将receive
操作传递给通道,则ok
的值将返回true
,如果由于通道关闭且为空而生成零值,则返回false
。在这个程序中,ok
的值将是false
,因为通道是关闭的和空的。
缓冲通道缓冲三个值,因此程序可以执行三个receive
操作来从通道产生值。最后,缓冲通道被关闭,因此不能再对其执行send
操作。
// Read the 3 values from the buffered channel
fmt.Println(<-nums)
fmt.Println(<-nums)
fmt.Println(<-nums)
close(nums) // Closes the channel
在这个简单的例子中,我们没有使用WaitGroup
类型来同步执行,因为我们关注的是通道的行为。如果您的程序想要等待执行完成,请使用WaitGroup
类型来同步执行。运行该程序时,您应该会看到以下输出:
1
10
30
50
缓冲和非缓冲通道的send
和receive
操作具有不同的行为。在接下来的部分中,我们将详细研究缓冲通道和无缓冲通道。
4-4.使用通道进行同步通信
问题
您希望以同步的方式通过通道在 goroutines 之间交换数据,这样您就可以确保一个send
操作能够成功地通过相应的receive
操作传递数据。
解决办法
无缓冲通道以同步方式提供数据交换,确保来自一个 goroutine 的通道上的send
操作成功传递到另一个 goroutine,同时在同一通道上有相应的receive
操作。
它是如何工作的
无缓冲通道确保发送和接收路由器之间的数据交换。当一个send
操作在一个 goroutine 的无缓冲通道上执行时,必须在另一个 goroutine 的相同通道上执行相应的receive
操作,以完成send
操作。因此,send
操作阻塞发送 goroutine,直到另一个 goroutine 执行相应的receive
操作。在执行send
操作之前,可能会尝试receive
操作。如果receive
操作首先执行,接收 goroutine 将被阻塞,直到另一个 goroutine 执行相应的send
操作。简而言之,完成一个 goroutine 中的send
或receive operation
需要执行另一个 goroutine 中相应的send
或receive
操作。这种通信机制确保了数据从一个路由器传递到另一个路由器。
僵局
为了理解无缓冲信道上通信操作的阻塞行为,让我们写一个程序。清单 4-3 显示了一个将创建死锁的示例程序;因此,它将在运行程序时失败。
package main
import (
"fmt"
)
func main() {
// Declare an unbuffered channel
counter := make(chan int)
// This will create a deadlock
counter <- 10 // Send operation to a channel from main goroutine
fmt.Println(<-counter) // Receive operation from the channel
}
Listing 4-3.Example Program That Creates a Deadlock so That the Program Will Fail
运行该程序时,您应该会看到以下错误:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
当执行通信操作时,由于无缓冲通道的阻塞行为,该程序将由于死锁而失败。这里,从主 goroutine 执行send
操作,同时通道试图从同一主 goroutine 执行receive
操作。在执行完send
操作后,定义了receive
操作。当send
操作执行时,它阻塞主 goroutine,这意味着它阻塞整个程序的执行,因为send
操作正在等待同一通道上相应的receive
操作。因为send
操作阻塞执行,所以receive
操作无法执行,导致死锁。在清单 4-4 中,我们通过在 goroutine 中编写send
操作来解决死锁问题。
package main
import (
"fmt"
)
func main() {
// Declare an unbuffered channel
counter := make(chan int)
// Perform send operation by launching new goroutine
go func() {
counter <- 10
}()
fmt.Println(<-counter) // Receive operation from the channel
}
Listing 4-4.Example Program That Fixes the Deadlock Caused in Listing 4-3
该程序将成功运行,不会出现任何问题,因为它通过启动新的 goroutine 来执行send
操作,而receive
操作是在主 goroutine 中执行的。
示例程序
让我们编写一个示例程序来理解无缓冲通道的通信机制,如清单 4-5 所示。
package main
import (
"fmt"
"sync"
)
// wg is used to wait for the program to finish.
var wg sync.WaitGroup
func main() {
count := make(chan int)
// Add a count of two, one for each goroutine.
wg.Add(2)
fmt.Println("Start Goroutines")
// Launch a goroutine with label "Goroutine-1"
go printCounts("Goroutine-1", count)
// Launch a goroutine with label "Goroutine-2"
go printCounts("Goroutine-2", count)
fmt.Println("Communication of channel begins")Sticky
count <- 1
// Wait for the goroutines to finish.
fmt.Println("Waiting To Finish")
wg.Wait()
fmt.Println("\nTerminating the Program")
}
func printCounts(label string, count chan int) {
// Schedule the call to WaitGroup's Done to tell goroutine is completed.
defer wg.Done()
for {
// Receives message from Channel
val, ok := <-count
if !ok {
fmt.Println("Channel was closed")
return
}
fmt.Printf("Count: %d received from %s \n", val, label)
if val == 10 {
fmt.Printf("Channel Closed from %s \n", label)
// Close the channel
close(count)
return
}
val++
// Send count back to the other goroutine.
count <- val
}
}
Listing 4-5.Example Program Demonstrating Unbuffered Channels
创建一个名为count
的integer
类型的无缓冲通道,并启动两个 goroutines。两个 goroutines 都通过提供通道count
和一个string label
来执行printCounts
功能。两个 goroutines 启动后,在通道count
上执行send
操作。这将等待在同一通道上获得相应的receive
操作。
// Launch a goroutine with label "Goroutine-1"
go printCounts("Goroutine-1", count)
// Launch a goroutine with label "Goroutine-2"
go printCounts("Goroutine-2", count)
fmt.Println("Communication of channel begins")
count <- 1
printCounts
函数打印从通道count
接收的值,并通过向count
提供新值在同一通道上执行send
操作,以与其他 goroutines 共享数据。在两个 goroutine 启动后,初始值 1 被发送到通道,因此一个 go routine 可以receive
初始值,并且可以完成send
操作。在从通道接收到一个值后,接收 goroutine sends
向通道增加一个值,因此它阻塞 goroutine,直到另一个 goroutine 从通道接收到该值。send
和receive
继续运行,直到count
的值达到 10。当通道count
的值达到 10 时,通道关闭,因此不能再执行send
操作。
func printCounts(label string, count chan int) {
// Schedule the call to WaitGroup's Done to tell goroutine is completed.
defer wg.Done()
for {
// Receives message from Channel
val, ok := <-count
if !ok {
fmt.Println("Channel was closed")
return
}
fmt.Printf("Count: %d received from %s \n", val, label)
if val == 10 {
fmt.Printf("Channel Closed from %s \n", label)
// Close the channel
close(count)
return
}
val++
// Send count back to the other goroutine.
count <- val
}
}
当在通道上执行receive
操作时,我们检查通道是否关闭,如果通道关闭,则从 goroutine 退出。
val, ok := <-count
if !ok {
fmt.Println("Channel was closed")
return
}
您应该会看到类似如下的输出:
Start Goroutines
Communication of channel begins
Waiting To Finish
Count: 1 received from Goroutine-1
Count: 2 received from Goroutine-2
Count: 3 received from Goroutine-1
Count: 4 received from Goroutine-2
Count: 5 received from Goroutine-1
Count: 6 received from Goroutine-2
Count: 7 received from Goroutine-1
Count: 8 received from Goroutine-2
Count: 9 received from Goroutine-1
Count: 10 received from Goroutine-2
Channel Closed from Goroutine-2
Channel was closed
Terminating the Program
请注意,每次运行程序时,goroutines 的顺序可能会改变。
使用范围表达式接收值
在清单 4-5 中,您使用通信操作符<-
从通道中读取值,并检查通道是否关闭。您已经使用了range
表达式来迭代各种数据结构的元素,比如数组、切片和映射。range
表达式也可以用来从通道中产生值,这对于大多数用例来说会更方便。通道上的range
表达式产生值,直到通道关闭。清单 4-6 用range
表达式重写了清单 4-5 的代码。
package main
import (
"fmt"
"sync"
)
// wg is used to wait for the program to finish.
var wg sync.WaitGroup
func main() {
count := make(chan int)
// Add a count of two, one for each goroutine.
wg.Add(2)
fmt.Println("Start Goroutines")
// Launch a goroutine with label "Goroutine-1"
go printCounts("Goroutine-1", count)
// Launch a goroutine with label "Goroutine-2"
go printCounts("Goroutine-2", count)
fmt.Println("Communication of channel begins")
count <- 1
// Wait for the goroutines to finish.
fmt.Println("Waiting To Finish")
wg.Wait()
fmt.Println("\nTerminating the Program")
}
func printCounts(label string, count chan int) {
// Schedule the call to WaitGroup's Done to tell goroutine is completed.
defer wg.Done()
for val := range count {
fmt.Printf("Count: %d received from %s \n", val, label)
if val == 10 {
fmt.Printf("Channel Closed from %s \n", label)
// Close the channel
close(count)
return
}
val++
// Send count back to the other goroutine.
count <- val
}
}
Listing 4-6.Example Program Demonstrates Unbuffered Channel and range Expression on Channel
range
表达式产生来自通道count
的值,直到通道关闭。
for val := range count {
fmt.Printf("Count: %d received from %s \n", val, label)
}
您应该会看到类似如下的输出:
Start Goroutines
Communication of channel begins
Waiting To Finish
Count: 1 received from Goroutine-1
Count: 2 received from Goroutine-2
Count: 3 received from Goroutine-1
Count: 4 received from Goroutine-2
Count: 5 received from Goroutine-1
Count: 6 received from Goroutine-2
Count: 7 received from Goroutine-1
Count: 8 received from Goroutine-2
Count: 9 received from Goroutine-1
Count: 10 received from Goroutine-2
Channel Closed from Goroutine-2
Terminating the Program
4-5.使用一个例程的输出作为另一个例程的输入
问题
您希望使用一个 goroutine 的输出作为另一个 goroutine 的输入,依此类推。
解决办法
Pipeline 是一种并发模式,指的是通过通道连接的一系列 goroutine 阶段,其中一个 goroutine 的输出是另一个 go routine 的输入,依此类推。
它是如何工作的
让我们编写一个示例程序来探索管道。清单 4-7 显示了一个用 goroutines 和通道演示管道的示例程序。示例程序有一个三级管道,其中三个 goroutines 由两个通道连接。在这个管道中,第一级的 goroutine 用于随机生成上限为 50 的值。管道有一个出站通道,向第二级的 goroutine 提供入站值。第二级的 goroutine 有一个入站通道和一个出站通道。当入站通道随机生成每个值并找出 Fibonacci 值时,它从第一个 goroutine 接收值。然后,它将得到的 Fibonacci 值提供给第三阶段的 goroutine,后者只打印第二阶段的 goroutine 的出站值。下面是示例程序。
package main
import (
"fmt"
"math"
"math/rand"
"sync"
)
type fibvalue struct {
input, value int
}
var wg sync.WaitGroup
// Generates random values
func randomCounter(out chan int) {
defer wg.Done()
var random int
for x := 0; x < 10; x++ {
random = rand.Intn(50)
out <- random
}
close(out)
}
// Produces Fibonacci values of inputs provided by randomCounter
func generateFibonacci(out chan fibvalue, in chan int) {
defer wg.Done()
var input float64
for v := range in {
input = float64(v)
// Fibonacci using Binet's formula
Phi := (1 + math.Sqrt(5)) / 2
phi := (1 - math.Sqrt(5)) / 2
result := (math.Pow(Phi, input) - math.Pow(phi, input)) / math.Sqrt(5)
out <- fibvalue{
input: v,
value: int(result),
}
}
close(out)
}
// Print Fibonacci values generated by generateFibonacci
func printFibonacci(in chan fibvalue) {
defer wg.Done()
for v := range in {
fmt.Printf("Fibonacci value of %d is %d\n", v.input, v.value)
}
}
func main() {
// Add 3 into WaitGroup Counter
wg.Add(3)
// Declare Channels
randoms := make(chan int)
fibs := make(chan fibvalue)
// Launching 3 goroutines
go randomCounter(randoms) // First stage of pipeline
go generateFibonacci(fibs, randoms) // Second stage of pipeline
go printFibonacci(fibs) // Third stage of pipeline
// Wait for completing all goroutines
wg.Wait()
}
Listing 4-7.A Three-Stage Pipeline with Three Goroutines Connected by Two Channels
该程序打印 10 个随机生成的斐波那契值。两个无缓冲通道用作三级流水线的入站和出站通道。通道randoms
的元素类型是integer
,通道fibs
的元素类型是一个名为fibvalue
的结构类型,由两个字段组成,用于保存一个随机数及其斐波那契值。三个 goroutines 用于完成该流水线。
go randomCounter(randoms) // First stage of pipeline
go generateFibonacci(fibs, randoms) // Second stage of pipeline
go printFibonacci(fibs) // Third stage of pipeline
第一阶段的 goroutine 随机生成上限为 50 的值。
func randomCounter(out chan int) {
defer wg.Done()
var random int
for x := 0; x < 10; x++ {
random = rand.Intn(50)
out <- random
}
close(out)
}
在三级流水线的第一级中,randomCounter
函数向第二级提供输入,第二级在generateFibonacci
函数中实现。randomCounter
功能使用一个用于send
10 个随机生成值的integer
通道,此后该通道关闭。
func generateFibonacci(out chan fibvalue, in chan int) {
defer wg.Done()
var input float64
for v := range in {
input = float64(v)
// Fibonacci using Binet's formula
Phi := (1 + math.Sqrt(5)) / 2
phi := (1 - math.Sqrt(5)) / 2
result := (math.Pow(Phi, input) - math.Pow(phi, input)) / math.Sqrt(5)
out <- fibvalue{
input: v,
value: int(result),
}
}
close(out)
}
generateFibonacci
功能使用两个通道:一个用于从第一级的 goroutine 接收输入,另一个用于向第三级的 goroutine 提供输入。在generateFibonacci
函数中,receive
操作在入站通道上执行,该通道从randomCounter
函数中获取值。可以发送generateFibonacci
的输入值,直到通过randomCounter
功能关闭通道。generateFibonacci
函数为每个输入值生成斐波那契值。这些值被发送到出站通道,以向第三级的 goroutine 提供输入。
func printFibonacci(in chan fibvalue) {
defer wg.Done()
for v := range in {
fmt.Printf("Fibonacci value of %d is %d\n", v.input, v.value)
}
}
流水线的最后阶段在printFibonacci
函数中实现,它打印从generateFibonacci
函数的出站通道接收的斐波那契值。在从generateFibonacci
功能关闭通道之前,可以输出printFibonacci
功能的输入值。
在这个示例程序中,第一级的输出用作第二级的输入,然后第二级的输出用作第三级的输入。您应该会看到类似如下的输出:
Fibonacci value of 31 is 1346268
Fibonacci value of 37 is 24157816
Fibonacci value of 47 is 2971215072
Fibonacci value of 9 is 34
Fibonacci value of 31 is 1346268
Fibonacci value of 18 is 2584
Fibonacci value of 25 is 75025
Fibonacci value of 40 is 102334154
Fibonacci value of 6 is 8
Fibonacci value of 0 is 0
渠道方向
在清单 4-7 中,您使用了由两个通道连接的三个 goroutines。在这些 goroutine 中,一个 goroutine 对一个通道执行send
操作,另一个 go routine 从相同的通道接收值。这里,goroutine 中的一个通道用于send
操作或receive
操作,这样当您将通道指定为参数时,您可以指定通道方向(send
或receive
)。
func generateFibonacci(out chan<- fibvalue, in <-chan int) {
}
这里声明out chan<- fibvalue
指定通道out
用于send
操作,而in <-chan int
指定通道in
用于receive
操作。放置在chan
关键字右侧的通信运算符<-
指定了一个channel
仅用于send
操作;放在chan
关键字的左边,同一个操作符指定一个通道仅用于receive
操作。
通道方向示例
清单 4-8 通过明确指定通道方向重写了清单 4-7 的示例代码。
package main
import (
"fmt"
"math"
"math/rand"
"sync"
)
type fibvalue struct {
input, value int
}
var wg sync.WaitGroup
func randomCounter(out chan<- int) {
defer wg.Done()
var random int
for x := 0; x < 10; x++ {
random = rand.Intn(50)
out <- random
}
close(out)
}
func generateFibonacci(out chan<- fibvalue, in <-chan int) {
defer wg.Done()
var input float64
for v := range in {
input = float64(v)
// Fibonacci using Binet's formula
Phi := (1 + math.Sqrt(5)) / 2
phi := (1 - math.Sqrt(5)) / 2
result := (math.Pow(Phi, input) - math.Pow(phi, input)) / math.Sqrt(5)
out <- fibvalue{
input: v,
value: int(result),
}
}
close(out)
}
func printFibonacci(in <-chan fibvalue) {
defer wg.Done()
for v := range in {
fmt.Printf("Fibonacci value of %d is %d\n", v.input, v.value)
}
}
func main() {
// Add 3 into WaitGroup Counter
wg.Add(3)
// Declare Channels
randoms := make(chan int)
fibs := make(chan fibvalue)
// Launching 3 goroutines
go randomCounter(randoms)
go generateFibonacci(fibs, randoms)
go printFibonacci(fibs)
// Wait for completing all goroutines
wg.Wait()
}
Listing 4-8.A Three-Stage Pipeline with Three Goroutines Connected by Two Channels
在randomCounter
功能中,通道out
仅用于send
操作。generateFibonacci
功能使用两个通道:通道in
用于receive
操作,通道out
用于send
操作。通道in``printFibonacci
功能仅用于receive
操作。
4-6.使用通道进行异步通信
问题
您希望以异步方式通过通道在 goroutines 之间交换数据,并且通道应该能够缓冲值。
解决办法
缓冲通道能够缓冲最大容量的值,并为数据交换提供异步通信。
它是如何工作的
与无缓冲通道不同,缓冲通道可以容纳最大容量的值。缓冲通道就像一个队列,在这个队列上,send
操作不会阻塞任何 goroutine,因为它具有保存元素的能力。只有当通道已满时,缓冲通道上的send
操作才会被阻止,这意味着通道已达到其缓冲容量。缓冲通道的capacity
在使用make
功能创建时确定。下面的语句创建了一个缓冲通道,能够保存三个integer
值的元素。
nums := make(chan int, 3)
下面是对通道nums
进行三个send
操作的代码块:
nums <- 10
nums <- 30
nums <- 50
缓冲通道上的send
操作不会阻止发送 goroutine。这里通道nums
能够保存三个integer
值的元素。一个send
操作在通道的后面插入一个元素,一个receive
操作从通道的前面移除一个元素。这种模式确保缓冲通道上的send
和receive
操作基于先进先出(FIFO)原则。通过send
操作插入的第一个元素将为通道上的第一个receive
操作产生。
以下代码块从通道nums
接收三个值:
fmt.Println(<-nums) // Print 10 (first inserted item)
fmt.Println(<-nums) // Print 30 (second inserted item)
fmt.Println(<-nums) // Print 50 (third inserted item)
一个缓冲通道可以容纳最多的元素。如果一个 goroutine 在缓冲通道上进行的send
操作超过了它的capacity
,这意味着该通道已满,并试图在同一通道上执行另一个send
操作,它会阻塞发送 goroutine,直到有空间可以通过另一个 goroutine 的receive
操作在该通道上插入新元素。同样,在一个空缓冲通道上的receive
操作阻塞接收 goroutine,直到一个元素被另一个 goroutine 的send
操作插入到通道中。
让我们通过编写一个示例程序来探索缓冲通道,如清单 4-9 所示。在这个例子中,一个缓冲通道用于保存来自多个 goroutines 的要执行的任务的信息。缓冲通道能够容纳 10 个指针的元素,这些元素包含关于要完成的作业的信息。正在使用预定义数量的 goroutines 执行这些作业;这是三个。这三个 goroutines 同时从缓冲通道接收值,然后执行作业。
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
type Task struct {
Id int
JobId int
Status string
CreatedOn time.Time
}
func (t *Task) Run() {
sleep := rand.Int63n(1000)
// Delaying the execution for the sake of example
time.Sleep(time.Duration(sleep) * time.Millisecond)
t.Status = "Completed"
}
// wg is used to wait for the program to finish.
var wg sync.WaitGroup
const noOfWorkers = 3
// main is the entry point for all Go programs.
func main() {
// Create a buffered channel to manage the task queue.
taskQueue := make(chan *Task, 10)
// Launch goroutines to handle the work.
// The worker process is distributing with the value of noOfWorkers.
wg.Add(noOfWorkers)
for gr := 1; gr <= noOfWorkers; gr++ {
go worker(taskQueue, gr)
}
// Add Tasks into Buffered channel.
for i := 1; i <= 10; i++ {
taskQueue <- &Task{
Id: i,
JobId: 100 + i,
CreatedOn: time.Now(),
}
}
// Close the channel
close(taskQueue)
// Wait for all the work to get done.
wg.Wait()
}
// worker is launched as a goroutine to process Tasks from
// the buffered channel.
func worker(taskQueue <-chan *Task, workerId int) {
// Schedule the call to Done method of WaitGroup.
defer wg.Done()
for v := range taskQueue {
fmt.Printf("Worker%d: received request for Task:%d - Job:%d\n", workerId, v.Id, v.JobId)
v.Run()
// Display we finished the work.
fmt.Printf("Worker%d: Status:%s for Task:%d - Job:%d\n", workerId, v.Status, v.Id, v.JobId)
}
}
Listing 4-9.Example Demonstrating Buffered Channels
名为Task
的结构类型被定义用于表示要执行的任务。名为Run
的方法被添加到Task
类型中,以复制运行一个任务,该任务将从 goroutines 中执行。
type Task struct {
Id int
JobId int
Status string
CreatedOn time.Time
}
func (t *Task) Run() {
sleep := rand.Int63n(1000)
// Delaying the execution for the sake of example
time.Sleep(time.Duration(sleep) * time.Millisecond)
t.Status = "Completed"
}
通过将指向Task
类型的指针指定为元素类型并将capacity
指定为 10 来创建缓冲通道。
taskQueue := make(chan *Task, 10)
缓冲通道taskQueue
保存要从预定义数量的 goroutines 中执行的任务。通过main
功能,程序启动预定义数量的 goroutines 来分配工作,完成任务的信息可从taskQueue
通道获得。在启动三个 goroutines 之后,缓冲通道被填充了指向Task
值的指针的 10 个元素。
// wg is used to wait for the program to finish.
var wg sync.WaitGroup
const noOfWorkers = 3 // number of goroutines to be used for executing the worker
// main is the entry point for all Go programs.
func main() {
// Create a buffered channel to manage the task queue.
taskQueue := make(chan *Task, 10)
// Launch goroutines to handle the work.
// The worker process is distributing with the value of noOfWorkers.
wg.Add(noOfWorkers)
for gr := 1; gr <= noOfWorkers; gr++ {
go worker(taskQueue, gr)
}
// Add Tasks into Buffered channel.
for i := 1; i <= 10; i++ {
taskQueue <- &Task{
Id: i,
JobId: 100 + i,
CreatedOn: time.Now(),
}
}
// Close the channel
close(taskQueue)
// Wait for all the work to get done.
wg.Wait()
}
函数worker
用于启动 goroutines,通过从缓冲通道接收值来执行任务。该通道包含 10 个任务的信息,通过将worker
函数作为 goroutines 启动,这些任务从三个 go routine 中分配和执行。worker
函数从通道接收元素(指向Task
的指针),然后执行Task
类型的Run
方法来完成任务。
func worker(taskQueue <-chan *Task, workerId int) {
// Schedule the call to Done method of WaitGroup.
defer wg.Done()
for v := range taskQueue {
fmt.Printf("Worker%d: received request for Task:%d - Job:%d\n", workerId, v.Id, v.JobId)
v.Run()
// Display we finished the work.
fmt.Printf("Worker%d: Status:%s for Task:%d - Job:%d\n", workerId, v.Status, v.Id, v.JobId)
}
}
简而言之,在这个例子中,一个缓冲通道被用来发送 10 个任务,这些任务被执行以完成一些工作。因为缓冲通道像队列一样工作,所以通道可以容纳最大容量的值,并且通道上的send
操作不会阻塞 goroutine。这里,在启动一个功能之后,10 个任务的工作由三个 go routine 执行,以便完成 10 个任务的工作可以从多个 go routine 中同时执行。
您应该会看到类似如下的输出:
Worker1: received request for Task:2 - Job:102
Worker3: received request for Task:1 - Job:101
Worker2: received request for Task:3 - Job:103
Worker1: Status:Completed for Task:2 - Job:102
Worker1: received request for Task:4 - Job:104
Worker1: Status:Completed for Task:4 - Job:104
Worker1: received request for Task:5 - Job:105
Worker3: Status:Completed for Task:1 - Job:101
Worker3: received request for Task:6 - Job:106
Worker2: Status:Completed for Task:3 - Job:103
Worker2: received request for Task:7 - Job:107
Worker3: Status:Completed for Task:6 - Job:106
Worker3: received request for Task:8 - Job:108
Worker3: Status:Completed for Task:8 - Job:108
Worker3: received request for Task:9 - Job:109
Worker3: Status:Completed for Task:9 - Job:109
Worker3: received request for Task:10 - Job:110
Worker1: Status:Completed for Task:5 - Job:105
Worker2: Status:Completed for Task:7 - Job:107
Worker3: Status:Completed for Task:10 - Job:110
输出显示,执行 10 个任务的工作是由作为 goroutines 启动的三个 workers 分配的。
4-7.在多个渠道上交流
问题
您希望在多个通道上执行通信操作。
解决办法
Go 提供了一个select
语句,让 goroutine 在多个通道上执行通信操作。
它是如何工作的
当您使用 Go 构建真实世界的并发程序时,您可能需要在一个 goroutine 中处理多个通道,这可能需要您在多个通道上执行通信操作。当与多个通道结合使用时,select
语句是一种强大的通信机制。一个select
块用多个 case 语句编写,让一个 goroutine 等待,直到其中一个 case 可以运行;然后,它执行该案例的代码块。如果有多个 case 块准备好执行,它会随机选择其中一个并执行该 case 的代码块。
清单 4-10 显示了一个示例程序,它执行一个select
块来从一个 goroutine 中的多个通道读取值。
package main
import (
"fmt"
"math"
"math/rand"
"sync"
)
type (
fibvalue struct {
input, value int
}
squarevalue struct {
input, value int
}
)
func generateSquare(sqrs chan<- squarevalue) {
defer wg.Done()
for i := 1; i <= 10; i++ {
num := rand.Intn(50)
sqrs <- squarevalue{
input: num,
value: num * num,
}
}
}
func generateFibonacci(fibs chan<- fibvalue) {
defer wg.Done()
for i := 1; i <= 10; i++ {
num := float64(rand.Intn(50))
// Fibonacci using Binet's formula
Phi := (1 + math.Sqrt(5)) / 2
phi := (1 - math.Sqrt(5)) / 2
result := (math.Pow(Phi, num) - math.Pow(phi, num)) / math.Sqrt(5)
fibs <- fibvalue{
input: int(num),
value: int(result),
}
}
}
func printValues(fibs <-chan fibvalue, sqrs <-chan squarevalue) {
defer wg.Done()
for i := 1; i <= 20; i++ {
select {
case fib := <-fibs:
fmt.Printf("Fibonacci value of %d is %d\n", fib.input, fib.value)
case sqr := <-sqrs:
fmt.Printf("Square value of %d is %d\n", sqr.input, sqr.value)
}
}
}
// wg is used to wait for the program to finish.
var wg sync.WaitGroup
func main() {
wg.Add(3)
// Create Channels
fibs := make(chan fibvalue)
sqrs := make(chan squarevalue)
// Launching 3 goroutines
go generateFibonacci(fibs)
go generateSquare(sqrs)
go printValues(fibs, sqrs)
// Wait for completing all goroutines
wg.Wait()
}
Listing 4-10.A select Block for Reading Values from Multiple Channels
该程序启动了三个 goroutines:一个用于生成 10 个随机生成的数字的斐波那契值;另一个用于产生 10 个随机产生的数字的平方值;最后一个用于打印第一个和第二个 goroutines 生成的结果值。从main
函数中,创建了两个通道,用于传输相应 goroutines 生成的 Fibonacci 值和平方值。函数generateFibonacci
作为 goroutine 启动,它对通道fibs
执行send
操作,以提供斐波那契的值。函数generateSquare
作为 goroutine 启动,它对通道sqrs
执行send
操作,以提供平方值。函数printValues
作为一个 goroutine 启动,它在fibs
和sqrs
通道上轮询,以便在值可以从两个通道receive
输出时打印结果值。
在printValues
函数中,一个select
表达式与两个 case 块一起使用。使用一个for
循环表达式,select
块是 20 次。我们使用 20 次来打印 10 个斐波那契值和 10 个平方值。在真实的场景中,您可能会在一个无限循环中运行它,在这个循环中,您可能会不断地与通道进行通信。
func printValues(fibs <-chan fibvalue, sqrs <-chan squarevalue) {
defer wg.Done()
for i := 1; i <= 20; i++ {
select {
case fib := <-fibs:
fmt.Printf("Fibonacci value of %d is %d\n", fib.input, fib.value)
case sqr := <-sqrs:
fmt.Printf("Square value of %d is %d\n", sqr.input, sqr.value)
}
}
}
这里的select
表达式由两个case
块组成:一个用于fibs
通道上的receive
操作,另一个用于sqrs
通道上的receive
操作。select
语句阻塞 goroutine,直到这些块中的任何一个可以运行,然后它执行那个case
块。如果所有的case
程序块都没有准备好执行,它将一直阻塞,直到一个值sent
进入该程序使用的两个通道中的任何一个。如果有多个case
块准备好执行,它会随机选取一个case
块,然后执行它。
您还可以在一个select
表达式中添加一个缺省块,如果所有其他的 case 块都没有准备好执行,那么它就会执行。还可以在select
块中实现一个超时表达式,如下所示:
select {
case fib := <-fibs:
fmt.Printf("Fibonacci value of %d is %d\n", fib.input, fib.value)
case sqr := <-sqrs:
fmt.Printf("Square value of %d is %d\n", sqr.input, sqr.value)
case <-time.After(time.Second * 3):
fmt.Println("timed out")
}
在前面的代码块中,超时表达式被添加到select
块中。如果select
语句不能在指定的超时时间内运行任何一个case
块,在本例中是 3 秒,那么超时块将被执行。The time.After
函数返回一个通道(<-chan time.Time
,该通道等待给定的持续时间过去,然后在返回的通道上发送当前时间。
您应该会看到类似如下的输出:
Fibonacci value of 31 is 1346268
Square value of 47 is 2209
Fibonacci value of 37 is 24157816
Square value of 9 is 81
Square value of 31 is 961
Square value of 18 is 324
Fibonacci value of 25 is 75025
Fibonacci value of 40 is 102334154
Square value of 0 is 0
Fibonacci value of 6 is 8
Fibonacci value of 44 is 701408732
Square value of 12 is 144
Fibonacci value of 11 is 89
Square value of 39 is 1521
Square value of 28 is 784
Fibonacci value of 11 is 89
Square value of 24 is 576
Square value of 45 is 2025
Fibonacci value of 37 is 24157816
Fibonacci value of 6 is 8