【Go by Example】笔记整合

之前分了几篇记录的笔记, 自己找起来不方便, 就整合到一个里面方便随时查

1. Hello World

go run 直接运行
go build编译成二进制文件直接执行

2. 值

字符串可以通过 + 连接

3. 变量

定义一个新变量, 可以像python那样一次定义多个, 不赋值会有默认值, int是0, string是""空字符串

//这三种等效
var a int = 1
var b = 1
c := 1
var d int

4. 常量

const 语句可以出现在任何 var 语句可以出现的地方
可以执行任意精度的运算
常量没确定类型,在需要数字的地方都可以用,会自动转换成float64之类的类型

5. For 循环

格式是

for initialization; condition; post {
    // zero or more statements
}
// a traditional "while" loop
for condition {
    // ...
}
// a traditional infinite loop
for {
    // ...
}

常见的demo如下, 打印奇数:

for n := 0; n <= 5; n++ {
     if n%2 == 0 {
         continue
     }
     fmt.Println(n)
 }

6. If/Else 分支

demo如下

if num := 9; num < 0 {
    fmt.Println(num, "is negative")
} else if num < 10 {
    fmt.Println(num, "has 1 digit")
} else {
    fmt.Println(num, "has multiple digits")
}

相比java,go的if条件不需要括号(也可以加),而且在判断条件之前可以加一句执行语句,用;隔开就行,感觉没啥用。
Go 没有三目运算符, 即使是基本的条件判断,依然需要使用完整的 if 语句。

7. Switch 分支结构

基本的switch和java一样,
在switch-case的case里, 可以用逗号来取多个条件, 比如

switch time.Now().Weekday() {
    case time.Saturday, time.Sunday:
        fmt.Println("It's the weekend")
    default:
        fmt.Println("It's a weekday")
    }

还可以switch后面不跟任何东西, 这样就类似于一个if-esle了;
还有一个叫type switch的东西,可以用来获取到类型:

    whatAmI := func(i interface{}) {
        switch t := i.(type) {
        case bool:
            fmt.Println("I'm a bool")
        case int:
            fmt.Println("I'm an int")
        default:
            fmt.Printf("Don't know type %T\n", t)
        }
    }

8.数组

定义一个数组用

var a [5]int
var twoD [2][3]int

具体访问的时候和C语言一样的,比如twoD[1][1]
默认初始化都是0

9. 切片

Slice切片很类似于数组,用make来定义

s := make([]string, 3)
s[0] = "a"
s[1] = "b"
s[2] = "c"

有一个append方法是用来向后面插元素的,返回值是新slice,这里如果slice的容量足够,会直接返回当前slice,如果容量不够会申请一个新的slice

t := append(s, "d")
fmt.Println(t)
fmt.Println(s)
// [a b c d]
// [a b c]

可以用copy(c, s)来复制;
可以用s[2:5]来切片,这里的left和right都是可以省略的,比如s[:5]
可以定义二维slice, 这么玩儿的twoD := make([][]int, 3)

10. Map

创建一个map可以这么做:

m := make(map[string]int)

插入和删除操作如下:

m["k2"] = 2
delete(m, "k2")

这里的delete是没有返回值的
要取出来一个值是这样做:

x1, x2 := m["k2"]

x1是取出来的值,x2表示map 中是否存在这个键;
如果map中没有这个key,就会返回0或者默认值;

直接初始化map可以这么做:

n := map[string]int{"foo": 1, "bar": 2}

这里要注意一个问题,不能向下面这样初始化一个map

var a map[string]string
a["b"] = "c"//这样会报错的,要先初始化内存
a = make(map[string]string)
a["b"] = "c"//这样才不会错

所以一般应该是用make来初始化map比较好;

11. Range 遍历

感觉是一个增强版的python的range,可以用来遍历一些数据结构,比如:

// 遍历数组
nums := []int{2, 3, 4}
for index, value:= range nums {
}
// 遍历map
kvs := map[string]string{"a": "apple", "b": "banana"}
for k, v := range kvs {
	fmt.Printf("%s -> %s\n", k, v)
}
// 返回键的range
for k := range kvs {
    fmt.Println("key:", k)
}
// 返回 位置和字符,这里的字符是ascii码
// output:
// 0 103
// 1 111
for i, c := range "go" {
    fmt.Println(i, c)
}

12. 函数

func add(a int, b int) int {
	return a+b
}

这里的入参部分可以省略成 a, b int

13. 多返回值

go原生支持多返回值,可以这么玩儿:

func vals() (int, int) {
	return 3, 7
}

func main() {
	_, x := vals()
	fmt.Println(x)
}

14. 变参函数

入参数量可以不固定,这么写:

func sum(nums ...int) {
	
}

这里注意,如果入参是一个切片,需要这么写func(slice...)

nums := []int{1, 2, 3, 4}
sum(nums...)

15. 闭包

闭包是什么, 我的理解就是, 一个函数可以作为一个对象来看待, 这个函数内部会维护一些变量, 实例化这个函数对象之后, 调用这个函数都可以访问这个对象维护的变量, 但是如果重新实例化了一个函数对象, 那么就会重新初始化维护一些变量;

比如:

func intSeq() func() int {
    i := 0
    return func() int {
        i++
        return i
    }
}

这段代码创建了一个返回值为一个func的函数, 同时, 这个intSeq函数内部维护了一个变量i, 这就是闭包的一个体现, 在调用的时候如下:

func main() {
    nextInt := intSeq()

    fmt.Println(nextInt())
    //1
    fmt.Println(nextInt())
    //2
    fmt.Println(nextInt())
    //3
    
    newInts := intSeq()
    fmt.Println(newInts())
    //1
}

因为nextInt这个函数对象中维护的i已经在三次调用中变成了3, 但是newInts中的i还是刚实例化的, 所以还是1;

16. 递归

就正常递归呗, 说啥呢, 贴个demo吧;

func fact(n int) int {
    if n == 0 {
        return 1
    }
    return n * fact(n-1)
}

func main() {
    fmt.Println(fact(7))
}

17. 指针

感觉和C的指针差不多, 有值传递和引用传递, 传指针的时候用&来传地址, 入参用*表示接一个地址, 但是写法有些区别, *写在类型前面, 比如*int, demo如下:

// 值传递
func zeroval(ival int) {
    ival = 0
}

//引用传递
func zeroptr(iptr *int) {
    *iptr = 0
}

func main() {
    i := 1
    fmt.Println("initial:", i)
    //1

    zeroval(i)
    fmt.Println("zeroval:", i)
	//1

    zeroptr(&i)
    fmt.Println("zeroptr:", i)
	// 0
}

18. 结构体

定义一个struct的语法:

type person struct {
    name string
    age  int
}

struct是可变 (immutable) 的

