字节青训营--golang阶段笔记

133 阅读8分钟

始终坚信帖子的第一读者是自己,所以这篇文章主要讲述在学习过程中不懂的和应该记住的知识,可能有些地方会比较晦涩,各位大佬千万不要专牛角尖,有什么问题可以评论区评论或者私信我,如果恰好我也懂,我都会回复的,不懂得话,咱就一起学习

文章是按照字节跳动青训营的课程写的,因为其学习资料不能公开,所以有些地方各位各显神通看看能不能找到

golang 语言相关

go语法及内存

这里记录一下for range循环时的指针问题

package main

import "fmt"

func main() {
	var a []int = []int{1, 2}
	t := make([]int, 2)
	for i, v := range a {
		t[i] = v
	}
	for _, v := range t {
		fmt.Println(v)
	}
}
/*
输出
1
2

*/

这段代码并没有任何问题,因为t为值传递

但在下面这段代码中

package main

import "fmt"

func main() {
	var a []int = []int{1, 2}
	t := make([]*int, 2)
	for i, v := range a {
		t[i] = &v
	}
	for _, v := range t {
		fmt.Println(*v)
	}
}
/*
输出
2
2

*/

究其原因,是因为v是一个固定地址的变量,在循环过程中是通过将数组a的值传递给v,故v的地址是一成不变的,在循环过程中其值是不断变化的

package main

import "fmt"

func main() {
	var a []int = []int{1, 2}
	t := make([]*int, 2)
	for i, v := range a {
		t[i] = &v
		fmt.Println(&v, "  ", &a[i])
	}
	for _, v := range t {
		fmt.Println(*v)
	}
}
/*
输出
0xc0000a6058    0xc0000a6070
0xc0000a6058    0xc0000a6078
2                           
2  

*/

内存管理

接下来看一看go的内存管理

推荐文章

因为看上面的文章都很详细,就不多说

简单的说,就是先进行逃逸分析,把没有被作用域外引用的小内存分配到栈,否则分配到堆

了解了一些内存分配问题,可以编写出高效,高质量的代码

goroutine 并发编程

下面将对并发编程进行一些相关记录

goroutine倾向于使用通信来实现共享内存....所以并发时一般采用channel来进行通信

这里推荐一篇文章 go并发编程之channel - 知乎 (zhihu.com)

go语言的channel保证同一个时间只有一个goroutine能够访问里面的数据,从设计上天然地避免线程竞争和数据冲突的问题,为开发者提供了一种优雅简单的工具;

首先在个人看来,要注意的是避免死锁,即在生产者生产完相关数据时,应记得close(channel),在channel关闭后,如果有从channel中读出数据,那么即读出的值为该channel类型得零值

还有一点就是,带缓冲channel一般是用在生产消费速度不平等情况下,达到提升性能的目的

goroutine并发控制

下面将讲述3个方法,用以实现并发控制

sync.Mutex

锁,一般推荐是用来锁住一段逻辑,而不是锁一个变量

在并发控制时,可能会出现以下情况

package main

import "fmt"

func main() {
	var x int = 0
	for j := 1; j <= 3; j++ {
		go func() {
			for i := 1; i <= 100000; i++ {
				x++
			}
		}()
	}
    time.Sleep(time.Second)
	fmt.Println(x)
}

// 输出21021

实际上,这个错误的原因即写覆盖,当两个协程同时读了x,然后在写过程中,就会出现有一个写的过程被覆盖了

所以,加锁,即不会造成这个错误

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	var x int = 0
	var lock sync.Mutex
	for j := 1; j <= 3; j++ {
		go func() {
			for i := 1; i <= 100000; i++ {
				lock.Lock()
				x++
				lock.Unlock()
			}
		}()
	}
	time.Sleep(time.Second)
	fmt.Println(x)
}

当然,这里演示锁的是变量,但实际上更应该用于锁一段逻辑,锁一个变量的话效率太低,可以使用atomic,从硬件层面实现原子性

WaitGroup

推荐文章 Go 并发任务编排利器之 WaitGroup - 知乎 (zhihu.com)

WaitGroup 是 Go 内置的 sync 包解决任务编排的并发原语。WaitGroup 直译是“等待组”,翻译成大白话就是等待一组协程完成任务。如果没有完成,就阻塞。

