Go 秘籍(二)

原文:Go Recipes

协议:CC BY-NC-SA 4.0

三、结构和接口

当你写程序时,你选择的语言的类型系统是非常重要的。类型允许您以结构化的方式组织应用程序数据,这些数据可以保存在各种数据存储区中。当您编写应用程序(尤其是业务应用程序)时,您使用各种类型来组织应用程序数据,并将这些类型的值保存到持久化存储中。当你用 Go 编写应用程序时,理解它的类型系统和设计理念是很重要的。Go 提供了intuintfloat64stringbool等多种内置类型。用于存储数组、切片和映射等值集合的数据结构被称为复合类型,因为它们由其他类型(内置类型和用户定义类型)组成。除了 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调用其嵌入类型CustomerToStringShippingAddress方法。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接口在其契约中提供了两个行为:PrintNamePrintDetails.

将接口实现为具体类型

让我们通过实现接口的两个行为PrintNamePrintDetails来创建一个具体类型的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接口。因为类型EmployeeTeamMember接口的实现,所以类型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

这里你调用了EmployeePrintDetails方法,并为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结构还提供了ProjectsLocations字段来表示经理管理的项目和位置。

到目前为止,您已经创建了一个名为TeamMember的接口类型,以及实现TeamMember接口的三个具体类型:EmployeeDeveloperManager。让我们创建一个示例程序来探索这些类型并演示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仅用于嵌入到DeveloperManager结构中,这些结构更多的是作为团队成员的雇员的具体实现。

type Team struct {
    Name, Description string
    TeamMembers       []TeamMember
}

TeamPrintTeamDetails方法打印一个Team对象的信息。在PrintTeamDetails方法中,它遍历TeamMembers集合的元素,并调用PrintNamePrintDetails方法来获取每个团队成员的信息。

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 启动的addTablemultiTable函数中,WaitGroupDone方法被调度使用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

您可以看到,addTablemultiTable函数同时在控制台窗口中生成输出,因为它们是并发执行的。在addTablemultiTable函数中,为了演示起见,执行会延迟一段随机生成的时间。当您运行程序时,输出的顺序每次都会不同,因为函数内部的执行是随机延迟的。

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值。

渠道沟通

一个通道有三种操作:sendreceiveclosesend操作向通道发送一个值或指针,当执行相应的receive操作时,该值或指针从通道中读取。通信操作符<-用于sendreceive操作:

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的缓冲通道也是用元素类型integercapacity3 创建的,这意味着它最多可以缓冲三个值。从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

缓冲和非缓冲通道的sendreceive操作具有不同的行为。在接下来的部分中,我们将详细研究缓冲通道和无缓冲通道。

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 中的sendreceive operation需要执行另一个 goroutine 中相应的sendreceive操作。这种通信机制确保了数据从一个路由器传递到另一个路由器。

僵局

为了理解无缓冲信道上通信操作的阻塞行为,让我们写一个程序。清单 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

创建一个名为countinteger类型的无缓冲通道,并启动两个 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 从通道接收到该值。sendreceive继续运行,直到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操作,这样当您将通道指定为参数时,您可以指定通道方向(sendreceive)。

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操作从通道的前面移除一个元素。这种模式确保缓冲通道上的sendreceive操作基于先进先出(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 启动,它在fibssqrs通道上轮询,以便在值可以从两个通道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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值