本篇主要是包过方法,接口,并发三个内容
1.golang方法
方法这个概念是面向对象来的,而golang是面向接口的语言,这里的方法不是(OOM)里的方法。我们要学习golang的接口,首先要知道方法,因为接口是方法声明的集合。
1.1方法的概念
- 方法用于展示和维护对象的自身状态(原谅我用对象这个词,尽管在go里可能不太合适,但是为了方便理解)。
- 方法是有关联状态的,而函数通常没有。
- 方法不支持重载(overload),receiver参数名字没有限制
- 可以为除了接口以及指针以外的其他类型定义方法(基础类型,自定义类型,甚至是包!)
- 方法和函数的定义区别在于前者有前置实例接受参数(receiver),编译器以此确定方法所属的类型。
让我们用例子来进一步理解他
package main
import "fmt"
type N int
/*
在面向对象的编程中,我们习惯在定义一个对象的方法的时候,在它的类定义中使用this.method(),
在某些语言里,尽管没有显式定义,但是会在调用的时候隐式使用this实例参数,
来调用方法,而在go里面我们在定义方法的时候用receiver来决定它属于那个类型
*/
//如果方法内部并不引用实例,可省略参数名,仅保留类型
func (N) test(){
println("hi")
}
//如果传入N会被编译器解释称*n
func (n N) value() {
n++
fmt.Printf("address:%p,value:%v\n", &n, n)
}
//如果传入n会被编译器解释成&n
func (n *N) pointer() {
(*n)++
fmt.Printf("address:%p,value:%v\n", n, *n)
}
func Methodtest() {
var n N = 10
p := &n
fmt.Printf("n:%p,value:%v\n", &n, n)
n.value()
n.pointer()
p.value()
p.pointer()
}
//output
n:0x118a007c,value:10
address:0x114120e0,value:11
address:0x114120bc,value:11
address:0x114120e4,value:12
address:0x114120bc,value:12
复制代码
注意到了嘛,我的方法定义的receiver不管是对象还是对象的指针,我都可以传入给他一个对象,反过来也是一样的! 当然了指针的值不能是nil,或者任何没有值的地址。
可以是用实例值或者指针的调用方式,编译器会根据方法recerver类型自动选择实例值或者指针类型切换。
值得注意的是结构体也是如此,结构体的指针和值对象编译器都会自动帮我们转化成需要的方式
1.1.1方法的receiver类型如何选择
- 要修改实例状态*T
- 无需修改状态的小对象或者固定值T
- 大对象用*T,减少复制成本
- 引用类型,字符串,函数等指针包装的对象,直接用T
- 若包含MuTex等同步字段,用*T,避免因为复制造成锁无效
- 其他无法确定的情况都用*T
1.1.2方法的override
package main
import "fmt"
type usr struct{}
type manager struct {
u1 usr
}
func (usr) toString() string {
return "user"
}
func (manager) toString() string {
return "manager"
}
func anonymousTest() {
var m manager
fmt.Println(m.toString())
fmt.Print(m.u1.toString())
}
//output
manager
user
复制代码
可以看到成员usr具有和Manager同名的方法,所以它把他重写了,直接用的是自己的方法,所以虽然没有重载,但是有重写!但是尽管能访问成员的方法,他们不属于继承关系,仅仅是重写!
1.1.3方法访问匿名成员
//如果 manager内部的成员变量是匿名的呢,要怎么调用它的方法?
package main
import "fmt"
type usr struct{}
type manager struct {
usr
}
func (usr) toString() string {
return "user"
}
func (manager) toString() string {
return "manager"
}
func anonymousTest() {
var m manager
fmt.Println(m.toString())
fmt.Print(m.usr.toString())
}
//可以看出来只要告诉它结构体的成员类型,编译器会自动寻找成员
复制代码
类型有一个与之相关的方法集,这决定了它是否实现了某个接口
- 类型T方法集包含所有receiver T方法
- 类型*T方法包含所有receiverT+*T方法
- T中匿名嵌入S,T的方法集就会包含receiverT+S方法
- T中匿名嵌入*S,T的方法集就会包含receiverT+S+*S方法
- T中匿名嵌入S或者S,T的方法集就会包含receiverT+S+S+T*的方法
package main
import (
"fmt"
"reflect"
)
type struct1 struct {
struct2
}
type struct2 struct {}
func (struct1) ValMethod1(){}
func (*struct1) PointMethod1(){}
func (struct2) ValMethod2(){}
func (*struct2) PointMethod2(){}
func methodSet(a interface{}){
t:=reflect.TypeOf(a) //返回方法集里所有方法的名字
for i,num:=0,t.NumMethod();i<num;i++{
m:=t.Method(i)
fmt.Println(m.Name,m.Type)
}
}
func reflectTest(){
var stu1 struct1
//用值调用成员方法
methodSet(stu1)
fmt.Println("---------------------------")
//用指针调用成员方法
methodSet(&stu1)
}
//output
ValMethod1 func(main.struct1)
ValMethod2 func(main.struct1)
---------------------------
PointMethod1 func(*main.struct1)
PointMethod2 func(*main.struct1)
ValMethod1 func(*main.struct1)
ValMethod2 func(*main.struct1)
复制代码
1.2 方法表达式
方法和和函数一样,除了直接调用外,还可以复制给变量,或者作为参数传递
1.method expression:
这是通过类型引用的传参,这时候receiver是第一参数,调用时要使用显式传参
2.method value:
基于指针或者实例引用的,参数签名不会发声改变,会立即计算并且复制该方法所需的receiver对象,与其绑定以便在稍后执行的时候,可以传入receiver参数
示例如下
package main
import "fmt"
type NN int
func (n NN) test(){
fmt.Println("address of n:",&n)
}
func methodExpression(){
//method expression
var n NN=25
fmt.Println("n's address is",&n)
f1:=NN.test
f1(n)
fmt.Println("-------------")
f2:=(*NN).test
f2(&n)
fmt.Println("-------------")
NN.test(n)
(*NN).test(&n)
//使用表达式传入方法的receiver就要严格按照方法签名传入给方法变量
//(*NN).test(n)
//(NN).test(&n)
//method value
fmt.Println("-------------")
f3:=n.test
f3()
fmt.Println("-------------")
f4:=(&n).test
f4()
}
复制代码
2.golang接口
接口是一系列方法签名的集合。注意的是一个receiver为*n的方法,如果是属于一个接口,那么用n去调用它不算实现了这个接口
2.1接口与隐式实现
类型通过实现一个接口的所有方法来实现该接口。既然无需专门显式声明,也就没有“implements”关键字。
隐式接口从接口的实现中解耦了定义,这样接口的实现可以出现在任何包中,无需提前准备。
那么空接口可以当成被所有类型实现,接口值承接所有数据类型
即便接口内的具体值为 nil,方法仍然会被 nil 接收者调用。但是接口内具体值为NIL的接口本身部位nil
package main
import (
"fmt"
"strconv"
"strings"
)
type IPAddr [4]byte
func (p IPAddr) String() string {
var ipParts []string
for _, item := range p {
ipParts = append(ipParts, strconv.Itoa(int(item)))
}
return strings.Join(ipParts, ".")
}
func stringerTest() {
hosts := map[string]IPAddr{
"loopback": {127, 0, 0, 1},
"googleDNS": {8, 8, 8, 8},
}
for name, ip := range hosts {
fmt.Printf("%v: %v\n", name, ip)
}
}
复制代码
上述代码为IPAddr实现了string方法,因为Printf调用的时候是借助内容的String()方法来打印字符串的。
2.1.1 接口的嵌套
可以像匿名字段那样嵌入其他接口,但是目标类型方法集中必须拥有包含嵌入接口方法在内的全部方法才算实现了该接口(),要求不能有同名的方法,因为不支持重载,还有不能嵌入自身或者循环嵌入,因为会导致递归错误。
package main
type stringer interface{
string() string
}
type tester interface{
stringer
test()
}
type data struct{}
func (*data) test(){}
func (data) string() string{
return ""
}
func main(){
var d data
var t tester=&d
t.test()
println(t.string())
}
复制代码
上述例子中超级接口变量可以隐式转化为子集接口变量,反过来不行。
上述代码中加入
func pp(a stringer){
println(a.string())
}
main函数改成
func main(){
var d data
var t tester=&d
pp(t) //超级接口变量隐式转化为子集接口变量
var s stringer=t//超集转化为子集
println(s.string())
//var t2 tester=s这个就不行
}
复制代码
2.2接口值
接口也是值。默认值为nil。它们可以像其它值一样传递。接口值可以用作函数的参数或返回值。
在内部,接口值可以看做包含值和具体类型的元组:
(value, type)
-
接口值保存了一个具体底层类型的具体值。
-
接口值调用方法时会执行其底层类型的同名方法。
一个接口变量可以是被定义成是实现了他的方法的receiver的变量
package main
import (
"fmt"
"math"
)
type I interface {
M()
}
type T struct {
S string
}
func (t *T) M() {
fmt.Println(t.S)
}
type F float64
func (f F) M() {
fmt.Println(f)
}
func main() {
var i I
//因为T是实现了接口I中的M方法,所以可以这样定义
i = &T{"Hello"}
describe(i)
i.M()
i = F(math.Pi)
describe(i)
i.M()
}
func describe(i I) {
fmt.Printf("(%v, %T)\n", i, i)
}
//output
(&{Hello}, *main.T)
Hello
(3.141592653589793, main.F)
3.141592653589793
复制代码
空接口
指定了零个方法的接口值被称为 空接口:
interface{}
空接口可保存任何类型的值。(因为每个类型都至少实现了零个方法。)类似于面向对象的Object
2.3类型断言
package main
import "fmt"
func main() {
var i interface{} = "hello"
s := i.(string)
fmt.Println(s)
//该语句断言接口值 i 保存了具体类型 T,并将其底层类型为 T 的值赋予变量 t。
s, ok := i.(string)
fmt.Println(s, ok)
f, ok := i.(float64)
fmt.Println(f, ok)
f = i.(float64) // 报错(panic)
fmt.Println(f)
}
复制代码
类型断言还有一种用法是类型选择
package main
import "fmt"
func do(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("Twice %v is %v\n", v, v*2)
case string:
fmt.Printf("%q is %v bytes long\n", v, len(v))
default:
fmt.Printf("I don't know about type %T!\n", v)
}
}
func main() {
do(21)
do("hello")
do(true)
}
复制代码
2.4接口应用实例
2.4.1 Stringer
fmt 包中定义的 Stringer 是最普遍的接口之一。
type Stringer interface {
String() string
}
复制代码
Stringer 是一个可以用字符串描述自己的类型。fmt 包(还有很多包)都通过此接口来打印值。
2.4.2 Error
Go 程序使用 error 值来表示错误状态。
与 fmt.Stringer 类似,error 类型是一个内建接口:
type error interface {
Error() string
}
复制代码
(与 fmt.Stringer 类似,fmt 包在打印值时也会满足 error。)
通常函数会返回一个 error 值,调用的它的代码应当判断这个错误是否等于 nil 来进行错误处理。
i, err := strconv.Atoi("42")
if err != nil {
fmt.Printf("couldn't convert number: %v\n", err)
return
}
fmt.Println("Converted integer:", i)
\\error 为 nil 时表示成功;非 nil 的 error 表示失败。
复制代码
Error的练习:
从之前的练习中复制 Sqrt 函数,修改它使其返回 error 值。Sqrt 接受到一个负数时,应当返回一个非 nil 的错误值。复数同样也不被支持。创建一个新的类型
type ErrNegativeSqrt float64
并为其实现
func (e ErrNegativeSqrt) Error() string
方法使其拥有 error 值,通过 ErrNegativeSqrt(-2).Error() 调用该方法应返回 “cannot Sqrt negative number: -2″。 修改 Sqrt 函数,使其接受一个负数时,返回 ErrNegativeSqrt 值。
package main
import (
"fmt"
"math"
)
type ErrNegativeSqrt float64
func (e ErrNegativeSqrt) Error() string{
return fmt.Sprint("cannot Sqrt negative number:",float64(e))
}
//注意上面这个Sprint中的e先用float做了转换,不转换会进入死循环?因为Sprint回去找这个类型里返回了String的方法,然后就会陷入死循环的调用
//为什么下面这个y可以当作error返回呢,因为他拥有error接口里的Error方法,实现了error接口
//相当于这一句var er error = y,这就是前面提到的接口值得概念了,接口值可以用来装载实现该接口的变量
func Sqrt(x float64) (float64, error) {
if x<0{
y:=ErrNegativeSqrt(x)
return 0,y
}
return math.Sqrt(x),nil
}
func main() {
fmt.Println(Sqrt(2))
fmt.Println(Sqrt(-2))
}
复制代码
2.4.3 Reader
io 包指定了 io.Reader 接口,它表示从数据流的末尾进行读取。
Go 标准库包含了该接口的许多实现,包括文件、网络连接、压缩和加密等等。
io.Reader 接口有一个 Read 方法:
func (T) Read(b []byte) (n int, err error)
复制代码
Read 用数据填充给定的字节切片并返回填充的字节数和错误值。在遇到数据流的结尾时,它会返回一个 io.EOF 错误。
示例代码创建了一个 strings.Reader 并以每次 8 字节的速度读取它的输出。
package main
import (
"fmt"
"io"
"strings"
)
func main() {
r := strings.NewReader("Hello, Reader!")
b := make([]byte, 8)
for {
n, err := r.Read(b)
fmt.Printf("n = %v err = %v b = %v\n", n, err, b)
fmt.Printf("b[:n] = %q\n", b[:n])
if err == io.EOF {
break
}
}
}
复制代码
练习:rot13Reader
有种常见的模式是一个 io.Reader 包装另一个 io.Reader,然后通过某种方式修改其数据流。
例如,gzip.NewReader 函数接受一个 io.Reader(已压缩的数据流)并返回一个同样实现了 io.Reader 的 *gzip.Reader(解压后的数据流)。
编写一个实现了 io.Reader 并从另一个 io.Reader 中读取数据的 rot13Reader,通过应用 rot13 代换密码对数据流进行修改。
rot13Reader 类型已经提供。实现 Read 方法以满足 io.Reader。
package main
import (
"io"
"os"
"strings"
)
type rot13Reader struct {
r io.Reader
}
func (ro13 *rot13Reader) Read(s []byte)(int,error){
//我有一个比较疑惑的店?为什么不是&ro13
n,er:=ro13.r.Read(s)
if er!=nil{
return 0,er
}
for i:=range s{
s[i]=transferRot13(s[i])
}
return n,nil
}
func transferRot13(b byte) byte{
switch {
case 'A' <= b && b <= 'M':
b = b + 13
case 'M' < b && b <= 'Z':
b = b - 13
case 'a' <= b && b <= 'm':
b = b + 13
case 'm' < b && b <= 'z':
b = b - 13
}
return b
}
func main() {
s := strings.NewReader("Lbh penpxrq gur pbqr!")
r := rot13Reader{s}
io.Copy(os.Stdout, &r)
}
复制代码
2.4.4图像
image 包定义了 Image 接口:
package image
type Image interface {
ColorModel() color.Model
Bounds() Rectangle
At(x, y int) color.Color
}
复制代码
注意: Bounds 方法的返回值 Rectangle 实际上是一个 image.Rectangle,它在 image 包中声明。
color.Color 和 color.Model 类型也是接口,但是通常因为直接使用预定义的实现 image.RGBA 和 image.RGBAModel 而被忽视了。这些接口和类型由 image/color 包定义。
练习:图像
定义你自己的 Image 类型,实现必要的方法并调用 pic.ShowImage。
Bounds 应当返回一个 image.Rectangle ,例如 image.Rect(0, 0, w, h)。
ColorModel 应当返回 color.RGBAModel。
At 应当返回一个颜色。上一个图片生成器的值 v 对应于此次的 color.RGBA{v, v, 255, 255}
package main
import (
"golang.org/x/tour/pic"
"image"
"image/color"
)
type Image struct{} //新建一个Image结构体
func (i Image) ColorModel() color.Model{ //实现Image包中颜色模式的方法
return color.RGBAModel
}
func (i Image) Bounds() image.Rectangle{ //实现Image包中生成图片边界的方法
return image.Rect(0,0,200,200)
}
func (i Image) At(x,y int) color.Color { //实现Image包中生成图像某个点的方法
return color.RGBA{R: uint8(x), G: uint8(y), B: uint8(255), A: uint8(255)}
}
func ImageTest() {
m := Image{}
pic.ShowImage(m) //调用
}
复制代码
3.goroutine线程
go程是由go运行时管理的轻量级线程,Go 程在相同的地址空间中运行,因此在访问共享的内存时必须进行同步。
Go程准确来说既不是线程,也不是协程,而是两种的综合体,能最大限度提升执行效率,发挥多核处理能力。goroutine和defer相似,也有延迟执行
var c int
func counter() int{
c++
return c
}
func main(){
d:=100
go func(x,y int){
time.sleep(time.Second)
println("go",x,y)
}(a,counter())//立即计算并且复制参数
a+=100
println("main",a,counter())
time.Sleep(time.Second*3)//等待goroutine结束
}
output
main:200 2
go:100 1
复制代码
可以看到虽然go程的执行顺序和定义不一定一致可能会延时。
3.1channel信道
信道是带有类型的管道,你可以通过它用信道操作符 <- 来发送或者接收值。信道的作用在于给不同的go程之间通信
ch <- v // 将 v 发送至信道 ch。
v := <-ch // 从 ch 接收值并赋予 v。
复制代码
和映射与切片一样,信道在使用前必须创建:
ch := make(chan int)
复制代码
默认情况下,发送和接收操作在另一端准备好之前都会阻塞。这使得 Go 程可以在没有显式的锁或竞态变量的情况下进行同步。
信道可以是 带缓冲的。将缓冲长度作为第二个参数提供给 make 来初始化一个带缓冲的信道:
ch := make(chan int, 100)
仅当信道的缓冲区填满后,向其发送数据时才会阻塞。当缓冲区为空时,接受方会阻塞。
3.1range和close
发送者可以使用close来表示信道关闭,没有需要再发送的值.接收者可以通过为接收表达式分配第二个参数来测试信道是否被关闭:若没有值可以接收且信道已被关闭,那么在执行完
v, ok := <-ch
复制代码
之后 ok 会被设置为 false。
循环 for i := range c 会不断从信道接收值,直到它被关闭。
注意: 只有发送者才能关闭信道,而接收者不能。向一个已经关闭的信道发送数据会引发程序恐慌(panic)。
还要注意: 信道与文件不同,通常情况下无需关闭它们。只有在必须告诉接收者不再有需要发送的值时才有必要关闭,例如终止一个 range 循环。
package main
import (
"fmt"
)
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}
func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
for i := range c {
fmt.Println(i)
}
}
复制代码
3.3 select语句
select 语句使一个 Go 程可以等待多个通信操作。
select 会阻塞到某个分支可以继续执行为止,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。而default语句会在所有信道堵塞的时候执行
package main
import "fmt"
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
复制代码
package main
import (
"fmt"
"time"
)
func main() {
tick := time.Tick(100 * time.Millisecond)
boom := time.After(500 * time.Millisecond)
for {
select {
case <-tick:
fmt.Println("tick.")
case <-boom:
fmt.Println("BOOM!")
return
default:
fmt.Println(" .")
time.Sleep(50 * time.Millisecond)
}
}
}
复制代码
3.4 锁
go的标准库sync中提供了锁的机制,我们通常用互斥锁Mutex这一数据结构来提供。Mutex有两个方法lock和unlock,也可以采用defer语句来保证一定会解锁。
package main
import (
"fmt"
"sync"
"time"
)
// SafeCounter 的并发使用是安全的。
type SafeCounter struct {
v map[string]int
mux sync.Mutex
}
// Inc 增加给定 key 的计数器的值。
func (c *SafeCounter) Inc(key string) {
c.mux.Lock()
// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
c.v[key]++
c.mux.Unlock()
}
// Value 返回给定 key 的计数器的当前值。
func (c *SafeCounter) Value(key string) int {
c.mux.Lock()
// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
defer c.mux.Unlock()
return c.v[key]
}
func main() {
c := SafeCounter{v: make(map[string]int)}
for i := 0; i < 1000; i++ {
go c.Inc("somekey")
}
time.Sleep(time.Second)
fmt.Println(c.Value("somekey"))
}
复制代码
3.4.1 等待并发结束
进程退出的时候不会等待并发任务结束,如果如果此时并发任务未完成就推出进程会导致很多错误。有两种方式,通道堵塞和waitgroup。
使用通道堵塞
func main(){
exit:=make(chan struct{})//创建通道,因为仅仅是通知,数据无意义
go func(){
time.Sleep(time.Second)
println("goroutine done")
close(exit)//这里是关闭通道,发出信号
}
println("main...")
<-exit//如果通道关闭,就立即解除堵塞,但是如果通道还没关闭,就堵塞,知道go程运行结束
println("main exit")
}
复制代码
sync.WaitGroup
通过设定计数器,让每个goroutine在推出前递减,直至归零解除堵塞
我们通过gotour上的例题来理解它的使用
练习:Web 爬虫
在这个练习中,我们将会使用 Go 的并发特性来并行化一个 Web 爬虫。
修改 Crawl 函数来并行地抓取 URL,并且保证不重复。
提示:你可以用一个 map 来缓存已经获取的 URL,但是要注意 map 本身并不是并发安全的!
- Done()方法用来减一计数,常常用在go程里,常常与defer合用
- Add()方法增加计数值
- Wait()堵塞进程,让他不要提前退出,等待所有并发任务完成
package main
import (
"fmt"
"sync"
)
type Fetcher interface {
// Fetch 返回 URL 的 body 内容,并且将在这个页面上找到的 URL 放到一个 slice 中。
Fetch(url string) (body string, urls []string, err error)
}
//为了避免url重复,要创建一个自己的结构体变量然后加锁
type urlMap struct {
urlRoute map[string] int
mux sync.Mutex
}
func check(um urlMap,urlName string) bool{
um.mux.Lock()
defer um.mux.Unlock()
_,ok:=um.urlRoute[urlName]
if ok==false{
um.urlRoute[urlName]=1
return true
}
return false
}
// Crawl 使用 fetcher 从某个 URL 开始递归的爬取页面,直到达到最大深度。
func Crawl(url string, depth int, fetcher Fetcher,wg *sync.WaitGroup,um urlMap) {
defer wg.Done()
// TODO: 并行的抓取 URL。
// TODO: 不重复抓取页面。
// 下面并没有实现上面两种情况:
if depth <= 0 {
return
}
body, urls, err := fetcher.Fetch(url)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("found: %s %q\n", url, body)
for _, u := range urls {
if check(um,u)==true{
wg.Add(1)
go Crawl(u, depth-1, fetcher,wg,um)
}
continue
}
return
}
func CrawlTest() {
var wg sync.WaitGroup
um:=urlMap{urlRoute: make(map[string]int)}
wg.Add(1)
go Crawl("https://golang.org/", 4, fetcher,&wg,um)
wg.Wait()
}
// fakeFetcher 是返回若干结果的 Fetcher。
type fakeFetcher map[string]*fakeResult
type fakeResult struct {
body string
urls []string
}
func (f fakeFetcher) Fetch(url string) (string, []string, error) {
if res, ok := f[url]; ok {
return res.body, res.urls, nil
}
return "", nil, fmt.Errorf("not found: %s", url)
}
// fetcher 是填充后的 fakeFetcher。
var fetcher = fakeFetcher{
"https://golang.org/": &fakeResult{
"The Go Programming Language",
[]string{
"https://golang.org/pkg/",
"https://golang.org/cmd/",
},
},
"https://golang.org/pkg/": &fakeResult{
"Packages",
[]string{
"https://golang.org/",
"https://golang.org/cmd/",
"https://golang.org/pkg/fmt/",
"https://golang.org/pkg/os/",
},
},
"https://golang.org/pkg/fmt/": &fakeResult{
"Package fmt",
[]string{
"https://golang.org/",
"https://golang.org/pkg/",
},
},
"https://golang.org/pkg/os/": &fakeResult{
"Package os",
[]string{
"https://golang.org/",
"https://golang.org/pkg/",
},
},
}
复制代码
local storage与线程不同,goroutine任务无法设置优先级,无法获取编号,没有局部存储,有时候连返回值都会丢弃,但除了优先级,其他功能都很容易实现。
我这里的局部存储就是用一个含有map的结构体实现的,为了实现局部存储要做同步处理,运行的时候会对它做并发读写检查。
3.5 runtime包里的小tip
- runtime.GOMAXPROCS(n int):运行的时候会创建很多线程,但任何时候仅有有限个线程参与并发任务并且执行,该数量默认与处理器核数相等。如果n<=1则设置线程数为内核数量不做任何修改。
- runtime.Gosched():暂停,释放线程给其他任务,当前任务被放回队列,会自动恢复执行
- runtime.Goexit():立即终止当前任务,终止整个调用堆栈,已经被压入调用栈的会执行,包过defer语句也会执行,与return只退出当前的函数不同。而标准库os.EXIT可以终止进程,但不会执行延迟调用。
该函数不会影响其他的并发任务,不会造成panic,无法捕获
package main
import "runtime"
func GoExitTest(){
exit:=make(chan struct{})
go func() {
defer close(exit)
defer println("a")
func(){
defer func() {
println("b",recover()==nil)//这里加入了一个判断,判断是否正常返回
}()
func() {
println("c")
runtime.Goexit() //本句话一下都不会被运行
println("c done.")
}()
println("b done")
}()
println("a done")
}()
//下面这句话的含义并不是将信道内容输出,而是起到一个阻塞作用,go程是会延迟执行的,如果主线程直接结束了,go程可能还没执行
//<-exit是阻塞直到有数据在信道内,或者信道被关闭才继续运行
<-exit
println("main exit")
}
//output
c
b
a
main exit
复制代码
但是在main包的main函数中,不仅终止整个调用堆栈,还会让进程直接崩溃
func main(){
for i:=0;i<2;i++{
go func(x int) {
for n:=0;n<2;n++{
fmt.Printf("%c:%d\n",x,n)
time.Sleep(time.Millisecond)
}
}(i)
}
runtime.Goexit()
println("main,exit.")
}
复制代码
也就是说主函数剩下的操作都不会进行了
3.6 panic和recover
panic的定义panic内置函数停止当前goroutine的正常执行,当函数F调用panic时,函数F的正常执行被立即停止,然后运行所有在F函数中的defer函数,然后F返回到调用他的函数对于调用者G,F函数的行为就像panic一样,终止G的执行并运行G中所defer函数,此过程会一直继续执行到goroutine所有的函数。panic可以通过内置的recover来捕获
和刚才的goexit有点相似,都是终止调用堆栈,而在源码中panic是一个接口 ,recover也是一个接口。
recover的定义recover内置函数用来管理含有panic行为的goroutine,recover运行在defer函数中,获取panic抛出的错误值,并将程序恢复成正常执行的状态。如果在defer函数之外调用recover,那么recover不会停止并且捕获panic错误如果goroutine中没有panic或者捕获的panic的值为nil,recover的返回值也是nil。由此可见,recover的返回值表示当前goroutine是否有panic行为.
panic内置函数里传入的是一个空接口值,也就是可以传入任何类型
package main
import "fmt"
func outside(){
defer fmt.Println("inside funciton done")
fmt.Println("start inside function")
inside()
fmt.Println("test the rest of the ouside")
}
func inside(){
defer func() {
fmt.Println("defer done")
}()
panic(struct {}{})
}
func main(){
outside()
fmt.Println("main done")
}
//output
start inside function
defer done
inside funciton done
panic: (struct {}) 0xa37350
goroutine 1 [running]:
main.inside()
C:/Users/I546894/Desktop/golearning/panicTest.go:15 +0x4a
main.outside()
C:/Users/I546894/Desktop/golearning/panicTest.go:8 +0xd1
main.main()
C:/Users/I546894/Desktop/golearning/main.go:81 +0x2db
exit status 2
复制代码
通过上面那个例子我们可以注意到panic调用defer已经压入调用栈的函数,但是不会运行接下来的程序,然后会返回调用者,在调用者里继续调用defer压栈的部分,依旧不执行剩余的部分,最后在最顶端的函数处报错,可以查看到panic的内容。
那么加入了recover之后会是怎么样子呢?约定俗成使用recover要在derfer的语句内,但是也不能直接调用。
package main
import (
"fmt"
)
func outside(){
defer fmt.Println("inside funciton done")
fmt.Println("start inside function")
inside()
fmt.Println("test the rest of the ouside")
}
func inside(){
defer func() {
fmt.Println("defer done")
}()
defer func() {
//recover()有更好的调用方式
if pan:=recover(); pan!=nil {
fmt.Println(pan)
}
}()
panic(struct {}{})
fmt.Println("the rest of the inside")
}
//output
start inside function
{}
defer done
test the rest of the ouside
inside funciton done
main done
复制代码
我们可以发现recover捕获到了panic之后,直接截胡了,由recover所在的goroutine来接管函数的运行,导致”the rest fo the inside”这行打印不出来,然后返回到调用的者函数继续正常运行。这是不是让你们想到了什么?对就是try…catch! 那么recover可以放在外面ouside函数,也就是调用者吗?
package main
import (
"fmt"
)
func outside(){
defer func() {
recover()//有更好的调用方式
//if pan:=recover(); pan!=nil {
// fmt.Println(pan)
//}
fmt.Println("recover goroutine")
}()
defer fmt.Println("inside funciton done")
fmt.Println("start inside function")
inside()
fmt.Println("test the rest of the ouside")
}
func inside(){
defer func() {
fmt.Println("defer done")
}()
//defer func() {
// recover()//有更好的调用方式
// //if pan:=recover(); pan!=nil {
// // fmt.Println(pan)
// //}
// fmt.Println("recover goroutine")
//}()
panic(struct {}{})
fmt.Println("the rest of the inside")
}
//output
start inside function
defer done
inside funciton done
recover goroutine
main done
复制代码
答案是肯定的!