WaitGroup即用于等待当前作用域的所有协程都跑完,这样就不会造成主线程退出作用域而清理内存导致协程里出现错误

WaitGroup 的用法很简单。标准库中的 WaitGroup 只有三个方法,分别是:

func (wg *WaitGroup) Add(delta int)
func (wg *WaitGroup) Done()
func (wg *WaitGroup) Wait()
  • Add:用来设置 WaitGroup 的计数值,delta 可正可负
  • Done:用来将 WaitGroup 的计数值减一,其实就是调用了 Add(-1)。
  • Wait:阻塞等待,直到 WaitGroup 的计数值变成0,进入下一步。

使用 WaitGroup 的常规套路如下:

  1. 声明 WaitGroup 变量
  2. 执行 Add 方法。协程组的个数有 n 个,执行 Add(n)
  3. 协程组中,每个协程最后,执行方法 Done
  4. 在协程组后面,执行 Wait 方法

下面将复述文章中实现110000001-1000000和的计算(顺便演示下atomic)

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

// 计算1000个数的和
func compute(m *sync.Mutex, wg *sync.WaitGroup, s, e int, count *int64) {
	var sum int64 = 0
	for i := s; i < e; i++ {
		sum += int64(i)
	}
	atomic.AddInt64(count, sum)
	wg.Done()
}

func main() {

	var m sync.Mutex
	var wg sync.WaitGroup

	var count int64
	wg.Add(1000)
	for i := 0; i < 1000; i++ {
		go compute(&m, &wg, i*1000+1, (i+1)*1000+1, &count)
	}
	wg.Wait()
	fmt.Println(count)
	return
}

context

context是goroutine的上下文,包含goroutine的运行状态,环境,现场等信息。context在goroutine之间传递上下文信息,包括取消信号,超时时间,截止时间等

推荐文章

context接口

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}

context 包定义了一个 Context 接口,包含 4 个方法:

  • Deadline() (deadline time.Time, ok bool)

    Deadline 方法返回结果有两个,第一个是截止时间,到了这个截止时间,Context 会自动取消;第二个是一个 bool 类型的值,如果 Context 没有设置截止时间,第二个返回结果是 false,如果需要取消这个 Context,就需要调用取消函数。

  • Done() <-chan struct{}

    Done 方法返回一个只读的 channel 对象,类型是 struct{},在 goroutine 中,如果 Done 方法返回的结果可以被读取,代表父 Context 被取消。

  • Err() error

    Err 方法返回 Context 被取消的原因。

  • Value(key interface{}) interface{}

    Value 方法返回此 Context 绑定的值。它是一个 kv 键值对,通过 key 获取对应 value 的值。