同时, struct和C一样可以通过.来访问其中的元素, 比如p.name
struct的指针也可以直接用.来访问其中的元素, 这里会自动解引用:

s := person{name: "Sean", age: 50}
sp := &s
fmt.Println(sp.name)

19. 方法

可以为struct来创建方法;

type rect struct {
    width, height int
}

func (r *rect) area() int {
    return r.width * r.height
}
func (r rect) perim() int {
    return 2*r.width + 2*r.height
}

func main() {
    r := rect{width: 10, height: 5}

    fmt.Println("area: ", r.area())
    fmt.Println("perim:", r.perim())

    rp := &r
    fmt.Println("area: ", rp.area())
    fmt.Println("perim:", rp.perim())
}

这里接收参数是指针的时候也可以直接通过.来访问;
可以像java里调用类的方法一样来使用结构体的方法;

这里注意一个点, 可以发现, 上面在定义area方法的时候指定struct用的是指针传递, 或者用go喜欢的方式来说,
就是area这个方法的接收器receiver是一个指针类型的接收器, 而perim方法的接收器是一个值类型的接收器;
二者的区别在于, 用指针类型接收器可以改变调用方法的那个对象的值, 可以修改接受结构体的值, 而且不需要做值拷贝; 用值类型接收器的话,
就可以做到不影响原来对象的内容, 但是会在调用方法时产生一个拷贝;

20. 接口

在go里, 接口就是"方法签名的集合";

比如定义一个geometry接口:

type geometry interface {
    area() float64
    perim() float64
}

定义的这个接口, 规定这个接口有2个方法, areaperim, 返回值都是float64;

然后就可以定义一个struct, 然后为struct定义接口中的方法:

type rect struct {
	width, height float64
}
type circle struct {
	radius float64
}

func (r rect) area() float64 {
	return r.width * r.height
}
func (r rect) perim() float64 {
	return 2*r.width + 2*r.height
}

func (c circle) area() float64 {
	return math.Pi * c.radius * c.radius
}
func (c circle) perim() float64 {
	return 2 * math.Pi * c.radius
}

然后如果我再定义一个函数measure:

func measure(g geometry) {
	fmt.Println(g)
	fmt.Println(g.area())
	fmt.Println(g.perim())
}

这个函数的输入是一个geometry接口, 这也就意味着, 凡是实现了geometry接口中所有方法的struct, 都可以作为measure函数的入参, 所以可以这样调用measure方法:

func main() {
	r := rect{width: 3, height: 4}
	c := circle{radius: 5}

	measure(r)
	measure(c)
}

这里定义了一个rect和一个circle, 这两个struct都实现了areaprim方法, 即都实现了geometry接口, 所以可以正常调用measure方法;

21. 错误处理

首先, 可以通过erroes.New直接来定义一个错误, 比如:

func f1(arg int) (int, error) {
	if arg == 42 {
		return -1, errors.New("can't work with 42")
	}

	return arg + 3, nil
}

也可以自定义一个error, 但是这个error要实现built-in的error接口, error接口长这样:

// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
	Error() string
}

所以, 如果我现在定义一个自己的argError, 我就要同时为它实现Error方法:

type argError struct {
	arg  int
	prob string
}

func (e *argError) Error() string {
	return fmt.Sprintf("%d - %s", e.arg, e.prob)
}

一般这个Error可能都类似java里的Exception的getMessage, 返回一个错误提示;
然后定义好自己的error之后, 就可以在返回的时候用这个argError了:

func f2(arg int) (int, error) {
	if arg == 42 {

		return -1, &argError{arg, "can't work with it"}
	}
	return arg + 3, nil
}

然后这样去使用f2函数:

    _, e := f2(42)
    if ae, ok := e.(*argError); ok {
        fmt.Println(ae.arg)
        fmt.Println(ae.prob)
    }

这里的ae, ok := e.(*argError) 是一个断言type assertion, 就是在这里尝试把e这个对象转换成(*argError)这样一个argError的指针, 如果转换成功了, 那就把ok赋为true, ae赋为argError类型的指针; 否则ok就是false, ae就是nil;

这个 answer 写的非常好, 可以看一下~

22. 协程

因为之前在java中没太多接触过携程,大概学习了一下协程的概念;
先看下给的demo:

func f(from string) {
    for i := 0; i < 3; i++ {
        fmt.Println(from, ":", i)
    }
}

func main() {

	// 这里是直接调用f函数
    f("direct")
    // 这是用协程来运行f函数
    go f("goroutine")
	// 这里用协程来运行一个匿名函数, 参数是"going"
    go func(msg string) {
        fmt.Println(msg)
    }("going")

    time.Sleep(time.Second)
    fmt.Println("done")
}

//output:
//direct : 0
//direct : 1
//direct : 2
//goroutine : 0
//going
//goroutine : 1
//goroutine : 2
//done

可以看到, 要用协程去运行一个函数, 只需要用关键字go就行了;

协程到底是什么呢, 可能是类似于子程序, 比如现在函数A中调用了函数B和C, 那么我执行函数A的时候, 应该是按顺序来执行函数BC, 这里的BC函数应该都是放在栈里依次调用的;

但是如果函数BC是以协程的方式运行的, 就有可能在执行函数A的时候, B执行了一半, 然后去执行C, 然后再回来执行B, 就有点像CPU的中断, 那么go的协程切换是在什么时候进行的呢, 这似乎是依赖于一个调度器的机制, 具体还不太了解; 埋坑~

到此为止的感受是go的协程还蛮方便的…具体再说吧

23. 通道

通道(channels) 是连接多个协程的管道。 你可以从一个协程将值发送到通道,然后在另一个协程中接收。
demo如下:

func main() {

    messages := make(chan string)

    go func() { messages <- "ping" }()

    msg := <-messages
    fmt.Println(msg)
}

感觉有点像一个消息队列, 好像还有缓冲管道之类的;
默认发送和接收操作是阻塞的,直到发送方和接收方都就绪;
channel可以用close()来关闭, 如果管道里没消息了, 还尝试取消息会导致死锁;

24. 通道缓冲

默认情况下,通道是 无缓冲

messages := make(chan string, 2)

这样创建的有缓冲通道 允许在没有对应接收者的情况下,缓存一定数量的值。
由于此通道是有缓冲的, 因此我们可以将这些值发送到通道中,而无需并发的接收。

25. 通道同步

就是一个用通道来做类似于FutureTask的事情

func worker(done chan bool) {
    fmt.Print("working...")
    time.Sleep(time.Second)
    fmt.Println("done")

    done <- true
}

func main() {

    done := make(chan bool, 1)
    go worker(done)

    <-done
}

26. 通道方向

通道作为一个函数的参数的时候可以指定通道的方向, 是只读的或者只写的:

// ping是一个只写通道
func ping(pings chan<- string, msg string) {
    pings <- msg
}

// pings是一个只读信道, pongs是一个只写信道
func pong(pings <-chan string, pongs chan<- string) {
    msg := <-pings
    pongs <- msg
}

尝试往只写通道里读数据会是一个编译时错误。

27. 通道选择器

Go 的 选择器(select) 让你可以同时等待多个通道操作。 将协程、通道和选择器结合,是 Go 的一个强大特性。

func main() {
	// 定义两个channel
    c1 := make(chan string)
    c2 := make(chan string)

    go func() {
        time.Sleep(1 * time.Second)
        c1 <- "one"
    }()
    go func() {
        time.Sleep(2 * time.Second)
        c2 <- "two"
    }()

    for i := 0; i < 2; i++ {
    // 这里select用来同时等待接收c1和c2通道的消息
        select {
        case msg1 := <-c1:
            fmt.Println("received", msg1)
        case msg2 := <-c2:
            fmt.Println("received", msg2)
        }
    }
}

上面程序中的两个func是协程并发的, 所以程序总共耗时大概2s+;

28. 超时处理

select可以用来同时等待多个通道的结果, 也可以设置超时处理, 用<-time.After(1 * time.Second)表示超时1s之后的处理;

    c1 := make(chan string, 1)
    go func() {
        time.Sleep(2 * time.Second)
        c1 <- "result 1"
    }()
    select {
    case res := <-c1:
        fmt.Println(res)
    case <-time.After(1 * time.Second):
        fmt.Println("timeout 1")
    }

完成超时处理之后会自动break出去, 就不会再打印c1在2s之后发过来的内容了;

29. 非阻塞通道操作

之前说无缓冲的通道的收发都是阻塞的, 就是会收的一方会一直阻塞等到发的一方完成为止;

那么如何做无阻塞的收发呢, 就是当收/发的那一时刻, 无法收/发就直接跳过就可以了:

func main() {
    messages := make(chan string)
    signals := make(chan bool)

	// 无阻塞收
    select {
    case msg := <-messages:
        fmt.Println("received message", msg)
    default:
        fmt.Println("no message received")
    }

	// 无阻塞发
    msg := "hi"
    select {
    case messages <- msg:
        fmt.Println("sent message", msg)
    default:
        fmt.Println("no message sent")
    }
    
	// 无阻塞多路选择器
    select {
    case msg := <-messages:
        fmt.Println("received message", msg)
    case sig := <-signals:
        fmt.Println("received signal", sig)
    default:
        fmt.Println("no activity")
    }
}

30. 通道的关闭

关闭 一个通道意味着不能再向这个通道发送值了。 该特性可以向通道的接收方传达工作已经完成的信息。

j, more := <-jobs

如果jobs这个通道已经被关闭了close(jobs), 那么读到的more就会是false;

31. 通道遍历

可以用range来遍历通道:

func main() {
    queue := make(chan string, 2)
    queue <- "one"
    queue <- "two"
    close(queue)

    for elem := range queue {
        fmt.Println(elem)
    }
}

这里一定要先关闭通道再遍历, 不然就会死锁
fatal error: all goroutines are asleep - deadlock!

32. Timer

timer提供了一个定时器, 是一个管道, 在定时器时间到了之后会收到一个消息, 然后就得知是时间到了;
demo如下:

timer1 := time.NewTimer(2 * time.Second)
<-timer1.C
fmt.Println("Timer 1 fired")

这样能看到2s之后timer1被触发了;

它和直接sleep的最大区别在于, timer可以被中途停止;

timer2 := time.NewTimer(time.Second)
go func() {
    <-timer2.C
    fmt.Println("Timer 2 fired")
}()
stop2 := timer2.Stop()
if stop2 {
    fmt.Println("Timer 2 stopped")
}

time.Sleep(2 * time.Second)

上面这段代码, 1s之后触发timer2的任务被放到协程中去执行了, 但是实际上主线程顺序执行的时候就会把timer2给stop掉, 所以实际上即使再等待2s依然看不到timer2被触发(因为它已经被关闭了);

这里我在想, 协程里的任务会不会一直在等timer2.c, 导致占用资源呢

33. Ticker

打点器, 用来做固定间隔做某个任务用, demo:

func main() {
    ticker := time.NewTicker(500 * time.Millisecond)
    done := make(chan bool)

    go func() {
        for {
            select {
            case <-done:
                return
            case t := <-ticker.C:
                fmt.Println("Tick at", t)
            }
        }
    }()

    time.Sleep(1600 * time.Millisecond)
    ticker.Stop()
    done <- true
    fmt.Println("Ticker stopped")
}

主要思路就是, 起一个ticker, 应该会维护一个通道, 每500ms向通道里发一个消息;
然后起一个协程, 在协程里面死循环用select去接收ticker通道里的消息, 如果接收到了消息就打出来;

然后主线程里面等1600ms之后, 把ticker停掉, 然后给done发一个消息去把协程里的死循环停掉;

这里好像不要done也行, 如果select去看ticker.C里面没有消息就会报Ticker stopped然后停掉, 但是这样做, 协程里应该会持续运行, 即使ticker被stop了也会继续跑;
就是这么玩儿:

ticker := time.NewTicker(500 * time.Millisecond)

go func() {
	for {
		fmt.Println("Tick at", <-ticker.C)
	}
}()

time.Sleep(1600 * time.Millisecond)
ticker.Stop()
fmt.Println("Ticker stopped")

34. 工作池

这个工作池感觉类似于线程池, 可以先创建几个worker, 就类似于先往工作池里放几个worker进去, 在初始jobs的channel是空的时候应该会阻塞直到有任务进来(但是这里我还不太懂, 到底会阻塞到什么时候);

func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        fmt.Println("worker", id, "started  job", j)
        time.Sleep(time.Second)
        fmt.Println("worker", id, "finished job", j)
        results <- j * 2
    }
}

然后创建两个channel, jobs和results, 这两个channel分别用来放任务和执行结果;
然后在for循环里放创建三个worker;

    const numJobs = 5
    jobs := make(chan int, numJobs)
    results := make(chan int, numJobs)

    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

然后先往jobs通道里放几个任务进去, 然后关闭通道表示任务放完了(不知道这一步的意义何在, 是不是只是语义上的意义); 然后再从results里面取结果出来(一定要取5次);

    for j := 1; j <= numJobs; j++ {
        jobs <- j
    }
    close(jobs)

    for a := 1; a <= numJobs; a++ {
        <-results
    }

