作者:李骁
在前面我们讲了结构(struct)和接口(interface),但关于这两种类型中非常重要的的方法以及方法调用一直没有具体讲解。那么在这一章里,我们来仔细看看方法有那些奇妙之处呢?
在 Go 语言中,结构体就像是类的一种简化形式,那么面向对象程序员可能会问:类的方法在哪里呢?在 Go 中有一个概念,它和方法有着同样的名字,并且大体上意思相近:
Go 方法是作用在接收器(receiver)上的一个函数,接收器是某种类型的变量。因此方法是一种特殊类型的函数。
定义方法的一般格式如下:
func (recv receiver_type) methodName(parameter_list) (return_value_list) { ... }
在方法名之前,func 关键字之后的括号中指定接收器 receiver。
type A struct {
Face int
}
func (a A) f() {
fmt.Println("hi ", a.Face)
}
上面代码中,我们定义了结构体 A ,注意f()就是 A 的方法,(a A)表示接收器。
a 是 receiver 的实例,f()是它的方法名,那么方法调用遵循传统的 object.name 选择器符号:a.f()。
如果 recv 一个指针,Go 会自动解引用。如果方法不需要使用 recv 的值,可以用 _ 替换它,比如:
func (_ receiver_type) methodName(parameter_list) (return_value_list) { ... }
package main
import (
"fmt"
)
type MyInt int
func (m MyInt) p() {
fmt.Println("Now", m)
}
func main() {
var pp MyInt =
作者:李骁
Concurrency is about dealing with lots of things at once.
Parallelism is about doing lots of things at once.并发: 指的是程序的逻辑结构。如果程序代码结构中的某些函数逻辑上可以同时运行,但物理上未必会同时运行。
并行: 并行是指程序的运行状态。并行则指的就是在物理层面也就是使用了不同CPU在执行不同或者相同的任务。
并发是在同一时间处理(dealing with)多件事情。并行是在同一时间做(doing)多件事情。并发的目的在于把当个 CPU 的利用率使用到最高。并行则需要多核 CPU 的支持。
Go 语言从语言层面上就支持了并发,goroutine是Go语言提供的一种用户态线程,有时我们也称之为协程。所谓的协程,某种程度上也可以叫做轻量线程,它不由os,而由应用程序创建和管理,因此使用开销较低(一般为4K)。我们可以创建很多的goroutine,并且它们跑在同一个内核线程之上的时候,就需要一个调度器来维护这些goroutine,确保所有的goroutine都使用cpu,并且是尽可能公平的使用cpu资源。调度器的主要有4个重要部分,分别是M、G、P、Sched,前三个定义在runtime.h中,Sched定义在proc.c中。
M (work thread) 代表了系统线程OS Thread,由操作系统管理。
P (processor) 衔接M和G的调度上下文,它负责将等待执行的G与M对接。P的数量可以通过GOMAXPROCS()来设置,它其实也就代表了真正的并发度,即有多少个goroutine可以同时运行。
G (goroutine) goroutine的实体,包括了调用栈,重要的调度信息,例如channel等。
在操作系统的OS Thread和编程语言的User Thread之间,实际上存在3中线程对应模型,也就是:1:1,1:N,M:N。
N:1 多个(N)用户线程始终在一个内核线程
作者:李骁
Go 奉行通过通信来共享内存,而不是共享内存来通信。所以,channel 是goroutine之间互相通信的通道,goroutine之间可以通过它发消息和接收消息。
channel是进程内的通信方式,因此通过channel传递对象的过程和调用函数时的参数传递行为比较一致,比如也可以传递指针等。
channel是类型相关的,一个channel只能传递(发送或接受 | send or receive)一种类型的值,这个类型需要在声明channel时指定。
默认的,信道的存消息和取消息都是阻塞的 (叫做无缓冲的信道)
使用make来建立一个通道:
var channel chan int = make(chan int)
// 或
channel := make(chan int)
Go中channel可以是发送(send)、接收(receive)、同时发送(send)和接收(receive)。
// 定义接收的channel
receive_only := make (<-chan int)
// 定义发送的channel
send_only := make (chan<- int)
// 可同时发送接收
send_receive := make (chan int)
定义只发送或只接收的channel意义不大,一般用于在参数传递中:
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int) // 不使用带缓冲区的channel
go send(c)
go recv(c)
time.Sleep(3 * time.Second)
close(c)
}
// 只能向chan里send数据
func send(c chan<- int) {
for i := 0;
作者:李骁
Go语言包中的sync包提供了两种锁类型:sync.Mutex和sync.RWMutex,前者是互斥锁,后者是读写锁。
互斥锁是传统的并发程序对共享资源进行访问控制的主要手段,在Go中,似乎更推崇由channel来实现资源共享和通信。它由标准库代码包sync中的Mutex结构体类型代表。只有两个公开方法:调用Lock()获得锁,调用unlock()释放锁。
使用Lock()加锁后,不能再继续对其加锁(同一个goroutine中,即:同步调用),否则会panic。只有在unlock()之后才能再次Lock()。异步调用Lock(),是正当的锁竞争,当然不会有panic了。适用于读写不确定场景,即读写次数没有明显的区别,并且只允许只有一个读或者写的场景,所以该锁也叫做全局锁。
func (m *Mutex) Unlock()用于解锁m,如果在使用Unlock()前未加锁,就会引起一个运行错误。已经锁定的Mutex并不与特定的goroutine相关联,这样可以利用一个goroutine对其加锁,再利用其他goroutine对其解锁。
建议:同一个互斥锁的成对锁定和解锁操作放在同一层次的代码块中。
使用锁的经典模式:
var lck sync.Mutex
func foo() {
lck.Lock()
defer lck.Unlock()
// ...
}
lck.Lock()会阻塞直到获取锁,然后利用defer语句在函数返回时自动释放锁。
下面代码通过3个goroutine来体现sync.Mutex 对资源的访问控制特征:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
wg := sync.WaitGroup{}
var mutex sync.Mutex
fmt.Println("Locking (G0)")
mutex.Lock()
fmt.Println("locked (G0)")
wg.Add(3)
f
作者:李骁
一个指针变量可以指向任何一个值的内存地址。它指向那个值的内存地址,在 32 位机器上占用 4 个字节,在 64 位机器上占用 8 个字节,并且与它所指向的值的大小无关。当然,可以声明指针指向任何类型的值来表明它的原始性或结构性;你可以在指针类型前面加上*号(前缀)来获取指针所指向的内容,这里的*号是一个类型更改器。使用一个指针引用一个值被称为间接引用。
当一个指针被定义后没有分配到任何变量时,它的值为 nil。
一个指针变量通常缩写为 ptr。
符号 "*" 可以放在一个指针前,如 “*intP”,那么它将得到这个指针指向地址上所存储的值;这被称为反引用(或者内容或者间接引用)操作符;另一种说法是指针转移。
对于任何一个变量 var, 如下表达式都是正确的:var == *(&var)
注意事项:
你不能得到一个数字或常量的地址,下面的写法是错误的。
例如:
const i = 5
ptr := &i // error: cannot take the address of i
ptr2 := &10 // error: cannot take the address of 10
所以说,Go 语言和 C、C++ 以及 D 语言这些低级(系统)语言一样,都有指针的概念。
但是对于经常导致 C 语言内存泄漏继而程序崩溃的指针运算(所谓的指针算法,如:pointer+2,移动指针指向字符串的字节数或数组的某个位置)是不被允许的。
Go 语言中的指针保证了内存安全,更像是 Java、C# 和 VB.NET 中的引用。
因此 c = *p++ 在 Go 语言的代码中是不合法的。
指针的一个高级应用是你可以传递一个变量的引用(如函数的参数),这样不会传递变量的拷贝。指针传递是很廉价的,只占用 4 个或 8 个字节。当程序在工作中需要占用大量的内存,或很多变量,或者两者都有,使用指针会减少内存占用和提高效率。被指向的变量也保存在内存中,直到没有任何指针指向它们,所以从它们被创建开始就具有相互独立的生命周期。
另一方面(虽然不太可能),
作者:李骁
我们总结一下前面看到的:Go 没有类,而是松耦合的类型、方法对接口的实现。
OO 语言最重要的三个方面分别是:封装,继承和多态,在 Go 中它们是怎样表现的呢?
封装(数据隐藏):
和别的 OO 语言有 4 个或更多的访问层次相比,Go 把它简化为了 2 层:
1)包范围内的:通过标识符首字母小写,对象 只在它所在的包内可见
2)可导出的:通过标识符首字母大写,对象 对所在包以外也可见类型只拥有自己所在包中定义的方法。
继承:
用组合实现,内嵌一个(或多个)包含想要的行为(字段和方法)的类型;多重继承可以通过内嵌多个类型实现。
多态:
用接口实现,某个类型的实例可以赋给它所实现的任意接口类型的变量。类型和接口是松耦合的,并且多重继承可以通过实现多个接口实现。Go 接口不是 Java 和 C# 接口的变体,而且:接口间是不相关的,并且是大规模编程和可适应的演进型设计的关键。
多重继承指的是类型获得多个父类型行为的能力,它在传统的面向对象语言中通常是不被实现的(C++ 和 Python 例外)。因为在类继承层次中,多重继承会给编译器引入额外的复杂度。但是在 Go 语言中,通过在类型中嵌入所有必要的父类型,可以很简单的实现多重继承。
作者:李骁
首先所有的包都应该有一定的必要文档,然后同样重要的是对包的测试。
名为 testing 的包被专门用来进行自动化测试,日志和错误报告。并且还包含一些基准测试函数的功能。
对一个包做(单元)测试,需要写一些可以频繁(每次更新后)执行的小块测试单元来检查代码的正确性。于是我们必须写一些 Go 源文件来测试代码。测试程序必须属于被测试的包,并且文件名满足这种形式 *_test.Go,所以测试代码和包中的业务代码是分开的。
_test 程序不会被普通的 Go 编译器编译,所以当放应用部署到生产环境时它们不会被部署;只有 Gotest 会编译所有的程序:普通程序和测试程序。
测试文件中必须导入 "testing" 包,并写一些名字以 TestZzz 打头的全局函数,这里的 Zzz 是被测试函数的字母描述,如 TestFmtInterface,TestPayEmployees 等。
测试函数必须有这种形式的头部:
func TestAbcde(t *testing.T)
T 是传给测试函数的结构类型,用来管理测试状态,支持格式化测试日志,如 t.Log,t.Error,t.ErrorF 等。在函数的结尾把输出跟想要的结果对比,如果不等就打印一个错误。成功的测试则直接返回。
用下面这些函数来通知测试失败:
1)func (t *T) Fail()
标记测试函数为失败,然后继续执行(剩下的测试)。
2)func (t *T) FailNow()
标记测试函数为失败并中止执行;文件中别的测试也被略过,继续执行下一个文件。
3)func (t *T) Log(args ...interface{})
args 被用默认的格式格式化并打印到错误日志中。
4)func (t *T) Fatal(args ...interface{})
结合 先执行 3),然后执行 2)的效果。
运行 Go test 来编译测试程序,并执行程序中所有的 TestZZZ 函数。如果所有的测试都通过会打印出 PASS。
对不能导出的函数不能进行单元或者基准测试。
Gote
作者:李骁
反射是用程序检查其所拥有的结构,尤其是类型的一种能力;这是元编程的一种形式。
反射可以在运行时检查类型和变量,例如它的大小、方法和 动态 的调用这些方法。这对于没有源代码的包尤其有用。
这是一个强大的工具,除非真得有必要,否则应当避免使用或小心使用。
变量的最基本信息就是类型和值。反射包的 Type 用来表示一个 Go 类型,反射包的 Value 为 Go 值提供了反射接口。
两个简单的函数,reflect.TypeOf 和 reflect.ValueOf,返回被检查对象的类型和值。
例如,x 被定义为:var x float64 = 3.4,那么 reflect.TypeOf(x) 返回 float64,reflect.ValueOf(x) 返回
实际上,反射是通过检查一个接口的值,变量首先被转换成空接口。这从下面两个函数签名能够很明显的看出来:
func TypeOf(i interface{}) Type
func ValueOf(i interface{}) Value
接口的值包含一个 type 和 value。
反射可以从接口值反射到对象,也可以从对象反射回接口值。
reflect.Type 和 reflect.Value 都有许多方法用于检查和操作它们。一个重要的例子是 Value 有一个 Type 方法返回 reflect.Value 的 Type。另一个是 Type 和 Value 都有 Kind 方法返回一个常量来表示类型:Uint、Float64、Slice 等等。同样 Value 有叫做 Int 和 Float 的方法可以获取存储在内部的值(跟 int64 和 float64 一样)
问题的原因是 v 不是可设置的(这里并不是说值不可寻址)。是否可设置是 Value 的一个属性,并且不是所有的反射值都有这个属性:可以使用 CanSet() 方法测试是否可设置。反射中有些内容是需要用地址去改变它的状态的。
当 v := reflect.ValueOf(x) 函数通过传递一个 x 拷贝创建
作者:李骁
func Alignof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Sizeof(x ArbitraryType) uintptr
type ArbitraryType int
type Pointer *ArbitraryType
这个包中,只提供了3个函数,两个类型。就这么少的量,却有着超级强悍的功能。学过C语言的都可能比较清楚,通过指针,知道变量在内存中占用的字节数,就可以通过指针加偏移量的操作,在地址中,修改,访问变量的值。在Go 语言中,怎么去实现这么疯狂的操作呢?就得靠unsafe包了。
ArbitraryType 是int的一个别名,但是Go 语言中,对ArbitraryType赋予了特殊的意义,千万不要死磕这个后边的int类型。通常,我们把interface{}看作是任意类型,那么ArbitraryType这个类型,在Go 语言系统中,比interface{}还要随意。
Pointer 是int指针类型的一个别名,在Go 语言系统中,可以把Pointer类型,理解成通用指针类型,用于转换不同类型指针。
Go 语言的指针类型长度与int类型长度,在内存中占用的字节数是一样的。ArbitraryType类型的变量也可以是指针。所以,千万不要死磕type后边的那个int。
func Alignof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Sizeof(x ArbitraryType) uintptr
通过分析发现,这三个函数的参数均是ArbitraryType类型,就是接受任何类型的变量。
1. Alignof返回变量对齐字节数量
2. Offsetof返回变量指定属性的偏移量,这个函数虽然接收的是任何类型的变量,但是这个又一个前提,就是变量要是一个struct类型,且还不能直接将这个struct类型的变量当作参数,只能将这个struct
作者:李骁
Go语言标准库sort包中实现了3种基本的排序算法:插入排序、快排和堆排序。和其他语言中一样,这三种方式都是不公开的,他们只在sort包内部使用。所以用户在使用sort包进行排序时无需考虑使用那种排序方式,sort.Interface定义的三个方法:获取数据集合长度的Len()方法、比较两个元素大小的Less()方法和交换两个元素位置的Swap()方法,就可以顺利对数据集合进行排序。sort包会根据实际数据自动选择高效的排序算法。
type Interface
type Interface interface {
Len() int // Len 为集合内元素的总数
Less(i, j int) bool //如果index为i的元素小于index为j的元素,则返回true,否则false
Swap(i, j int) // Swap 交换索引为 i 和 j 的元素
}
sort包里面已经实现了[]int, []float64, []string的排序。
任何实现了 sort.Interface 的类型(一般为集合),均可使用该包中的方法进行排序。这些方法要求集合内列出元素的索引为整数。
package main
import (
"fmt"
"sort"
)
func main() {
a := []int{3, 5, 4, -1, 9, 11, -14}
sort.Ints(a)
fmt.Println(a)
ss := []string{"surface", "ipad", "mac pro", "mac air", "think pad", "idea pad"}
sort.Strings(ss)
fmt.Println(ss)
sort.Sort(sort.Reverse(sort.StringSlice(ss)))
fmt.Printf("After reverse: %v\n", ss)
}
程序输出:
[-14 -1 3 4 5 9 11