下面介绍几种常用的控制并发方法

  • 超时

    package main
    
    import (
    	"context"
    	"fmt"
    	"time"
    )
    
    func test(context context.Context) {
    	//todo
    	for {
    		fmt.Println("testing...")
    		time.Sleep(time.Second)
    	}
    }
    
    func main() {
    
    	context, _ := context.WithTimeout(context.Background(), time.Second*5)
    	go test(context)
    	<-context.Done()
    	return
    }
    /*
    输出:
    testing...
    testing...
    testing...
    testing...
    testing...
    
    */
    
  • 截至时间

    package main
    
    import (
    	"context"
    	"fmt"
    	"time"
    )
    
    func test(context context.Context) {
    	//todo
    	for {
    		fmt.Println("testing...")
    		select {
    		case <-context.Done():
    			fmt.Println("goroutime end")
    			return
    		default:
    			time.Sleep(time.Second)
    		}
    	}
    }
    func testr() {
    	context, _ := context.WithDeadline(context.Background(), time.Now().Add(time.Second*5))
    	go test(context)
    	<-context.Done()
    	fmt.Println("end")
    	//cancel()
    }
    func main() {
    
    	testr()
    	time.Sleep(time.Second * 100)
    	return
    }
    
    /*
    输出:
    testing...
    testing...
    testing...
    testing...
    testing...
    end
    testing...   
    goroutime end
    
    
    */
    
    
  • cancel

    • 同时取消多个协程

      可以看到,可以通过select控制特定的协程关闭

      package main
      
      import (
      	"context"
      	"fmt"
      	"time"
      )
      
      func main() {
      	ctx, cancel := context.WithCancel(context.Background())
      	go worker(ctx, "节点一")
      	go worker2(ctx, "节点二")
      	time.Sleep(time.Second * 5)
      	cancel()
      	time.Sleep(time.Second * 5)
      	fmt.Println("main goroutine 已结束")
      }
      
      func worker(ctx context.Context, node string) {
      	for {
      		select {
      		case <-ctx.Done():
      			fmt.Println(node, "goroutine 已停止")
      			return
      		default:
      			fmt.Println(node, "goroutine 正在运行")
      			time.Sleep(time.Second)
      		}
      	}
      }
      func worker2(ctx context.Context, node string) {
      	for {
      		fmt.Println(node, "goroutine 正在运行")
      		time.Sleep(time.Second)
      	}
      }
      /*输出
      节点一 goroutine 正在运行
      节点二 goroutine 正在运行
      节点二 goroutine 正在运行
      节点一 goroutine 正在运行
      节点一 goroutine 正在运行
      节点二 goroutine 正在运行
      节点二 goroutine 正在运行
      节点一 goroutine 正在运行
      节点一 goroutine 正在运行
      节点二 goroutine 正在运行
      节点二 goroutine 正在运行
      节点一 goroutine 已停止
      节点二 goroutine 正在运行
      节点二 goroutine 正在运行
      节点二 goroutine 正在运行
      节点二 goroutine 正在运行
      main goroutine 已结束
      */
      
      
    • 当有一个协程完成,退出全部协程

      package main
      
      import (
      	"context"
      	"fmt"
      	"time"
      )
      
      func main() {
      	ctx, cancel := context.WithCancel(context.Background())
      	go worker(ctx, cancel, "节点一")
      	go worker2(ctx, cancel, "节点二")
      	<-ctx.Done()
      }
      
      func worker(ctx context.Context, cancel context.CancelFunc, node string) {
      	defer cancel()
      	for i := 1; i <= 8; i++ {
      		fmt.Println("test 1")
      		time.Sleep(time.Second)
      	}
      }
      func worker2(ctx context.Context, cancel context.CancelFunc, node string) {
      	defer cancel()
      	for i := 1; i <= 3; i++ {
      		fmt.Println("test 2")
      		time.Sleep(time.Second)
      	}
      }
      
      /*
      输出:
      test 2
      test 1
      test 1
      test 2
      test 2
      test 1
      
      */
      

      要注意好goroutine的作用时机,避免发生goroutine泄漏 (如下)

      package main
      
      import (
      	"context"
      	"fmt"
      	"time"
      )
      
      func test(context context.Context) {
      	//todo
      	for {
      		fmt.Println("testing...")
      		time.Sleep(time.Second)
      	}
      }
      func testr() {
      	context, _ := context.WithDeadline(context.Background(), time.Now().Add(time.Second*5))
      	go test(context)
      	<-context.Done()
      	fmt.Println("end")
      	//cancel()
      }
      func main() {
      
      	testr()
      	time.Sleep(time.Second * 10)
      	return
      }
      
      /*
      输出:
      testing...
      testing...
      testing...
      testing...
      testing...
      testing...
      end
      testing...
      testing...
      testing...
      testing...
      
      */
      
      

值得一提的是,带value的context推荐用以下的方法

package main

import (
	"context"
	"fmt"
)

func test(ctx context.Context) {
	mp := ctx.Value("map").(map[string]interface{})
	fmt.Println(mp["test"])
	mp["hello"] = "world"
}
func main() {
	temp := make(map[string]interface{})
	ctx := context.WithValue(context.Background(), "map", temp)
	mp := ctx.Value("map").(map[string]interface{})
	mp["test"] = 1
	test(ctx)
	fmt.Println(mp["hello"])
	return
}
/*

输出
1
world

*/

go mod

没啥好说

一图胜千言,golang Module【入门到精通】 - 知乎 (zhihu.com)

go mod使用 | 全网最详细 - 知乎 (zhihu.com)

性能优化

看ppt就行,总结为以下几点

对于pprof,把项目拉下来跑一遍就行