我的疑问是, jobs在for循环中放进去之后, 是什么时候开始被worker执行的
我猜测可能是像线程池一样一放进去就由worker来争抢执行
但如果是这样, 问题在于worker的for j := range jobs循环中, 我的理解是jobs就类似于一个队列, 如果当前队列中没有元素, 遍历结束了就应该结束for循环了, 但事实似乎并非如此, 这个for循环就一直阻塞在这里等待jobs持续添加内容, 并且可能是在争抢读取jobs的内容;
这里我做了个实验, 发现确实对于有缓冲通道, 用for range去遍历的时候确实是会一直阻塞争抢读channel的, 对于无缓冲通道, 写完之后必须要close channel, 否则会死锁(为什么会死锁呢?)

35. WaitGroup

先看这里的worker, 传一个waitgroup进来;
这里要传指针进来, 因为如果不传指针会使用值引用, 会copy一份同样的waitgroup过来, 再在worker内部执行Done方法的时候, 原来的waitgroup不会收到这个信号;
然后这里面的defer的作用是, 可以视作在return之前执行wg.Done(), 就是在return之前调用;

func worker(id int, wg *sync.WaitGroup) {

    defer wg.Done()

    fmt.Printf("Worker %d starting\n", id)

    time.Sleep(time.Second)
    fmt.Printf("Worker %d done\n", id)
}

然后在主函数调用的时候这样做, 先定义一个waitgroup, 然后在协程里运行worker;
最后通过wg.Wait()来等待所有worker运行任务结束;

func main() {

    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go worker(i, &wg)
    }

    wg.Wait()
}

36. 速率限制

这里提供了两个做速率限制的demo;
先用一个channel来模拟request, 放了5个request进去;

requests := make(chan int, 5)
for i := 1; i <= 5; i++ {
	requests <- i
}
close(requests)

建一个ticker, 每200ms有一条消息, 然后遍历这个limiter阻塞等消息, 每次消息来就处理;

limiter := time.Tick(200 * time.Millisecond)

for req := range requests {
	<-limiter
	fmt.Println("request", req, time.Now())
}

然后还可以做一个允许3次爆发请求的limiter, 思路也差不多, demo如下:

// 先建一个limiter
	burstyLimiter := make(chan time.Time, 3)

// 先放三条进去, 允许三次爆发请求
	for i := 0; i < 3; i++ {
		burstyLimiter <- time.Now()
	}
// 起一个ticker, 每200毫秒发一次消息
	go func() {
		for t := range time.Tick(200 * time.Millisecond) {
			burstyLimiter <- t
		}
	}()

	burstyRequests := make(chan int, 5)
	for i := 1; i <= 5; i++ {
		burstyRequests <- i
	}
	close(burstyRequests)

// 前3次会消耗掉burstyLimiter里面预留的3次, 然后去处理ticker发过来的
	for req := range burstyRequests {
		<-burstyLimiter	
		fmt.Println("request", req, time.Now())
	}

37. 原子计数器

这里展示了一下go里的协程竞争问题;

func main() {
// 建一个uint64的变量ops当计数器
    var ops uint64
// 起一个waitgroup用来等待多个协程完成递增操作
    var wg sync.WaitGroup

    for i := 0; i < 50; i++ {
        wg.Add(1)
// 起50个协程, 在每个协程里面做1000次自增1的操作
        go func() {
            for c := 0; c < 1000; c++ {
                atomic.AddUint64(&ops, 1)
            }
            wg.Done()
        }()
    }
// 等待50个协程全部完成
    wg.Wait()
// 输出ops的值, 发现是50000
    fmt.Println("ops:", ops)
}

这里如果不用atomic.AddUint64(&ops, 1), 直接用ops+=1的话就会出现竞争问题, 多个协程发生冲突, 最终ops的值就不会是50000了;

这里正好点进去看了下代码, 发现atomic.AddUint64(&ops, 1) 这个函数也可以用来做减法操作, 但是这个函数限制入参是两个uint, 如下

// AddUint64 atomically adds delta to *addr and returns the new value.
// To subtract a signed positive constant value c from x, do AddUint64(&x, ^uint64(c-1)).
// In particular, to decrement x, do AddUint64(&x, ^uint64(0)).
func AddUint64(addr *uint64, delta uint64) (new uint64)

所以, 第二个delta不能直接传一个负数进去, 要按照注释里面说的, 比如要减c, 那就要把delta传^uint64(c-1) , 这里是利用到了补码的概念, 一个负数的补码是正数取反再+1, 所以:

负数 = ^正数 +1
-c = ^c + 1 = ^(c-1)

所以, 要用atmoic的自减1就可以这么写:

atomic.AddUint64(&ops, ^(uint64(1))+1)
// or
atomic.AddUint64(&ops, ^(uint64(0)))

38. 互斥锁

互斥锁sync.Mutex, 就类似于java里的reentrantlock, 区别在于java的reentrantlock是可重入的, 但是go的sync.Mutex是不可重入的, 就比如在一个线程里, 已经获得了一次锁L, 然后又调用了一个需要占用锁L的方法, 那么java中是可以再次获得锁的, 还能知道获得了几次锁; 但是go中就不可以;

这里查了一下, go中如何实现可重入锁, 但是似乎没有, 因为 go的设计者认为,如果需要重入锁,就说明你的代码写的有问题

demo的代码有点长, 贴在下面, 很好理解, 就不解释了;

package main

import (
    "fmt"
    "math/rand"
    "sync"
    "sync/atomic"
    "time"
)

func main() {

    var state = make(map[int]int)

    var mutex = &sync.Mutex{}

    var readOps uint64
    var writeOps uint64

    for r := 0; r < 100; r++ {
        go func() {
            total := 0
            for {

                key := rand.Intn(5)
                mutex.Lock()
                total += state[key]
                mutex.Unlock()
                atomic.AddUint64(&readOps, 1)

                time.Sleep(time.Millisecond)
            }
        }()
    }

    for w := 0; w < 10; w++ {
        go func() {
            for {
                key := rand.Intn(5)
                val := rand.Intn(100)
                mutex.Lock()
                state[key] = val
                mutex.Unlock()
                atomic.AddUint64(&writeOps, 1)
                time.Sleep(time.Millisecond)
            }
        }()
    }

    time.Sleep(time.Second)

    readOpsFinal := atomic.LoadUint64(&readOps)
    fmt.Println("readOps:", readOpsFinal)
    writeOpsFinal := atomic.LoadUint64(&writeOps)
    fmt.Println("writeOps:", writeOpsFinal)

    mutex.Lock()
    fmt.Println("state:", state)
    mutex.Unlock()
}

最后我机子上的运行结果是:

readOps: 99031
writeOps: 9903
state: map[0:1 1:5 2:79 3:62 4:36]

因为read起了100个协程, write只起了10个协程, 所以差不多readOps是writeOps的十倍左右;
(似乎demo里的total是没用的)

