11.1 接口
Go 语言不是一种 “传统” 的面向对象编程语言:它里面没有类和继承的概念。
但是 Go 语言里有非常灵活的 接口 概念,通过它可以实现很多面向对象的特性。
接口定义了一组方法(方法集),但是这些方法不包含(实现)代码:它们没有被实现(它们是抽象的)。接口里也不能包含变量。
type Namer interface {
Method1(param_list) return_type
Method2(param_list) return_type
...
}
接口是一种契约,实现类型必须满足它,它描述了类型的行为,规定类型可以做什么。接口彻底将类型能做什么,以及如何做分离开来,使得相同接口的变量在不同的时刻表现出不同的行为,这就是多态的本质。
编写参数是接口变量的函数,这使得它们更具有普适性、一般性。
erPrinterReaderWriterLoggerConvertererRecoverableableI.NETJava
Go 语言中的接口都很简短,通常它们会包含 0 个、最多 3 个方法。
var ai Namerainil
Nameraireceiver
类型不需要显式声明它实现了某个接口:接口被隐式地实现。多个类型可以实现同一个接口。
实现某个接口的类型(除了实现接口方法外)可以有其他的方法。
一个类型可以实现多个接口。
接口类型可以指向一个实例的引用, 该实例的类型实现了此接口(接口是动态类型)。
即使接口在类型之后才定义,二者处于不同的包中,被单独编译:只要类型实现了接口中的方法,它就实现了此接口。
type Shaper interface {
Area() float32
}
type Square struct {
side float32
}
func (sq *Square) Area() float32 {
return sq.side * sq.side
}
type Rectangle struct {
length, width float32
}
func (r Rectangle) Area() float32 {
return r.length * r.width
}
func main() {
r := Rectangle{5, 3} // Area() of Rectangle needs a value
q := &Square{5} // Area() of Square needs a pointer
// shapes := []Shaper{Shaper(r), Shaper(q)}
// or shorter
shapes := []Shaper{r, q}
for n, _ := range shapes {
fmt.Println("Shape details: ", shapes[n])
fmt.Println("Area of this shape is: ", shapes[n].Area()) //接口实例上调用方法
}
}
在接口实例上调用方法,它使此方法更具有一般性
这是 多态 的 Go 版本,多态:根据当前的类型选择正确的方法,或者说:同一种类型在不同的实例上似乎表现出不同的行为。
通过接口产生 更干净、更简单 及 更具有扩展性 的代码。在开发中为类型添加新的接口很容易。
备注
有的时候,也会以一种稍微不同的方式来使用接口这个词:从某个类型的角度来看,它的接口指的是:它的所有导出方法,只不过没有显式地为这些导出方法额外定一个接口而已。
11.2 接口嵌套接口(内嵌接口)
一个接口可以包含一个或多个其他的接口,这相当于直接将这些内嵌接口的方法列举在外层接口中一样。
type ReadWrite interface {
Read(b Buffer) bool
Write(b Buffer) bool
}
type Lock interface {
Lock()
Unlock()
}
type File interface {
ReadWrite
Lock
Close()
}
11.3 类型断言
varI
类型断言
varIT
v := varI.(T) // unchecked type assertion
varI
类型断言可能是无效的,虽然编译器会尽力检查转换是否有效,但是它不可能预见所有的可能性。如报错:panic: interface conversion: main.Shape is *main.Square, not main.Rectangle
2)
if v, ok := varI.(T); ok { // checked type assertion
Process(v)
return
}
// varI is not of type T
vvarIToktruevTokfalse
应该总是使用上面的方式来进行类型断言。
ifok
if _, ok := varI.(T); ok {
// ...
}
type Shape interface {
Area() float64
}
type Square struct {
side float64
}
func (s *Square) Area() float64 {
return s.side * s.side
}
type Rectangle struct {
length, width float64
}
func (r Rectangle) Area() float64 {
return r.length * r.width
}
func main() {
r := Rectangle{3, 5}
s := &Square{5}
shapes := []Shape{r, s}
for i2 := range shapes {
fmt.Printf("index: %d\n", i2)
// v 为指针(*Square)
if v, ok := shapes[i2].(*Square); ok {
fmt.Println("square")
}
// v 为 Rectangle 变量
if v, ok := shapes[i2].(Rectangle); ok {
fmt.Println("square")
}
}
// 判断一个值是否实现了某个接口
if shape, ok := shapes[0].(Shape); ok {
fmt.Println(shape.Area())
}
}
类型判断:type-switch
switch
switch v := shapes[i2].(type) {
case Rectangle:
fmt.Printf("Rectangle: %T, value: %v\n", v, v)
case *Square:
fmt.Printf("Square: %T, value: %v\n", v, v)
case nil:
fmt.Printf("nil value\n")
default:
fmt.Printf("Unexpected type %T\n", v)
}
type-switchtype-switchfallthrough
如果仅仅是测试变量的类型,不用它的值,那么就可以不需要赋值语句,比如:
switch areaIntf.(type) {
case *Square:
// TODO
case *Circle:
// TODO
...
default:
// TODO
}
在处理来自于外部的、类型未知的数据时,比如解析诸如 JSON 或 XML 编码的数据,类型测试和转换会非常有用。
测试一个值是否实现了某个接口
vStringer
if shape, ok := shapes[0].(Shape); ok {
fmt.Println(shape.Area())
}
使用接口使代码更具有普适性。
11.4 接口方法集
接口变量中存储的具体值是不可寻址的
type List []int
func (l List) Len() int {
return len(l)
}
func (l *List) Append(val int) {
*l = append(*l, val)
}
type Appender interface {
Append(int)
}
func CountInto(a Appender, start, end int) {
for i := start; i <= end; i++ {
a.Append(i)
}
}
type Lener interface {
Len() int
}
func LongEnough(l Lener) bool {
return l.Len()*10 > 42
}
func main() {
var lst List
// compiler error:
// cannot use lst (type List) as type Appender in argument to CountInto:
// List does not implement Appender (Append method has pointer receiver)
// CountInto(lst, 1, 10)
if LongEnough(lst) { // VALID: Identical receiver type
fmt.Printf("- lst is long enough\n")
}
// A pointer value
plst := new(List)
CountInto(plst, 1, 10) // VALID: Identical receiver type
if LongEnough(plst) {
// VALID: a *List can be dereferenced for the receiver
fmt.Printf("- plst is long enough\n")
}
}
将一个值赋值给一个接口时,编译器会确保所有可能的接口方法都可以在此值上被调用,因此不正确的赋值在编译期就会失败。
总结
Go 语言规范定义了接口方法集的调用规则:
*T*TTTTT*T
11.5 空接口
空接口或者最小接口 不包含任何方法,它对实现不做任何要求:
type Any interface {}
anyAny
Java/C#Object
var val interface {}
interface {}
构建通用类型或包含不同类型变量的数组
使用空接口
type Element interface{}
type Vector struct {
a []Element
}
VectorElement
复制数据切片至空接口切片
类似:
var dataSlice []myType = FuncReturnSlice()
var interfaceSlice []interface{} = dataSlice //错误
cannot use dataSlice (type []myType) as type []interface { } in assignment
// 必须使用 `for-range` 语句来一个一个显式地赋值
var interfaceSlice []interface{} = make([]interface{}, len(dataSlice))
for i, d := range dataSlice {
interfaceSlice[i] = d
}
结论: interface{} 可以接收任何类型,但是[]interface{} 并不可以接收任何类型的切片
可是普通类型的切片内存布局是:
通用类型的节点数据结构
type Node struct {
le *Node
data interface{}
ri *Node
}
func NewNode(left, right *Node) *Node {
return &Node{left, nil, right}
}
func (n *Node) SetData(data interface{}) {
n.data = data
}
接口到接口
GoRubyPython
type any01 interface {
Name() string
}
type any02 interface {
Age() int
}
type type01 struct {
}
func (t *type01) Name() string {
return "hello"
}
func (t *type01) Age() int {
return 19
}
func main() {
var empty interface{}
var a any01
var c any02
fmt.Printf("%T\n", a)
b := new(type01)
empty = b
a = empty.(any01)
c = a.(any02)
c.Age()
}
aany02aAgeaAgeAgeif mpi, ok := a.(any02); ok { mpi.Age() }
十二、反射 (reflection)/反射包
Value
type Value struct {
// contains filtered or unexported fields
}
nil
一个值可以被多个go例程并发使用,前提是底层的Go值可以并发地用于等价的直接操作。
要比较两个 Value,请比较Interface方法的结果。对两个Value使用==不会比较它们表示的基础值。
1、Interface()
func (v Value) Interface() (i any)
var i interface{} = (v's underlying value)
2、Type()
func (v Value) Type() Type
返回v的 Type。
3、Kind()
func (v Value) Kind() Kind
Kind returns v’s Kind. If v is the zero Value (IsValid returns false), Kind returns Invalid.
4、Len()
func (v Value) Len() int
Len返回v的长度。如果v的Kind不是Array、Chan、Map、Slice、String或指向Array的指针,它会 panic
5、Int()
func (v Value) Int() int64
Int返回v的基础值,作为int64。如果v的Kind不是Int、Int8、Int16、Int32或Int64,则会panic
6、Float()
func (v Value) Float() float64
Float返回v的基础值,为float64。如果v的种类不是floati32或floati64,它会panic
7、String()
func (v Value) String() string
8、Bool()
func (v Value) Bool() bool
Bool返回v的基础值。如果v不是Bool类型,它会 panic
9、Complex()
func (v Value) Complex() complex128
Complex返回v的基础值,作为 complex128。如果v的Kind不是Complex64或Complex128,它就会 panic
10、CanSet()
func (v Value) CanSet() bool
CanSet 确定是否可以修改v的值。只有当值是可寻址的且不是通过使用未导出的结构字段获得时,才可以更改值。如果CanSet返回false,调用Set或任何类型特定的setter(例如SetBool, SetInt)将会出现panic。
11、NumField()
func (v Value) NumField() int
NumField 返回结构v中字段的数量,如果v的Kind不是struct则会panic
12、NumMethod()
func (v Value) NumMethod() int
NumMethod 返回该Value的方法集中的方法数量。
对于非接口类型,它返回导出方法的数量。
对于接口类型,它返回导出和未导出方法的数量。
13、Call()
func (v Value) Call(in []Value) []Value
调用输入参数为in的函数v。例如,如果len(in) == 3, v. call (in)表示Go调用v(in[0], in[1], in[2])。如果v星人不是Func就叫恐慌。它以值的形式返回输出结果。在Go中,每个输入参数必须可赋值给函数对应的输入参数的类型。如果v是一个可变参数函数,Call创建可变参数片本身,复制相应的值。
14、Field()
func (v Value) Field(i int) Value
Field 返回结构体v的第i个字段。如果v的Kind不是Struct,或者i超出了范围,则会产生 panic
15、Method()
func (v Value) Method(i int) Value
方法返回对应于v的第 i (从0开始)个方法的函数值。调用返回函数的参数不应该包括 receiver;返回的函数将始终使用v作为 receiver。如果 i 超出了范围,或者v为nil接口值,则方法会 panic
16、Elem()
func (v Value) Elem() Value
Elem返回接口 v 所包含的值或 v 所指向的指针的值。如果 v 的种类不是接口或指针,它会panic。如果 v 为nil,则返回零值。
17、MapKeys
// MapKeys返回一个包含map中所有键的切片,未指定顺序
// 如果v的Kind不是Map,它就会恐慌
// 如果v表示nil Map,则返回空片。
func (v Value) MapKeys() []Value
Type
type Type interface {
// 字段返回一个结构类型的第i个字段。
// 如果类型的类型不是结构体,它会 panic。如果i不在范围[0,NumField())),它会 panic
Field(i int) StructField
// contains filtered or unexported methods
}
Type 是Go类型的表示。
并非所有方法都适用于所有类型的值。
在调用特定于类型的方法之前,使用Kind方法找出值的类型。调用不适合类型的方法会导致panic
Type 值具有可比性,例如==操作符,因此可以将它们用作映射键。
如果两个Type值表示相同的类型,则它们是相等的。
Kind
type Kind uint
Kind 表示一个 Type 所表示的特定类型。The zero Kind is not a valid kind.
const (
Invalid Kind = iota
Bool
Int
Int8
Int16
Int32
Int64
Uint
Uint8
Uint16
Uint32
Uint64
Uintptr
Float32
Float64
Complex64
Complex128
Array
Chan
Func
Interface
Map
Pointer
Slice
String
Struct
UnsafePointer
)
String()
func (k Kind) String() string
String returns the name of k.
12.1 方法和类型的反射
反射是用程序检查其所拥有的结构,尤其是类型的一种能力;这是元编程的一种形式。
反射可以在运行时检查变量的类型,例如:它的大小、它的方法以及它能“动态地”调用这些方法。这对于没有源代码的包尤其有用。这是一个强大的工具,除非真得有必要,否则应当避免使用或小心使用。
TypeValue
reflect.TypeOfreflect.ValueOf
func reflectTest() {
var x float64 = 3.4
fmt.Println(reflect.TypeOf(x)) // float64
fmt.Println(reflect.ValueOf(x)) // 3.4
}
实际上,反射是通过检查一个接口的值,变量首先被转换成空接口,接口的值包含一个 type 和 value。
1、ValueOf()
func ValueOf(i any) Value
ValueOf返回一个新值,初始化为存储在接口i中的具体值。
ValueOf(nil) returns the zero Value.
2、TypeOf()
func TypeOf(i any) Type
TypeOf返回表示i的动态类型的反射Type
If i is a nil interface value, TypeOf returns nil.
反射可以从接口值反射到对象(即反射对象),也可以从对象反射回接口值。
ValueType()reflect.ValueTypeTypeValueKind()UintFloat64SliceValueInt()Float()int64float64
type MyInt int
var m MyInt = 5
v := reflect.ValueOf(m)
v.Kind() // reflect.Int
vInterface()vfmt.Println(v.Interface())
12.2 通过反射修改(设置)值
var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println(v.Type())
fmt.Println(v.CanSet()) // false
// v.SetFloat(3.1414) //panic: reflect: reflect.Value.SetFloat using unaddressable value
// v = reflect.ValueOf(x) // panic: reflect: call of reflect.Value.Elem on float64 Value
v = reflect.ValueOf(&x)
fmt.Println(v.Type()) // *float64
v = v.Elem()
fmt.Println(v.CanSet()) // true
v.SetFloat(3.1415)
fmt.Println(v.Interface()) // 3.1415
fmt.Println(v.String()) // 3.1415
reflect.Value.SetFloat using unaddressable valuevValueCanSet()
v := reflect.ValueOf(x)xvvxvxv = reflect.ValueOf(&x)
Elem()v = v.Elem()
反射中有些内容是需要用地址去改变它的状态的。
12.3 反射结构类型
NumField()forField(i)
nMethod(n).Call(nil)
type notKnownType struct {
s1, s2, s3 string
}
func (n notKnownType) String() string {
return n.s1 + "-" + n.s2 + "-" + n.s3
}
var secret interface{} = notKnownType{"Ada", "Oberon", "Go"}
func reflectTest02() {
value := reflect.ValueOf(secret)
typ := reflect.TypeOf(secret)
fmt.Println("type:", typ)
kind := value.Kind()
fmt.Println("kind:", kind)
for i := 0; i < value.NumField(); i++ {
fmt.Printf("field %d: %v\n", i, value.Field(i))
}
result := value.Method(0).Call(nil)
fmt.Println(result)
}
但是如果尝试更改一个值,会得到一个错误:
panic: reflect.Value.SetString using value obtained using unexported field
这是因为结构中只有被导出字段(首字母大写)才是可设置的
func reflectTest03() {
t := T{23, "abc"}
//v := reflect.ValueOf(t).Elem() // panic: reflect: call of reflect.Value.Elem on struct Value
v := reflect.ValueOf(&t).Elem()
t2 := v.Type()
for i := 0; i < v.NumField(); i++ {
f := v.Field(i)
fmt.Printf("%d: %s %s = %v\n", i, t2.Field(i).Name, f.Type(), f.Interface())
}
v.Field(0).SetInt(100)
v.Field(1).SetString("def")
fmt.Println("更改后的值:", v)
}
12.4 Printf() 和反射
fmtPrintf()...
func Printf(format string, args ... interface{}) (n int, err error)
Printf()...Printf()Printf()%d%u%ldPrint()Println()
12.5 接口与动态类型
12.5.1 Go 的动态类型
在经典的面向对象语言(像 C++,Java 和 C#)中数据和方法被封装为类的概念:类包含它们两者,并且不能剥离。
Go 没有类:数据(结构体或更一般的类型)和方法是一种松耦合的正交关系。
Go 中的接口跟 Java/C# 类似:都是必须提供一个指定方法集的实现。但是更加灵活通用:任何提供了接口方法实现代码的类型都隐式地实现了该接口,而不用显式地声明。
和其它语言相比,Go 是唯一结合了接口值,静态类型检查(是否该类型实现了某个接口),运行时动态转换的语言,并且不需要显式地声明类型是否满足某个接口。该特性允许我们在不改变已有的代码的情况下定义和使用新接口。
接收一个(或多个)接口类型作为参数的函数,其实参可以是任何实现了该接口的类型的变量。 实现了某个接口的类型可以被传给任何以此接口为参数的函数。
类似于 Python 和 Ruby 这类动态语言中的动态类型 (duck typing);这意味着对象可以根据提供的方法被处理(例如,作为参数传递给函数),而忽略它们的实际类型:它们能做什么比它们是什么更重要。
12.5.2 动态方法调用
像 Python,Ruby 这类语言,动态类型是延迟绑定的(在运行时进行):方法只是用参数和变量简单地调用,然后在运行时才解析
interface{}
12.5.3 接口的提取
提取接口是非常有用的设计模式,可以减少需要的类型和方法数量,而且不需要像传统的基于类的面向对象语言那样维护整个的类层次结构。
Go 接口可以让开发者找出自己写的程序中的类型。假设有一些拥有共同行为的对象,并且开发者想要抽象出这些行为,这时就可以创建一个接口来使用。
12.5.4 显式地指明类型实现了某个接口
如果你希望满足某个接口的类型显式地声明它们实现了这个接口,你可以向接口的方法集中添加一个具有描述性名字的方法。例如:
type Fooer interface {
Foo()
ImplementsFooer()
}
ImplementsFooerFooer
type Bar struct{}
func (b Bar) ImplementsFooer() {}
func (b Bar) Foo() {}
大部分代码并不使用这样的约束,因为它限制了接口的实用性。
但是有些时候,这样的约束在大量相似的接口中被用来解决歧义。
12.5.5 空接口和函数重载
...TTTfmt.Printf
fmt.Printf(format string, a ...interface{}) (n int, errno error)
String()
12.5.6 接口的继承
当一个类型包含(内嵌)另一个类型(实现了一个或多个接口)的指针时,这个类型就可以使用(另一个类型)所有的接口方法。
例如:
type Task struct {
Command string
*log.Logger
}
这个类型的工厂方法像这样:
func NewTask(command string, logger *log.Logger) *Task {
return &Task{command, logger}
}
log.LoggerLog()Tasktask
task.Log()
类型可以通过继承多个接口来提供像多重继承一样的特性:
type ReaderWriter struct {
*io.Reader
*io.Writer
}
上面概述的原理被应用于整个 Go 包,多态用得越多,代码就相对越少。这被认为是 Go 编程中的重要的最佳实践。
有用的接口可以在开发的过程中被归纳出来。添加新接口非常容易,因为已有的类型不用变动(仅仅需要实现新接口的方法)。已有的函数可以扩展为使用接口类型的约束性参数:通常只有函数签名需要改变。对比基于类的 OO 类型的语言在这种情况下则需要适应整个类层次结构的变化。
12.6 Go 中的面向对象
总结:Go 没有类,而是松耦合的类型、方法对接口的实现。
OO 语言最重要的三个方面分别是:封装、继承和多态,在 Go 中它们是怎样表现的呢?
-
封装(数据隐藏):和别的 OO 语言有 4 个或更多的访问层次相比,Go 把它简化为了 2 层):
1)包范围内的:通过标识符首字母小写,对象只在它所在的包内可见
2)可导出的:通过标识符首字母大写,对象对所在包以外也可见
类型只拥有自己所在包中定义的方法。
- 继承:用组合实现:内嵌一个(或多个)包含想要的行为(字段和方法)的类型;多重继承可以通过内嵌多个类型实现
- 多态:用接口实现:某个类型的实例可以赋给它所实现的任意接口类型的变量。类型和接口是松耦合的,并且多重继承可以通过实现多个接口实现。Go 接口不是 Java 和 C# 接口的变体,而且接口间是不相关的,并且是大规模编程和可适应的演进型设计的关键。
12.7 结构体、集合和高阶函数
通常在应用中定义了一个结构体,也可能需要这个结构体的(指针)对象集合,比如:
type Any interface{}
type Car struct {
Model string
Manufacturer string
BuildYear int
// ...
}
type Cars []*Car
然后我们就可以使用高阶函数,实际上也就是把函数作为定义所需方法(其他函数)的参数,例如:
Process()
// Process all cars with the given function f:
func (cs Cars) Process(f func(car *Car)) {
for _, c := range cs {
f(c)
}
}
Process()cars
// Find all cars matching a given criteria.
func (cs Cars) FindAll(f func(car *Car) bool) Cars {
cars := make([]*Car, 0)
cs.Process(func(c *Car) {
if f(c) {
cars = append(cars, c)
}
})
return cars
}
car
// Process cars and create new data.
func (cs Cars) Map(f func(car *Car) Any) []Any {
result := make([]Any, 0)
ix := 0
cs.Process(func(c *Car) {
result[ix] = f(c)
ix++
})
return result
}
现在我们可以定义下面这样的具体查询:
allNewBMWs := allCars.FindAll(func(car *Car) bool {
return (car.Manufacturer == "BMW") && (car.BuildYear > 2010)
})
map
func MakeSortedAppender(manufacturers []string)(func(car *Car),map[string]Cars) {
// Prepare maps of sorted cars.
sortedCars := make(map[string]Cars)
for _, m := range manufacturers {
sortedCars[m] = make([]*Car, 0)
}
sortedCars["Default"] = make([]*Car, 0)
// Prepare appender function:
appender := func(c *Car) {
if _, ok := sortedCars[c.Manufacturer]; ok {
sortedCars[c.Manufacturer] = append(sortedCars[c.Manufacturer], c)
} else {
sortedCars["Default"] = append(sortedCars["Default"], c)
}
}
return appender, sortedCars
}
现在我们可以用它把汽车分类为独立的集合,像这样:
manufacturers := []string{"Ford", "Aston Martin", "Land Rover", "BMW", "Jaguar"}
sortedAppender, sortedCars := MakeSortedAppender(manufacturers)
allUnsortedCars.Process(sortedAppender)
BMWCount := len(sortedCars["BMW"])
十三、协程 (goroutine) 与通道 (channel)
Go 原生支持应用之间的通信(网络,客户端和服务端,分布式计算和程序的并发。程序可以在不同的处理器和计算机上同时执行不同的代码段。Go 语言为构建并发程序的基本代码块是协程 (goroutine) 与通道 (channel)。他们需要语言,编译器,和 runtime 的支持。Go 语言提供的垃圾回收器对并发编程至关重要。
不要通过共享内存来通信,而通过通信来共享内存。
通信强制协作。
13.1 什么是协程
一个应用程序是运行在机器上的一个进程;进程是一个运行在自己内存地址空间里的独立执行体。一个进程由一个或多个操作系统线程组成,这些线程其实是共享同一个内存地址空间的一起工作的执行体。
一个并发程序可以在一个处理器或者内核上使用多个线程来执行任务,但是只有同一个程序在某个时间点同时运行在多核或者多处理器上才是真正的并行。
并行是一种通过使用多处理器以提高速度的能力。所以并发程序可以是并行的,也可以不是。
公认的,使用多线程的应用难以做到准确,最主要的问题是内存中的数据共享,它们会被多线程以无法预知的方式进行操作,导致一些无法重现或者随机的结果(称作竞态)。
不要使用全局变量或者共享内存,它们会给你的代码在并发运算的时候带来危险。
解决之道在于同步不同的线程,对数据加锁,这样同时就只有一个线程可以变更数据。
Communicating Sequential Processes(顺序通信处理)message passing-model(消息传递)
goroutines(协程)
channels
协程是轻量的,比线程更轻:使用 4K 的栈内存就可以在堆中创建它们。
存在两种并发方式:确定性的(明确定义排序)和非确定性的(加锁/互斥从而未定义排序)。Go 的协程和通道理所当然的支持确定性的并发方式(例如通道具有一个 sender 和一个 receiver)
go
协程的栈会根据需要进行伸缩,不出现栈溢出;开发者不需要关心栈的大小。当协程结束的时候,它会静默退出:用来启动这个协程的函数不会得到任何的返回值。
main()go
13.2 并发和并行的差异
并行是一种通过使用多处理器以提高速度的能力。
往往是,一个设计良好的并发程序在并行方面的表现也非常出色。
runtime.GOMAXPROCS()
这会告诉运行时有多少个协程同时执行。
环境变量
GOMAXPROCSn-11 + GOMAXPROCSGOMAXPROCS
GOMAXPROCS
runtime.Goexit()
GoexitGoexitGoexitnil
协程按顺序启动,然后开始并行运行
协程是独立的处理单元,一旦陆续启动一些协程,你无法确定他们是什么时候真正开始执行的。你的代码逻辑必须独立于协程调用的顺序。
Go 协程 (goroutines) 和协程 (coroutines)
在其他语言中,比如 C#,Lua 或者 Python 都有协程的概念。这个名字表明它和 Go 协程有些相似,不过有两点不同:
- Go 协程意味着并行(或者可以以并行的方式部署),协程一般来说不是这样的
- Go 协程通过通道来通信;协程通过让出和恢复操作来通信
13.3 协程间的信道
协程间通信才会变得更有用:彼此之间发送和接收信息并且协调/同步他们的工作
通道(channel),就像一个可以用于发送类型化数据的管道,由其负责协程之间的通信,从而避开所有由共享内存导致的陷阱;这种通过通道进行通信的方式保证了同步性。数据在通道中进行传递:在任何给定时间,一个数据被设计为只有一个协程可以对其访问,所以不会发生数据竞争。 数据的所有权(可以读写数据的能力)也因此被传递。
通道服务于通信的两个目的:值的交换和同步,保证了两个计算(协程)任何时候都是可知状态。
声明通道:
var identifier chan datatype
nilchan intchan stringinterface{}
通道实际上是类型化消息的队列,它是先进先出(FIFO) 的结构
make()
var ch1 chan string
ch1 = make(chan string)
funcChan := make(chan func())
通道是第一类对象:可以存储在变量中,作为函数的参数传递,从函数返回以及通过通道发送它们自身。另外它们是类型化的,允许类型检查,比如尝试使用整数通道发送一个指针。
通信操作符 <-
ch <- int1chint1
int2 := <- chint2
chchanx, ok := <-ch
func main() {
var chInt chan string
chInt = make(chan string)
go send(chInt)
go getData(chInt)
time.Sleep(9 * 1e9)
}
func send(ch chan string) {
ch <- "hello"
ch <- "world"
}
func getData(ch chan string) {
var input string
for {
input = <-ch
fmt.Printf("%s ", input)
}
}
如果 2 个协程需要通信,你必须给他们同一个通道作为参数才行。
getData()send()ch
go
通道阻塞
默认情况下,通信是同步且无缓冲的(cap()为0)。无缓冲的通道(unbuffered channel)是指在接收前没有能力保存任何值的通道。这种类型的通道要求发送goroutine 和接收goroutine 同时准备好,才能完成发送和接收操作。所以通道的发送/接收操作在对方准备好之前是阻塞的:
1)对于同一个通道,发送操作(协程或者函数中的),在接收者准备好之前是阻塞的:
2)对于同一个通道,接收操作是阻塞的(协程或函数中的),直到发送者可用:如果通道中没有数据,接收者就阻塞了。
通过一个(或多个)通道交换数据进行协程同步
通信是一种同步形式:通过通道,两个协程在通信(协程会合)中某刻同步交换数据。无缓冲通道成为了多个协程同步的完美工具。
甚至可以在通道两端互相阻塞对方,形成了叫做死锁(程序运行不下去)的状态。
异步通道-使用带缓冲的通道
make
buf := 100
ch1 := make(chan string, buf) // buf 是通道可以同时容纳的元素(这里是 `string`个数)
在缓冲满载(缓冲被全部使用)之前,给一个带缓冲的通道发送数据是不会阻塞的,而从通道读取数据也不会阻塞,直到缓冲空了。
cap()
缓冲容量和类型无关
ch :=make(chan type, value)
- value == 0 -> synchronous, unbuffered (阻塞)
- value > 0 -> asynchronous, buffered(非阻塞)取决于 value 元素
在设计算法时首先考虑使用无缓冲通道,只在不确定的情况下使用缓冲。
协程中用通道输出结果
ch := make(chan int)
go sum(bigArray, ch) // bigArray puts the calculated sum on ch
// .. do something else for a while
sum := <- ch // wait for, and retrieve the sum
可以使用通道来达到同步的目的,这个很有效的用法在传统计算机中称为信号量 (semaphore)
或者换个方式:通过通道发送信号告知处理已经完成(在协程中)
在其他协程运行时让 main 程序无限阻塞的通常做法是在 main() 函数的最后放置一个 select {}
也可以使用通道让 main 程序等待协程完成,就是所谓的信号量模式
select
信号量模式
chmain()<-ch
func compute(ch chan int){
ch <- someComputation() // when it completes, signal on the channel.
}
func main(){
ch := make(chan int) // allocate a channel.
go compute(ch) // start something in a goroutines
doSomethingElseForAWhile()
result := <- ch
}
这个信号也可以是其他的,不返回结果
type Empty interface {}
var empty Empty
...
data := make([]float64, N)
res := make([]float64, N)
sem := make(chan Empty, N)
...
for i, xi := range data { // for 循环的每一个迭代是并行完成的:
go func (i int, xi float64) {
res[i] = doSomething(i, xi)
sem <- empty
} (i, xi)
}
// wait for goroutines to finish
for i := 0; i < N; i++ { <-sem }
ixiixi
for
for
用带缓冲通道实现一个信号量
sync
- 带缓冲通道的容量和要同步的资源容量相同
- 通道的长度(当前存放的元素个数)与当前资源被使用的数量相同
- 容量减去通道的长度就是未处理的资源个数(标准信号量的整数值)
因此我们创建了一个长度可变但容量为 0(字节)的通道
type Empty interface {}
type semaphore chan Empty
直接对信号量进行操作:
// acquire n resources
func (s semaphore) P(n int) {
e := new(Empty)
for i := 0; i < n; i++ {
s <- e
}
}
// release n resources
func (s semaphore) V(n int) {
for i:= 0; i < n; i++{
<- s
}
}
实现一个互斥的例子
/* mutexes */
func (s semaphore) Lock() {
s.P(1)
}
func (s semaphore) Unlock(){
s.V(1)
}
/* signal-wait */
func (s semaphore) Wait(n int) {
s.P(n)
}
func (s semaphore) Signal() {
s.V(1)
}
// integer producer:
func numGen(start, count int, out chan<- int) {
for i := 0; i < count; i++ {
out <- start
start = start + count
}
close(out)
}
习惯用法:通道工厂模式
func pump() chan int {
ch := make(chan int)
go func() {
for i := 0; ; i++ {
ch <- i
}
}()
return ch
}
func suck(ch chan int) {
go func() {
for v := range ch { //通道使用 for 循环
fmt.Println(v)
}
}()
}
func main() {
ch := pump()
suck(ch)
time.Sleep(2e9)
}
给通道使用 for 循环
forrangech
for v := range ch {
fmt.Printf("The value is %v\n", v)
}
习惯用法:通道迭代器模式
itemsIter()items
func (c *container) Iter () <- chan item {
ch := make(chan item)
go func () {
for i:= 0; i < c.Len(); i++{ // or use a for-range loop
ch <- c.items[i]
}
} ()
return ch
}
调用这个方法的代码可以这样迭代容器:
for x := range container.Iter() { ... }
迭代运行在自己启动的协程中,所以上边的迭代用到了一个通道和两个协程(可能运行在不同的线程上)。 这样我们就有了一个典型的生产者-消费者模式。如果在程序结束之前,向通道写值的协程未完成工作,则这个协程不会被垃圾回收;这是设计使然。这种看起来并不符合预期的行为正是由通道这种线程安全的通信方式所导致的。如此一来,一个协程为了写入一个永远无人读取的通道而被挂起就成了一个 bug ,而并非你预想中的那样被悄悄回收掉 (garbage-collected) 了。
两个协程经常是一个阻塞另外一个。如果程序工作在多核心的机器上,大部分时间只用到了一个处理器。可以通过使用带缓冲(缓冲空间大于 0)的通道来改善。
习惯用法:生产者消费者模式
for {
Consume(Produce())
}
通道的方向
通道类型可以用注解来表示它只发送或者只接收:
var send_only chan<- int // channel can only send data
var recv_only <-chan int // channel can only receive data
<-chan T
var c = make(chan int) // bidirectional
go source(c)
go sink(c)
func source(ch chan<- int){
for { ch <- 1 }
}
func sink(ch <-chan int) {
for { <-ch }
}
习惯用法:管道和选择器模式
Go 指导的很赞的例子,打印了输出的素数,使用选择器(‘筛’)作为它的算法。每个 prime 都有一个选择器。
func generate(ch chan int) {
for i := 2; ; i++ {
ch <- i
}
}
func filter(in, out chan int, prime int) {
for {
i := <-in
if i%prime != 0 {
out <- i
}
}
}
func main() {
ch := make(chan int)
go generate(ch)
for {
prime := <-ch
fmt.Print(prime, " ")
ch1 := make(chan int)
go filter(ch, ch1, prime)
ch = ch1
time.Sleep(1e9)
}
}
版本二:
// Send the sequence 2, 3, 4, ... to returned channel
func generate() chan int {
ch := make(chan int)
go func() {
for i := 2; ; i++ {
ch <- i
}
}()
return ch
}
// Filter out input values divisible by 'prime', send rest to returned channel
func filter(in chan int, prime int) chan int {
out := make(chan int)
go func() {
for {
if i := <-in; i%prime != 0 {
out <- i
}
}
}()
return out
}
func sieve() chan int {
out := make(chan int)
go func() {
ch := generate()
for {
prime := <-ch
ch = filter(ch, prime)
out <- prime
}
}()
return out
}
func main() {
primes := sieve()
for {
fmt.Println(<-primes)
}
}
13.4 协程的同步:关闭通道-测试阻塞的通道
通道可以被显式的关闭;尽管它们和文件不同:不必每次都关闭。只有在当需要告诉接收者不会再提供新的值的时候,才需要关闭通道。只有发送者需要关闭通道,接收者永远不会需要。
defer
ch := make(chan float64)
defer close(ch)
close(ch)<-panic()
如何来检测可以收到没有被阻塞(或者通道没有被关闭)?
使用逗号 ok 模式用来检测通道是否被关闭
v, ok := <-ch // ok is true if v received value
if v, ok := <-ch; ok {
process(v)
}
select
select {
case v, ok := <-ch:
if ok {
process(v)
} else {
fmt.Println("The channel is closed")
}
default:
fmt.Println("The channel is blocked")
}
select
使用 for-range 语句来读取通道是更好的办法,因为这会自动检测通道是否关闭:
for input := range ch {
process(input)
}
func source() chan int {
ch := make(chan int)
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
fmt.Println("通道关闭。。。")
}()
return ch
}
func main() {
ch := source()
for v := range ch {
fmt.Println("获取到的值为: ", v)
}
time.Sleep(1e9)
fmt.Println("main 结束。")
}
结果:
获取到的值为: 0
获取到的值为: 1
获取到的值为: 2
获取到的值为: 3
获取到的值为: 4
通道关闭。。。
main 结束。
阻塞和生产者-消费者模式:
前面通道迭代器中,两个协程经常是一个阻塞另外一个。如果程序工作在多核心的机器上,大部分时间只用到了一个处理器。可以通过使用带缓冲(缓冲空间大于 0)的通道来改善。
由于容器中元素的数量通常是已知的,需要让通道有足够的容量放置所有的元素。这样,迭代器就不会阻塞(尽管消费者协程仍然可能阻塞)。然而,这实际上加倍了迭代容器所需要的内存使用量,所以通道的容量需要限制一下最大值。记录运行时间和性能测试可以帮助你找到最小的缓存容量带来最好的性能。
13.5 使用 select 切换协程
selectselect
select {
case u:= <- ch1:
...
case v:= <- ch2:
...
...
default: // no value ready to be received
...
}
defaultfallthroughswitchcasebreakreturn
select
defaultdefault
selectdefaultdefaultselect
select
caseselectcase
func suck(ch1, ch2 chan int) {
for {
select {
case v, ok := <-ch1:
if ok {
fmt.Printf("Received on channel 1: %d\n", v)
}
case v := <-ch2:
fmt.Printf("Received on channel 2: %d\n", v)
}
}
}
习惯用法:后台服务模式
select
// Backend goroutine.
func backend() {
for {
select {
case cmd := <-ch1:
// Handle ...
case cmd := <-ch2:
...
case cmd := <-chStop:
// stop server
}
}
}
chRequestswitch
func backend() {
for req := range chRequest {
switch req.Subjext() {
case A1: // Handle case ...
case A2: // Handle case ...
default:
// Handle illegal request ..
// ...
}
}
}
13.6 通道、超时和计时器(Ticker)
timetime.TickerC
type Ticker struct {
C <-chan Time // the channel on which the ticks are delivered.
// contains filtered or unexported fields
...
}
nsint64time.NewTickerDurationfunc NewTicker(dur) *Ticker
在协程周期性的执行一些事情(打印状态日志,输出,计算等等)的时候非常有用。
ticker := time.NewTicker(updateInterval)
defer ticker.Stop()
...
select {
case u:= <-ch1:
...
case v:= <-ch2:
...
case <-ticker.C:
logState(status) // call some logging function logState
default: // no value ready to be received
...
}
习惯用法:简单超时模式
ch
timeout := make(chan bool, 1)
go func() {
time.Sleep(1e9) // one second
timeout <- true
}()
selectchtimeoutchtimeoutch
select {
case <-ch:
// a read from ch has occured
case <-timeout:
// the read from ch has timed out
break
}
time.After()timeout-channel
ch := make(chan error, 1)
go func() { ch <- client.Call("Service.Method", args, &reply) } ()
select {
case resp := <-ch
// use resp and reply
case <-time.After(timeoutNs):
// call timed out
break
}
timeoutNsselecttimeoutclient.Callch1
Query
func Query(conns []Conn, query string) Result {
ch := make(chan Result, 1)
for _, conn := range conns {
go func(c Conn) {
select {
case ch <- c.DoQuery(query):
default:
}
}(conn)
}
return <- ch
}
13.7 协程和恢复 (recover)
停掉了服务器内部一个失败的协程而不影响其他协程的工作
func server(workChan <-chan *Work) {
for work := range workChan {
go safelyDo(work) // start the goroutine for that work
}
}
func safelyDo(work *Work) {
defer func() {
if err := recover(); err != nil {
log.Printf("Work failed with %s in %v", err, work)
}
}()
do(work)
}
do(work)panic()
deferpanic()recover()safelyDo()deferrecover()logging()logging()do()panic()
13.8 新旧模型对比:任务和 worker
假设我们需要处理很多任务;一个 worker 处理一项任务。
type Task struct {
// some state
}
旧模式:使用共享内存进行同步
由各个任务组成的任务池共享内存;为了同步各个 worker 以及避免资源竞争,我们需要对任务池进行加锁保护:
type Pool struct {
Mu sync.Mutex
Tasks []*Task
}
sync.Mutex
新模式:使用通道
对一个通道读数据和写数据的整个过程是原子性的
使用通道进行同步:使用一个通道接受需要处理的任务,一个通道接受处理完成的任务(及其结果)
主线程扮演着 Master 节点角色:
func main() {
pending, done := make(chan *Task), make(chan *Task)
go sendWork(pending) // put tasks with work on the channel
for i := 0; i < N; i++ { // start N goroutines to do work
go Worker(pending, done)
}
consumeWork(done) // continue with the processed tasks
}
pendingdone
func Worker(in, out chan *Task) {
for {
t := <-in
process(t)
out <- t
}
}
对于任何可以建模为 Master-Worker 范例的问题,一个类似于 worker 使用通道进行通信和交互、Master 进行整体协调的方案都能完美解决。如果系统部署在多台机器上,各个机器上执行 Worker 协程,Master 和 Worker 之间使用 netchan 或者 RPC 进行通信
怎么选择是该使用锁还是通道?
-
使用锁的情景:
- 访问共享数据结构中的缓存信息
- 保存应用程序上下文和状态信息数据
-
使用通道的情景:
- 与异步操作的结果进行交互
- 分发任务
- 传递数据所有权
当你发现你的锁使用规则变得很复杂时,可以反省使用通道会不会使问题变得简单些。
13.9 惰性生成器的实现
生成器是指当被调用时返回一个序列中下一个值的函数,例如:
generateInteger() => 0
generateInteger() => 1
generateInteger() => 2
....
生成器每次返回的是序列中下一个值而非整个序列;这种特性也称之为惰性求值:只在你需要时进行求值,同时保留相关变量资源(内存和 CPU):这是一项在需要时对表达式进行求值的技术
生成一个无限数量的自然数序列:
var resume chan int
func integers() chan int {
yield := make(chan int)
count := 0
go func() {
for {
yield <- count
count++
}
}()
return yield
}
func generateInteger() int {
return <-resume
}
BuildLazyEvaluator
type Any interface{}
type EvalFunc func(Any) (Any, Any)
func main() {
evenFunc := func(state Any) (Any, Any) {
os := state.(int)
ns := os + 2
return os, ns
}
even := BuildLazyIntEvaluator(evenFunc, 0)
for i := 0; i < 10; i++ {
fmt.Printf("%vth even: %v\n", i, even())
}
}
func BuildLazyEvaluator(evalFunc EvalFunc, initState Any) func() Any {
retValChan := make(chan Any)
loopFunc := func() {
var actState Any = initState
var retVal Any
for {
retVal, actState = evalFunc(actState)
retValChan <- retVal
}
}
retFunc := func() Any {
return <- retValChan
}
go loopFunc()
return retFunc
}
func BuildLazyIntEvaluator(evalFunc EvalFunc, initState Any) func() int {
ef := BuildLazyEvaluator(evalFunc, initState)
return func() int {
return ef().(int)
}
}
13.10 实现 Futures 模式
所谓 Futures 就是指:有时候在你使用某一个值之前需要先对其进行计算。这种情况下,你就可以在另一个处理器上进行该值的计算,到使用时,该值就已经计算完毕了。
Futures 模式通过闭包和通道可以很容易实现,类似于生成器,不同地方在于 Futures 需要返回一个值。
计算两个矩阵 A 和 B 乘积的逆:
func InverseProduct(a Matrix, b Matrix) {
a_inv_future := InverseFuture(a) // start as a goroutine
b_inv_future := InverseFuture(b) // start as a goroutine
a_inv := <-a_inv_future
b_inv := <-b_inv_future
return Product(a_inv, b_inv)
}
func InverseFuture(a Matrix) chan Matrix {
future := make(chan Matrix)
go func() {
future <- Inverse(a)
}()
return future
}
当开发一个计算密集型库时,使用 Futures 模式设计 API 接口是很有意义的。
13.11 典型的客户端/服务器(C/S)模式
客户端-服务器应用正是 goroutines 和 channels 的亮点所在。
使用 Go 的服务器通常会在协程中执行向客户端的响应,故而会对每一个客户端请求启动一个协程。一个常用的操作方法是客户端请求自身中包含一个通道,而服务器则向这个通道发送响应。
type Reply struct{...}
type Request struct{
arg1, arg2, arg3 some_type
replyc chan *Reply
}
run()
type binOp func(a, b int) int
func run(op binOp, req *Request) {
req.replyc <- op(req.a, req.b)
}
func server(op binOp, service chan *Request) {
for {
req := <-service; // requests arrive here
// start goroutine for request:
go run(op, req); // don’t wait for op to complete
}
}
func startServer(op binOp) chan *Request {
reqChan := make(chan *Request);
go server(op, reqChan);
return reqChan;
}
func main() {
adder := startServer(func(a, b int) int { return a + b })
const N = 100
var reqs [N]Request
for i := 0; i < N; i++ {
req := &reqs[i]
req.a = i
req.b = i + N
req.replyc = make(chan int)
adder <- req // adder is a channel of requests
}
}
卸载 (Teardown):通过信号通道关闭服务器
server()main()
func startServer(op binOp) (service chan *Request, quit chan bool) {
service = make(chan *Request)
quit = make(chan bool)
go server(op, service, quit)
return service, quit
}
server()selectservicequit
func server(op binOp, service chan *request, quit chan bool) {
for {
select {
case req := <-service:
go run(op, req)
case <-quit:
return
}
}
}
quittrueserver
main()
adder, quit := startServer(func(a, b int) int { return a + b })
main()quit <- true
限制同时处理的请求数
使用带缓冲区的通道很容易实现这一点,其缓冲区容量就是同时处理请求的最大数量。
const MAXREQS = 50
var sem = make(chan int, MAXREQS)
type Request struct {
a, b int
replyc chan int
}
func process(r *Request) {
// do something
}
func handle(r *Request) {
sem <- 1 // doesn't matter what we put in it
process(r)
<-sem // one empty place in the buffer: the next request can start
}
func server(service chan *Request) {
for {
request := <-service
go handle(request)
}
}
func main() {
service := make(chan *Request)
go server(service)
}
通过这种方式,应用程序可以通过使用缓冲通道(通道被用作信号量)使协程同步其对该资源的使用,从而充分利用有限的资源(如内存)。
13.12 链式协程
展示了启动巨量的 Go 协程是多么容易
var numGoroutine = flag.Int("n", 100000, "How many goroutines")
func q(left, right chan int) {
left <- 1 + <-right
}
func main() {
flag.Parse()
leftmost := make(chan int)
var left, right chan int = nil, leftmost
log.Println("启动协程。。。")
for i := 0; i < *numGoroutine; i++ {
left, right = right, make(chan int)
go q(left, right)
}
log.Println("协程启动完毕。。。")
right <- 0
x := <-leftmost
log.Println("结果为:", x) // 100000 about <1s
}
main()for0100000
13.13 在多核心上并行计算
NCPUconst NCPU = 4 //对应一个四核处理器NCPU
func DoAll(){
sem := make(chan int, NCPU) // Buffering optional but sensible
for i := 0; i < NCPU; i++ {
go DoPart(sem)
}
// Drain the channel sem, waiting for NCPU tasks to complete
for i := 0; i < NCPU; i++ {
<-sem // wait for one task to complete
}
// All done.
}
func DoPart(sem chan int) {
// do the part of the computation
sem <-1 // signal that this piece is done
}
func main() {
runtime.GOMAXPROCS(NCPU) // runtime.GOMAXPROCS = NCPU
DoAll()
}
sem
13.14 并行化大量数据的计算
inout
Preprocess(预处理) / StepA(步骤A) / StepB(步骤B) / … / PostProcess(后处理)
一个更高效的计算方式是让每一个处理步骤作为一个协程独立工作。每一个步骤从上一步的输出通道中获得输入数据。这种方式仅有极少数时间会被浪费,而大部分时间所有的步骤都在一直执行中:
func ParallelProcessData (in <-chan *Data, out chan<- *Data) {
// make channels:
preOut := make(chan *Data, 100)
stepAOut := make(chan *Data, 100)
stepBOut := make(chan *Data, 100)
stepCOut := make(chan *Data, 100)
// start parallel computations:
go PreprocessData(in, preOut)
go ProcessStepA(preOut,StepAOut)
go ProcessStepB(StepAOut,StepBOut)
go ProcessStepC(StepBOut,StepCOut)
go PostProcessData(StepCOut,out)
}
通道的缓冲区大小可以用来进一步优化整个过程。
13.15 漏桶算法
Buffervar freeList = make(chan *Buffer,100)freeListfreeListserverChan
var serverChan = make(chan *Buffer)
以下是客户端的算法代码:
func client() {
for {
var b *Buffer
// Grab a buffer if available; allocate if not
select {
case b = <-freeList:
// Got one; nothing more to do
default:
// None free, so allocate a new one
b = new(Buffer)
}
loadInto(b) // Read next message from the network
serverChan <- b // Send to server
}
}
服务器的循环则接收每一条来自客户端的消息并处理它,之后尝试将缓冲返回给共享的空闲缓冲区:
func server() {
for {
b := <-serverChan // Wait for work.
process(b)
// Reuse buffer if there's room.
select {
case freeList <- b:
// Reuse buffer if free slot on freeList; nothing more to do
default:
// Free list full, just carry on: the buffer is 'dropped'
}
}
}
freeListfreeList
13.16 对 Go 协程进行基准测试
func main() {
fmt.Println("Sync:", testing.Benchmark(BenchmarkChannelSync).String())
fmt.Println("Buffered:", testing.Benchmark(BenchmarkChannelBuffered).String())
}
func BenchmarkChannelSync(b *testing.B) {
ch := make(chan int)
go func() {
for i := 0; i < b.N; i++ {
ch <- i
}
close(ch)
}()
for range ch {
}
}
func BenchmarkChannelBuffered(b *testing.B) {
ch := make(chan int, 100)
go func() {
for i := 0; i < b.N; i++ {
ch <- i
}
close(ch)
}()
for range ch {
}
}
Sync: 5192534 237.0 ns/op
Buffered: 18510776 63.96 ns/op
13.17 使用通道并发访问对象
type Person struct {
name string
salary float64
chF chan func()
}
func NewPerson(name string, salary float64) *Person {
p := &Person{name, salary, make(chan func())}
go p.backend()
return p
}
func (p *Person) backend() {
for f2 := range p.chF {
f2()
}
}
func (p *Person) SetSalary(salary float64) {
p.chF <- func() {
p.salary = salary
}
}
func (p *Person) Salary() float64 {
fChan := make(chan float64)
p.chF <- func() {
fChan <- p.salary
}
return <-fChan
}
func (p *Person) String() string {
return "Person - name: " + p.name + "- salary: " + strconv.FormatFloat(p.Salary(), 'f', 2, 64)
}
func main() {
person := NewPerson("William", 1800.09)
fmt.Println("初始:", person)
person.SetSalary(1900.78)
fmt.Println("修改后:", person)
}
PersonchF