39. 状态协程 Stateful Goroutines

上面38里避免线程同步问题的方法是用锁, go还提供了一个更有意思的方法是使用go的channel选择器来做;

对于一个对象, 如果要限制同一时间只有一个线程能访问, 那么可以使用锁, 其实也可以把这个对象交给一个协程管理, 然后留两个channel, 一个读一个写, 然后在这个协程内部使用select处理来自reads管道和writes管道的请求, 这样就可以保证线程安全;
demo如下:

先定义读和写两种操作

type readOp struct {
	key  int
	resp chan int
}
type writeOp struct {
	key  int
	val  int
	resp chan bool
}

定义两个readOps和writeOps用来计数分别做了多少次读写操作;
起两个管道reads和writes, 用来发read和write请求;

	var readOps uint64
	var writeOps uint64

	reads := make(chan readOp)
	writes := make(chan writeOp)

起一个协程, 里面的state就是需要上锁的对象, 需要保障线程安全;
在协程中用死循环, 用select去读reads和writes管道发来的请求, 然后做对应处理;

这里我当时有一个担心, select如果同时收到来自reads和writes的请求, 是不是会按顺序先处理read后处理write呢, 这样就会让read更容易被处理, 肯定有问题;
但是实际上这样的担心是多余的, 因为go在运行的时候会自动打乱select内部的顺序, 所以并不会先处理read, 也不会先处理write, 都是随机的

	go func() {
		var state = make(map[int]int)
		for {
			select {
			case read := <-reads:
				read.resp <- state[read.key]
			case write := <-writes:
				state[write.key] = write.val
				write.resp <- true
			}
		}
	}()

然后分别测试读和写:
读的时候要建一个readOp对象, 把这个对象发到reads这个channel, 然后通过readOp的resp这个管道来接收结果;
写同理;

	for r := 0; r < 100; r++ {
		go func() {
			for {
			
				read := readOp{
					key:  rand.Intn(5),
					resp: make(chan int)}
				reads <- read
				<-read.resp
				atomic.AddUint64(&readOps, 1)
				time.Sleep(time.Millisecond)
			}
		}()
	}
	
	for w := 0; w < 10; w++ {
		go func() {
			for {
				write := writeOp{
					key:  rand.Intn(5),
					val:  rand.Intn(100),
					resp: make(chan bool)}
				writes <- write
				<-write.resp
				atomic.AddUint64(&writeOps, 1)
				time.Sleep(time.Millisecond)
			}
		}()
	}

最后看结果:

	readOpsFinal := atomic.LoadUint64(&readOps)
	fmt.Println("readOps:", readOpsFinal)
	writeOpsFinal := atomic.LoadUint64(&writeOps)
	fmt.Println("writeOps:", writeOpsFinal)

我电脑的输出结果是

readOps: 99120
writeOps: 9918

可见, read大概是write的十倍, 也并没有出现非常夸张的更优先处理read;

40. 排序

可以对内置数据类型直接排序, 或者判断是否有序

    strs := []string{"c", "a", "b"}
    sort.Strings(strs)
    fmt.Println("Strings:", strs)

    ints := []int{7, 2, 4}
    sort.Ints(ints)
    fmt.Println("Ints:   ", ints)

    s := sort.IntsAreSorted(ints)
    fmt.Println("Sorted: ", s)
    
	// 找到[0,156)中>132的最小的数
	fmt.Println(sort.Search(156, func(i int) bool {
		return i>132
	}))

41. 使用函数自定义排序

要实现自定义排序, demo如下:

type byLength []string

func (s byLength) Len() int {
    return len(s)
}
func (s byLength) Swap(i, j int) {
    s[i], s[j] = s[j], s[i]
}
func (s byLength) Less(i, j int) bool {
    return len(s[i]) < len(s[j])
}

func main() {
    fruits := []string{"peach", "banana", "kiwi"}
    sort.Sort(byLength(fruits))
    fmt.Println(fruits)
}

作为Sort的入参, 必须要实现Interface接口, 也就是要是要实现Len, SwapLess这三个方法;
这里定义了一个byLength方法, 实现对string数组按字符串长度进行排序, Less里面定义了排序的逻辑, 然后在传入参的时候把字符串数组强转成byLength传进去;

42. Panic

感觉就类似于java的Exception

package main

import "os"

func main() {
// 这一步之后会报异常
    panic("a problem")
// 后面的代码都是 Unreachable Code
    _, err := os.Create("/tmp/file")
    if err != nil {
        panic(err)
    }
}

43. Defer

就约等于java里的finnally
一般用来关闭文件流什么的

44. 组合函数

比如给了一个集合, 要过滤出这个集合中满足某个条件的一些内容, 就需要组合函数, 这些函数都要自己写;
怀念java中的stream啊啊啊啊啊;
demo如下随便放几个吧:

func Index(vs []string, t string) int {
	for i, v := range vs {
		if v == t {
			return i
		}
	}
	return -1
}

func Include(vs []string, t string) bool {
	return Index(vs, t) >= 0
}

func Any(vs []string, f func(string) bool) bool {
	for _, v := range vs {
		if f(v) {
			return true
		}
	}
	return false
}

func All(vs []string, f func(string) bool) bool {
	for _, v := range vs {
		if !f(v) {
			return false
		}
	}
	return true
}

func Filter(vs []string, f func(string) bool) []string {
	vsf := make([]string, 0)
	for _, v := range vs {
		if f(v) {
			vsf = append(vsf, v)
		}
	}
	return vsf
}

func Map(vs []string, f func(string) string) []string {
	vsm := make([]string, len(vs))
	for i, v := range vs {
		vsm[i] = f(v)
	}
	return vsm
}

45. 字符串函数

可以用strings这个包里的一些内置函数操作string;
这里定义var p = fmt.Println可以方便地使用fmt.Println;

var p = fmt.Println
func main() {

    p("Contains:  ", s.Contains("test", "es"))
    p("Count:     ", s.Count("test", "t"))
    p("HasPrefix: ", s.HasPrefix("test", "te"))
    p("HasSuffix: ", s.HasSuffix("test", "st"))
    p("Index:     ", s.Index("test", "e"))
    p("Join:      ", s.Join([]string{"a", "b"}, "-"))
    p("Repeat:    ", s.Repeat("a", 5))
    p("Replace:   ", s.Replace("foo", "o", "0", -1))
    p("Replace:   ", s.Replace("foo", "o", "0", 1))
    p("Split:     ", s.Split("a-b-c-d-e", "-"))
    p("ToLower:   ", s.ToLower("TEST"))
    p("ToUpper:   ", s.ToUpper("test"))
    p()

    p("Len: ", len("hello"))
    p("Char:", "hello"[1])
}

46. 字符串格式化

可以用printf格式化输出字符串, 跟java, c都一样;
有几个特殊的, 比如%+v这个类似于json输出, %T输出类型, %t输出布尔值, %e 和 %E输出科学计数法, %p是指针之类的;
可以这样s := fmt.Sprintf("a %s", "string")来获取格式化之后的字符串;

47. 正则表达式

用的是regexp包;


	match, _ := regexp.MatchString("p([a-z]+)ch", "peach")
	fmt.Println(match)

	r, _ := regexp.Compile("p([a-z]+)ch")

	fmt.Println(r.MatchString("peach"))

	fmt.Println(r.FindString("peach punch"))

	fmt.Println(r.FindStringIndex("peach punch"))

48. JSON

可以直接用json包来做json相关的转换, 官方包;
普通的基本数据类型可以直接这么做:

intB, _ := json.Marshal(1)
fmt.Println(string(intB))

自定义的struct也可以用这个Marshal

type response1 struct {
    Page   int
    Fruits []string
}
res2D := &response2{
    Page:   1,
    Fruits: []string{"apple", "peach", "pear"}}
res2B, _ := json.Marshal(res2D)
fmt.Println(string(res2B))

对于json反序列化成go对象可以这么做:
比如自定义了一个对象

type response2 struct {
	Page   int      `json:"page"`
	Fruits []string `json:"fruits"`
}

这里面可以指定每个字段分别从json中的哪个字段去读;

	str := `{"page": 1, "fruits": ["apple", "peach"]}`
	res := response2{}
	json.Unmarshal([]byte(str), &res)
	fmt.Println(res)
	fmt.Println(res.Fruits[0])

对于go里的对象, 要输出到流, 可以直接用Encode方法:

enc := json.NewEncoder(os.Stdout)
d := map[string]int{"apple": 5, "lettuce": 7}
enc.Encode(d)

49. XML

xml格式的处理;
这样格式化:

out, _ := xml.MarshalIndent(coffee, " ", "  ")

这样解析xml格式:

if err := xml.Unmarshal(out, &p); err != nil {
   panic(err)
}
fmt.Println(p)

没啥意思, 没用过, 以后再说吧;

50. 时间

time.Now()获取当前时间
通过年月日时分秒毫秒和时区指定一个时间

then := time.Date(
        2009, 11, 17, 20, 34, 58, 651387237, time.UTC)

提取时间中的信息:

p(then.Year())
p(then.Month())
p(then.Day())
p(then.Hour())
p(then.Minute())
p(then.Second())
p(then.Nanosecond())
p(then.Location())
p(then.Weekday())
p(then.Before(now))
p(then.After(now))
p(then.Equal(now))

还可以算两个时间的时间间隔, 并且判断, 改变时间之类的:

diff := now.Sub(then)
p(diff)

p(diff.Hours())
p(diff.Minutes())
p(diff.Seconds())
p(diff.Nanoseconds())

p(then.Add(diff))
p(then.Add(-diff))

51. 时间戳

获取时间戳

now := time.Now()
secs := now.Unix()
nanos := now.UnixNano()
fmt.Println(now)

millis := nanos / 1000000
fmt.Println(secs)
fmt.Println(millis)
fmt.Println(nanos)

时间戳转时间:

// 1970-01-01 08:00:00 +0800 CST
fmt.Println(time.Unix(0, 0))

52. 时间的格式化和解析

go内置了一些时间的格式, 比如:

t := time.Now()
p(t.Format(time.RFC3339))
t1, e := time.Parse(
	time.RFC3339,
	"2012-11-01T22:08:41+00:00")
p(t1)

然后解析时间的话一定要用例子来表示格式:

p(t.Format("3:04PM"))
p(t.Format("Mon Jan _2 15:04:05 2006"))
p(t.Format("2006-01-02T15:04:05.999999-07:00"))
form := "3 04 PM"
t2, e := time.Parse(form, "8 41 PM")
p(t2)

fmt.Printf("%d-%02d-%02dT%02d:%02d:%02d-00:00\n",
	t.Year(), t.Month(), t.Day(),
	t.Hour(), t.Minute(), t.Second())

ansic := "Mon Jan _2 15:04:05 2006"
_, e = time.Parse(ansic, "8:41PM")
p(e)

例子的时间一定是下面这个数字, 真反人类啊, 参见知乎问题
如何评价golang的time.Format方法一定要用2006-01-02 15:04:05作参数?

// 2006年1月2日3点4分5秒 时区是7
01/02 03:04:05 2006 -0700

53. 随机数

生成随机数可以直接用rand

// [0, 100)
rand.Intn(100)
//[0, 1)
rand.Float64()

也可以自定义随机种子:

s1 := rand.NewSource(time.Now().UnixNano())
r1 := rand.New(s1)
fmt.Print(r1.Intn(100))

我觉得这排版就xx离谱, 前面用了那么多随机数了, 到第53才讲随机数-.-

然后因为这个随机数如果种子固定了后面的序列都是固定的, 是伪随机数, 要更安全应该用crypto/rand这个包

54. 数字解析

解析数字, 类似java的valueOf或者parseInt这些吧

// 64是位数
f, _ := strconv.ParseFloat("1.234", 64)
fmt.Println(f)

// 0表示自动猜测进制, 比如2就是2进制, 10就是十进制
// 64表示位数, 0是int, 0, 8, 16, 32, 64 分别对应 int, int8, int16, int32, and int64.
i, _ := strconv.ParseInt("123", 0, 64)
fmt.Println(i)
// 解析一个十进制数成int类型
// 同 ParseInt(s, 10, 0)
k, _ := strconv.Atoi("135")
fmt.Println(k)
// 报错
_, e := strconv.Atoi("wat")
fmt.Println(e)

55. URL 解析

可以用url这个包来解析url, 贴个demo吧;

func main() {

    s := "postgres://user:pass@host.com:5432/path?k=v#f"

    u, err := url.Parse(s)
    if err != nil {
        panic(err)
    }

    fmt.Println(u.Scheme)

    fmt.Println(u.User)
    fmt.Println(u.User.Username())
    p, _ := u.User.Password()
    fmt.Println(p)

    fmt.Println(u.Host)
    host, port, _ := net.SplitHostPort(u.Host)
    fmt.Println(host)
    fmt.Println(port)

    fmt.Println(u.Path)
    fmt.Println(u.Fragment)

    fmt.Println(u.RawQuery)
    m, _ := url.ParseQuery(u.RawQuery)
    fmt.Println(m)
    fmt.Println(m["k"][0])
}

56. SHA1 哈希

就是算一个sha1哈希值:

	s := "sha1 this string"
// 要算md5的话只要这里改成
// 	h := md5.New()
	h := sha1.New()

	h.Write([]byte(s))

	bs := h.Sum(nil)

	fmt.Println(s)
	fmt.Printf("%x\n", bs)

57. Base64 编码

就是base64编码和解码, 有两种, 一个是标准base64, 一个是url base64;
区别在于:

在上面的base64传统编码中会出现+, /两个会被url直接转义的符号,因此如果希望通过url传输这些编码字符串,我们需要先做传统base64编码,随后将+和/分别替换为- _两个字符,在接收端则做相反的动作解码

所以区别就是url的base64把普通的base64里的+换成-, /换成_

    data := "abc123!?$*&()'-=@~"

    sEnc := b64.StdEncoding.EncodeToString([]byte(data))
    fmt.Println(sEnc)

    sDec, _ := b64.StdEncoding.DecodeString(sEnc)
    fmt.Println(string(sDec))
    fmt.Println()

    uEnc := b64.URLEncoding.EncodeToString([]byte(data))
    fmt.Println(uEnc)
    uDec, _ := b64.URLEncoding.DecodeString(uEnc)
    fmt.Println(string(uDec))

58. 读文件

可以用ioutil直接读;
也可以用os.Open先打开文件, 然后再Read读, 可以用Seek来改变读文件cursor的位置, 还有个ReadAtLeast去读至少某个长度的内容, 也有个缓冲读取器, demo如下:

func check(e error) {
    if e != nil {
        panic(e)
    }
}

func main() {

    dat, err := ioutil.ReadFile("/tmp/dat")
    check(err)
    fmt.Print(string(dat))

    f, err := os.Open("/tmp/dat")
    check(err)

    b1 := make([]byte, 5)
    n1, err := f.Read(b1)
    check(err)
    fmt.Printf("%d bytes: %s\n", n1, string(b1[:n1]))

    o2, err := f.Seek(6, 0)
    check(err)
    b2 := make([]byte, 2)
    n2, err := f.Read(b2)
    check(err)
    fmt.Printf("%d bytes @ %d: ", n2, o2)
    fmt.Printf("%v\n", string(b2[:n2]))

    o3, err := f.Seek(6, 0)
    check(err)
    b3 := make([]byte, 2)
    n3, err := io.ReadAtLeast(f, b3, 2)
    check(err)
    fmt.Printf("%d bytes @ %d: %s\n", n3, o3, string(b3))

    _, err = f.Seek(0, 0)
    check(err)

    r4 := bufio.NewReader(f)
    b4, err := r4.Peek(5)
    check(err)
    fmt.Printf("5 bytes: %s\n", string(b4))

    f.Close()
}

这里遇到个坑啊, windows系统里的换行符是\r\n所以换行符会读2个byte;

59. 写文件

和读差不多

60. 行过滤器

就是读用户输入用的, 类似java的Scanner

scanner := bufio.NewScanner(os.Stdin)


for scanner.Scan() {

	ucl := strings.ToUpper(scanner.Text())

	fmt.Println(ucl)
}

if err := scanner.Err(); err != nil {
	fmt.Fprintln(os.Stderr, "error:", err)
	os.Exit(1)
}

要读取用户输入, 还有一个思路是fmt.Scanf, 这个应该是类似于C语言的scannf

61. 文件路径

为文件路径的编辑提供了统一的包, 比如filepath.Join用来连接路径:

p := filepath.Join("dir1", "dir2", "filename")
fmt.Println("Dir(p):", filepath.Dir(p))
fmt.Println("Base(p):", filepath.Base(p))

还有些其他的方法, 没仔细看了;

62. 目录

os包和ioutil包来管理文件, 具体不写了;
记得一般可以用defer来最后删除文件, 目录什么的;

63. 临时文件和目录

可以用f, err := ioutil.TempFile("", "sample")创建临时文件;
可以用defer来清理临时文件, 不过系统也会定时自动清理;

64. 单元测试

go提供了一个testing包用来做单元测试, 直接有命令行工具go test

首先, 比如有一个函数, 放在文件名为intfunc.go这个文件里, 那么测试文件一定要命名为以_test结尾, 比如是intfunc_test.go
下面这就是一个用单元测试的例子;

func TestIntMinBasic(t *testing.T) {
	ans := IntMin(2, -2)
	if ans != -2 {

		t.Errorf("IntMin(2, -2) = %d; want -2", ans)
	}
}

然后也可以用 表驱动 风格编写单元测试

func TestIntMinTableDriven(t *testing.T) {
	var tests = []struct {
		a, b int
		want int
	}{
		{0, 1, 0},
		{1, 0, 0},
		{2, -2, -2},
		{0, -1, -1},
		{-1, 0, -1},
	}

	for _, tt := range tests {

		testname := fmt.Sprintf("%d,%d", tt.a, tt.b)
		t.Run(testname, func(t *testing.T) {
			ans := IntMin(tt.a, tt.b)
			if ans != tt.want {
				t.Errorf("got %d, want %d", ans, tt.want)
			}
		})
	}
}

然后在目录里go test -v表示以verbose模式运行测试

65. 命令行参数

获取命令行参数的办法:

argsWithProg := os.Args
argsWithoutProg := os.Args[1:]

arg := os.Args[3]

fmt.Println(argsWithProg)
fmt.Println(argsWithoutProg)
fmt.Println(arg)
输出:
$ go build command-line-arguments.go
$ ./command-line-arguments a b c d
[./command-line-arguments a b c d]
[a b c d]
c

66. 命令行标志

就是用来接收命令行中的参数的标志:

package main

import (
    "flag"
    "fmt"
)

func main() {

    wordPtr := flag.String("word", "foo", "a string")

    numbPtr := flag.Int("numb", 42, "an int")
    boolPtr := flag.Bool("fork", false, "a bool")

    var svar string
    flag.StringVar(&svar, "svar", "bar", "a string var")

    flag.Parse()

    fmt.Println("word:", *wordPtr)
    fmt.Println("numb:", *numbPtr)
    fmt.Println("fork:", *boolPtr)
    fmt.Println("svar:", svar)
    fmt.Println("tail:", flag.Args())
}

用的是flag这个包;
上面这个代码, 能接收4个参数:
word, 默认值是foo, 描述是a string;
num, 默认值是42, 描述是an int;
fork, 默认值是false, 描述是a bool;
svar, 默认值是bar, 描述是a string var, 接收到的这个参数会被存到svar这个字符串地址里

调用flag.parse()来解析参数, 同时还能用一个flag.Args()来接收其他未预定的参数;

这时候这个程序就可以用-h来看具体有哪些可以接收的参数, 然后这些参数的description就是代码里写的描述部分;

$ ./command-line-flags -h
Usage of ./command-line-flags:
  -fork=false: a bool
  -numb=42: an int
  -svar="bar": a string var
  -word="foo": a string
$ ./command-line-flags -word=opt
word: opt
numb: 42
fork: false
svar: bar
tail: []

67. 命令行子命令

gogit 这种命令行工具,都有很多的 子命令 。 并且每个工具都有一套自己的 flag,比如: go buildgo get 是 go 里面的两个不同的子命令。 flag 包让我们可以轻松的为工具定义简单的子命令。
下面这段代码定义了两个子命令, 分别是foobar;
foo接收两个参数, 分别是:
布尔值enable, 默认值false, 描述是enable
字符串name, 默认值是空字符串, 描述是name

bar接收一个int参数level, 默认值是0, 描述是level

解析命令的时候通过os.Args去判断有没有用子命令, 然后取os.Args[1]来判断用的是哪个子命令;

    fooCmd := flag.NewFlagSet("foo", flag.ExitOnError)
    fooEnable := fooCmd.Bool("enable", false, "enable")
    fooName := fooCmd.String("name", "", "name")

    barCmd := flag.NewFlagSet("bar", flag.ExitOnError)
    barLevel := barCmd.Int("level", 0, "level")

    if len(os.Args) < 2 {
        fmt.Println("expected 'foo' or 'bar' subcommands")
        os.Exit(1)
    }

    switch os.Args[1] {

    case "foo":
        fooCmd.Parse(os.Args[2:])
        fmt.Println("subcommand 'foo'")
        fmt.Println("  enable:", *fooEnable)
        fmt.Println("  name:", *fooName)
        fmt.Println("  tail:", fooCmd.Args())
    case "bar":
        barCmd.Parse(os.Args[2:])
        fmt.Println("subcommand 'bar'")
        fmt.Println("  level:", *barLevel)
        fmt.Println("  tail:", barCmd.Args())
    default:
        fmt.Println("expected 'foo' or 'bar' subcommands")
        os.Exit(1)
    }
$ ./command-line-subcommands foo -enable -name=joe a1 a2
subcommand 'foo'
  enable: true
  name: joe
  tail: [a1 a2]

$ ./command-line-subcommands bar -level 8 a1
subcommand 'bar'
  level: 8
  tail: [a1]

$ ./command-line-subcommands bar -enable a1
flag provided but not defined: -enable
Usage of bar:
  -level int
        level

68. 环境变量

读写环境变量, 和遍历系统所有环境变量
但是似乎这里遍历得到的和我直接在系统的环境变量里看到的不太一样, 不知道为啥, 没在意了

os.Setenv("FOO", "1")
fmt.Println("FOO:", os.Getenv("FOO"))
fmt.Println("BAR:", os.Getenv("BAR"))

fmt.Println()
for _, e := range os.Environ() {
    pair := strings.SplitN(e, "=", 2)
    fmt.Println(pair[0])
}

69. HTTP 客户端

就是发get请求:

	resp, err := http.Get("http://gobyexample.com")
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	fmt.Println("Response status:", resp.Status)

	scanner := bufio.NewScanner(resp.Body)
	for i := 0; scanner.Scan() && i < 5; i++ {
		fmt.Println(scanner.Text())
	}

	if err := scanner.Err(); err != nil {
		panic(err)
	}

70. HTTP 服务端

就是监听端口处理请求;
可以拿到RequestResponseWriter

package main

import (
	"fmt"
	"net/http"
)

func hello(w http.ResponseWriter, req *http.Request) {

	fmt.Fprintf(w, "hello\n")
}

func headers(w http.ResponseWriter, req *http.Request) {

	for name, headers := range req.Header {
		for _, h := range headers {
			fmt.Fprintf(w, "%v: %v\n", name, h)
		}
	}
}

func main() {

	http.HandleFunc("/hello", hello)
	http.HandleFunc("/headers", headers)

	http.ListenAndServe(":8090", nil)
}

结果:

C:\...\go\src\awesomeProject>curl localhost:8090/hello
hello

C:\...\go\src\awesomeProject>curl localhost:8090/headers
User-Agent: curl/7.55.1
Accept: */*

71. Context

应该就是可以获得context;
这里获得context之后用了一个选择器, case <-time.After(10 * time.Second)是模拟在进行一个十秒钟的任务, 如果任务执行期间, context结束了,那么ctx的管道里就会接到一个ctx.Done, 就取消任务;

package main

import (
    "fmt"
    "net/http"
    "time"
)

func hello(w http.ResponseWriter, req *http.Request) {

    ctx := req.Context()
    fmt.Println("server: hello handler started")
    defer fmt.Println("server: hello handler ended")

    select {
    case <-time.After(10 * time.Second):
        fmt.Fprintf(w, "hello\n")
    case <-ctx.Done():

        err := ctx.Err()
        fmt.Println("server:", err)
        internalError := http.StatusInternalServerError
        http.Error(w, err.Error(), internalError)
    }
}

func main() {

    http.HandleFunc("/hello", hello)
    http.ListenAndServe(":8090", nil)
}

72. 生成进程

这章没看懂, 似乎是可以用go来执行一些系统的命令或者程序, 但是我在的win环境好像跑不了demo, 就先没看了, 回头换unix下再, 顺便更加坚定了脱离windows换linux的决心!

73. 执行进程

这部分是我们 用其它的进程,来完全替代当前的 Go 进程
试不了, 就贴个demo吧;

binary, lookErr := exec.LookPath("ls")
if lookErr != nil {
    panic(lookErr)
}

args := []string{"ls", "-a", "-l", "-h"}

env := os.Environ()

execErr := syscall.Exec(binary, args, env)
if execErr != nil {
    panic(execErr)
}

74. 信号

go程序可以接收系统发来的信号, 比如用ctrl+c打断正在运行的进程(我好像只知道这个);

	sigs := make(chan os.Signal, 1)
	done := make(chan bool, 1)

	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)

	go func() {
		sig := <-sigs
		fmt.Println()
		fmt.Println(sig)
		done <- true
	}()

	fmt.Println("awaiting signal")
	<-done
	fmt.Println("exiting")

思路就是起一个sigschannel来接收系统信号, 然后用Notify去注册这个channel, 再起一个协程, 在协程里阻塞等待信号, 来了之后做处理;

我tm手贱把done<-true删掉了, 这样应该即使我ctrl+c也不会结束进程, 会一直阻塞在<-done那句代码, 试了一下果然如此…然后在任务管理器里找到go.exe结束进程就好了…

75. 退出

os.Exit()来退出程序, 用这个退出的时候defer不会执行:

    defer fmt.Println("!")
    os.Exit(3)

这里的defer就不会执行了;

如果你使用 go run 来运行 exit.go,那么退出状态将会被 go 捕获并打印。

$ go build exit.go
$ ./exit
$ echo $?
3

终于看完啦, 完结撒花~~~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值