go的调度 goroutine调度用了什么系统调用
go的调度原理是基于GMP模型,G代表一个goroutine,不限制数量;M=machine,代表一个线程,最大1万,所有G任务还是在M上执行;P=processor代表一个处理器,包含G运行的一切资源。G运行需要获取P,在M上运行。
参考:
https://cloud.tencent.com/developer/article/1422385
https://blog.csdn.net/chushoufengli/article/details/114940228
go struct能不能比较
除了slice、map、function不可比较,其他的基本的类型数字、指针、接口、chan、数组都能比较。
同一个struct的两个实例的比较,如果包含了不可比较的类型的字段则不能比较。否则可以比较。
不同struct的两个实例的比较,需要先进行结构类型的转换 转换成同一个结构类型才可以进行比较 结构体之间进行转换需要他们具备完全相同的成员(字段名、字段类型、字段个数) 。
func testEqual(){
var a chan int
var b chan int
print(a == b)
aA := [2]int{1, 2,}
aB := [2]int{1, 2,}
print(aA == aB)
_p1 := p1{"1", 2, [2]int{1,2},}
_p2 := p1{"1", 2, [2]int{1,2},}
print(_p1 == _p2)
_p3 := p2{"1", 2, [2]int{1,2},}
//强制类型转换
print(p1(_p3) == _p2)
}
type p1 struct {
f1 string
f2 int
f3 [2]int
}
type p2 struct {
f1 string
f2 int
f3 [2]int
}
select可以用于什么
golang 的 select 就是监听 IO 操作,当 IO 操作发生时,触发相应的动作,每个case语句里必须是一个IO操作,确切的说,应该是一个面向channel的IO操作
client如何实现长连接
server设置超时时间。客户端for循环遍历接收。
长连接:客户端发送RESTFUL请求,需要监测某一资源变化情况,服务端提供watch机制,在资源有变化时通知client端。
func httpClientLongCon() {
req, err := http.NewRequest("GET", "https://www.baidu.com", nil)
if err != nil {
log.Fatal(err)
}
httpClient := &http.Client{}
ret, err := httpClient.Do(req)
if err != nil {
log.Fatal(err)
}
buf := make([]byte, 4096)
for {
n, err := ret.Body.Read(buf)
if n == 0 && err != nil {
break
}
fmt.Println(string(buf[:n]))
}
}
主协程如何等其余协程完再操作
sync.waitgroup
slice,len,cap,共享,扩容
append函数,因为slice底层数据结构是,由指向数组的指针、len、cap组成,所以,在使用append扩容时,会查看数组后面有没有连续内存快,有就在后面添加,没有就重新生成一个大的素组
切片的扩容
如果切片的容量小于1024个元素,那么扩容的时候slice的cap就在当前容量的基础上翻番,乘以2;一旦元素个数超过1024个元素,增长因子就变成1.25,即每次增加当前容量的四分之一。
当向切片中添加数据时,如果没有超过容量,直接添加,如果超过容量,自动扩容(成倍增长)
当超过容量,切片指向的就不再原来的数组,而是内存地址中开辟了一个新的数组
map如何顺序读取
map不能顺序读取,是因为他是无序的,想要有序读取,首先的解决的问题就是,把key变为有序,所以可以把key放入切片,对切片进行排序,遍历切片,通过key取值。
func orderIterMap() {
m := make(map[string]string)
m["a"] = "123"
m["b"] = "456"
keys := []string{
"a", "b",
}
for i, _ := range keys {
if value, ok := m[keys[i]]; ok {
fmt.Println(value)
} else {
fmt.Printf("%s not exists", keys[i])
}
}
}
实现set
func main() {
mySet := New()
mySet.Add("a")
mySet.Add("b")
mySet.Add(1)
print(mySet)
}
type Set struct {
e map[interface{}]bool
}
//对外暴露的构造函数
func New() *Set {
return &Set{e: make(map[interface{}]bool)}
}
func (set *Set) Add(element interface{}) bool {
if !set.e[element] {
set.e[element] = true
return true
}
return false
}
func (set *Set) Remove(element interface{}) {
delete(set.e, element)
}
func (set *Set) Clear() {
set.e = make(map[interface{}]bool)
}
func (set *Set) Contains(element interface{}) bool {
return set.e[element]
}
func (set *Set) String() string {
var buf bytes.Buffer
buf.WriteString("Set{")
for k := range set.e {
buf.WriteString(fmt.Sprintf("%v,", k))
}
buf.WriteString("}")
return buf.String()
}
func print(o ...interface{}) {
fmt.Println(o)
}
实现消息队列(多生产者,多消费者)
使用切片加锁可以实现。
使用channel实现。
//chan
func producer(c chan int, i int) {
c <- i
}
func customer(c chan int) {
fmt.Println(<-c)
}
func testPC() {
queue := make(chan int, 10)
for i := 0; i < 10; i++ {
go producer(queue, i)
}
for i := 0; i < 10; i++ {
go customer(queue)
}
time.Sleep(time.Second)
}
//slice + lock
type queue struct {
msg []int
lock sync.Mutex
}
func producer(q *queue, i int) {
q.lock.Lock()
defer q.lock.Unlock()
q.msg = append(q.msg, i)
}
func customer(q *queue) {
q.lock.Lock()
defer q.lock.Unlock()
for len(q.msg) > 0 {
m := q.msg[0]
fmt.Println(GetGID(), m)
q.msg = q.msg[1:]
}
}
func testPC() {
q := queue{}
for i := 0; i < 10; i++ {
go producer(&q, i)
}
for i := 0; i < 10; i++ {
go customer(&q)
}
time.Sleep(time.Second)
}
func GetGID() uint64 {
b := make([]byte, 64)
b = b[:runtime.Stack(b, false)]
b = bytes.TrimPrefix(b, []byte("goroutine "))
b = b[:bytes.IndexByte(b, ' ')]
n, _ := strconv.ParseUint(string(b), 10, 64)
return n
}
Go的反射包
参考:https://blog.csdn.net/u012291393/article/details/78378386
手写循环队列
写的循环队列是不是线程安全,不是,怎么保证线程安全,加锁,效率有点低啊,然后面试官就提醒Go推崇原子操作和channel。。。
func testSqQueue(){
sq := InitQueue()
sq.EnQueue(1)
sq.EnQueue(2)
}
const CAP = 5
type SqQueue struct {
data [CAP]int
front int
rear int
}
func InitQueue() *SqQueue {
return &SqQueue{front: 0, rear: 0}
}
func (s *SqQueue) EnQueue(d int) error {
if (s.rear+1)%CAP == s.front {
return errors.New("full")
}
s.data[s.rear] = d
s.rear = (s.rear + 1) % CAP
return nil
}
func (s *SqQueue) DeQueue() (int, error) {
if s.rear == s.front {
return 1, errors.New("empty")
}
e := s.data[s.front]
s.data[s.front] = 0
s.front = (s.front + 1) % CAP
return e, nil
}
高效地拼接字符串
Go 语言中,字符串是只读的,也就意味着每次修改操作都会创建一个新的字符串。如果需要拼接多次,应使用
strings.Builder
,最小化内存拷贝次数。strings.Builder不是线程安全的。
func testStrBuilder() {
a := strings.Builder{}
a.WriteString("123")
}
//底层
// WriteString appends the contents of s to b's buffer.
// It returns the length of s and a nil error.
func (b *Builder) WriteString(s string) (int, error) {
b.copyCheck()
b.buf = append(b.buf, s...)
return len(s), nil
}
go优缺点
- 静态强类型
- 编译型
- 并发型。GMP调度器,内置一个大小的协程池
- 有自动垃圾回收功能的编程语言
go使用踩过什么坑
函数传值、传引用;for range 切片不能改变值
sync.Pool用过吗,为什么使用,对象池,避免频繁分配对象(GC有关),那里面的对象是固定的吗?
是用来保存和复用临时对象,以减少内存分配,降低CG压力。 里面的对象不是固定的。sync.Pool可以安全被多个线程同时使用,保证线程安全。sync.Pool中保存的任何项都可能随时不做通知的释放掉,所以不适合用于像socket长连接或数据库连接池。sync.Pool主要用途是增加临时对象的重用率,减少GC负担。
func (s *Student) String() string {
return s.name
}
func testPool() {
studentPool := sync.Pool{
New: func() interface{} {
return &Student{"abc"}
}}
for i:=0;i<100000;i++{
stud := studentPool.Get().(*Student)
fmt.Printf("%p %v\n", stud, stud)
}
}
//对比
type C1 struct {
B1 [10000000]int
}
func usePool() {
pool := sync.Pool{New:
func() interface{} {
return new(C1)
}}
startTime := time.Now()
for i := 0; i < 10000; i++ {
c := pool.Get().(*C1)
c.B1[0] = 1
pool.Put(c)//需要加上
}
fmt.Println("Used time : ", time.Since(startTime))
}
func standard() {
startTime := time.Now()
for i := 0; i < 10000; i++ {
var c C1
c.B1[0] = 1
}
fmt.Println("Used time : ", time.Since(startTime))
}
//standard Used time : 2m36.8892607s
//usePool Used time : 70.8105ms
go的runtime如何实现
Go 的 Runtime 是整个Go语言的核心,负责协程的调度,垃圾回收,以及协程运行环境维护等等。 Go代码由Go提供的专门的编译器编译。而 runtime 其实就是一个静态链接库,Go编译器会在链接阶段将runtime的部分与go代码进行静态链接。Runtime 相当于用户代码和系统内核的一个中间层。用户层使用channel,goroutine等等,都是调用的Runtime提供的接口,对于Go的业务代码来说,是接触不到真正的内核调用的。
GMP模型:一个G表示一个单个的 goroutine,对于用户层来说,每次调用 go 关键字就会创建一个G,编译器会将使用 go 关键字的代码片段(或函数)通过 newProc() 生成一个G,G的结构包括了这个代码段的上下文环境,最终这些G会交给runtime去调度。 M是runtime在OS层创建的线程,每个M都有一个正在M上运行的G,还有诸如缓存,锁,以及一个指向全局的G队列的指针等数据。
go的锁实现
type Mutex struct {
state int32
sema uint32
}
- Mutex.state表示互斥锁的状态,比如是否被锁定等。
- Mutex.sema表示信号量,协程阻塞等待该信号量,解锁的协程释放信号量从而唤醒等待信号量的协程。
我们看到Mutex.state是32位的整型变量,内部实现时把该变量分成四份,用于记录Mutex的四种状态。下图展示Mutex的内存布局:
- Locked: 表示该Mutex是否已被锁定,0:没有锁定 1:已被锁定。
- Woken: 表示是否有协程已被唤醒,0:没有协程唤醒 1:已有协程唤醒,正在加锁过程中。
- Starving:表示该Mutex是否处理饥饿状态, 0:没有饥饿 1:饥饿状态,说明有协程阻塞了超过1ms。
- Waiter: 表示阻塞等待锁的协程个数,协程解锁时根据此值来判断是否需要释放信号量。
多协程加锁解锁释放信号量场景;自旋;自旋条件;防止协程饿死
引用:https://blog.csdn.net/weixin_34208283/article/details/91699624
看过sql的连接池实现吗
github.com/jinzhu/gorm/dialects/mysql github.com/go-sql-driver/mysql gorm复用标准库sql的连接池。go标准库sql已经实现了连接池。
引用:https://www.cnblogs.com/ZhuChangwu/p/13412853.html#%E4%B8%80%E3%80%81%E5%A6%82%E4%BD%95%E7%90%86%E8%A7%A3%E6%95%B0%E6%8D%AE%E5%BA%93%E8%BF%9E%E6%8E%A5
go什么情况下会发生内存泄漏?
ctx没有cancel的时候。。。
定时器
func testAfter() {
tchan := time.After(time.Second * 3)
fmt.Println(time.Now().String(),"tchan=", <-tchan)
}
func testNewTicker() {
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
done := make(chan bool)
go func() {
time.Sleep(5 * time.Second)
done <- true
}()
for {
select {
case t := <-ticker.C:
fmt.Println("Current time: ", t)
case <-done:
return
}
}
}
func testTick() {
c := time.Tick(2 * time.Second)
for next := range c {
fmt.Printf("%v \n", next)
}
}
context包的用途
http包、sql中也用 。
Context 的主要作用就是在不同的 Goroutine 之间同步请求特定的数据、取消信号以及处理请求的截止日期。
引用:https://segmentfault.com/a/1190000024441501
func testCancelCtx() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
//每隔1s说一话,testCancelCtx函数在10s后执行cancel,那么speak检测到取消信号就会退出。
go func(ctx context.Context) {
for range time.Tick(time.Second) {
select {
case <-ctx.Done():
return
default:
fmt.Println("speak")
}
}
}(ctx)
time.Sleep(10 * time.Second)
}
func testDeadlineCtx() {
later, _ := time.ParseDuration("10s")
deadline := time.Now().Add(later)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
go func(ctx context.Context) {
select {
case <-ctx.Done():
fmt.Println(ctx.Err())
case <-time.After(20 * time.Second)://其他的case没有io的话,该case io会阻塞20秒后输入
fmt.Println("stop")
}
}(ctx)
time.Sleep(20 * time.Second)
}
func testTimeoutCtx() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
go func(ctx context.Context) {
select {
case <-ctx.Done():
fmt.Println(ctx.Err())
case <-time.After(20 * time.Second):
fmt.Println("stop monitor")
}
}(ctx)
time.Sleep(20 * time.Second)
}
type key string
func testValueCtx() {
ctx := context.WithValue(context.Background(), key("a"), "123")
Get(ctx, "a")
Get(ctx, "b")
}
func Get(ctx context.Context, k key) {
if v, ok := ctx.Value(k).(string); ok {
fmt.Println(v)
}
}
怎么实现协程完美退出?
1、通过channel传递退出信号。这种方式可以实现优雅地停止goroutine,但是当goroutine特别多的时候,这种方式 不太行。
2、使用waitgroup
go为什么高并发好?
语言层支持并发。Goroutine非常轻量:可以轻松支持10w 级别的Goroutine 运行上下文切换代价小;而对比线程的上下文切换则需要涉及模式切换(从用户态切换到内核态);内存占用少:线程栈空间通常是 2M,Goroutine 栈空间最小 2K;
Go调度器:GMP模型。。。
引用:https://cloud.tencent.com/developer/article/1594342
怎么理解go的interface
interface的内部实现包含了 2 个字段,类型
T
和 值V
空的interface类似一个任意类型,任何类型的struct都实现了空接口。
协程泄露
协程泄露是指协程创建后,长时间得不到释放。程序后续不断地创建新的协程,最终导致内存耗尽,程序崩溃。常见的导致协程泄露的场景有以下几种:
- 缺少接收器,导致发送阻塞
这个例子中,每执行一次 query,则启动1000个协程向信道 ch 发送数字 0,但只接收了一次,导致 999 个协程被阻塞,不能退出。
func query() int {
ch := make(chan int)
for i := 0; i < 1000; i++ {
go func() { ch <- 0 }()
}
return <-ch
}
func main() {
for i := 0; i < 4; i++ {
query()
fmt.Printf("goroutines: %d\n", runtime.NumGoroutine())
}
}
- 缺少发送器,导致接收阻塞
那同样的,如果启动 1000 个协程接收信道的信息,但信道并不会发送那么多次的信息,也会导致接收协程被阻塞,不能退出。
- 死锁(dead lock)
两个或两个以上的协程在执行过程中,由于竞争资源或者由于彼此通信而造成阻塞,这种情况下,也会导致协程被阻塞,不能退出。
- 无限循环(infinite loops)
在协程中,为了避免网络等问题,采用了无限重试的方式,发送 HTTP 请求,直到获取到数据。那如果 HTTP 服务宕机,永远不可达,导致协程不能退出,发生泄漏。
定位方法:goroutine泄露会使用到pprof,pprof是Go的性能工具 ;监控内存,协程数。
解决方法:goroutine泄漏处理,设置timeout,select加定时器。监测机制
引用:https://zhuanlan.zhihu.com/p/74090074
用channel实现自定义定时器
type MyTimer struct {
timeDelay time.Duration
c chan time.Time
}
func NewTimer(t time.Duration) *MyTimer {
return &MyTimer{t, make(chan time.Time)}
}
func (t *MyTimer) Tick() {
go func() {
for {
time.Sleep(t.timeDelay)
t.c <- time.Now()
}
}()
}
func testMyTimer() {
timer := NewTimer(2 * time.Second)
timer.Tick()
for i := range timer.c {
fmt.Println(i)
}
}
逃逸分析
Go 语言的局部变量分配在栈上还是堆上由编译器决定。Go 语言编译器会自动决定把一个变量放在栈还是放在堆,编译器会做逃逸分析(escape analysis),当发现变量的作用域没有超出函数范围,就可以在栈上,反之则必须分配在堆上。
foo() 函数中,如果 v 分配在栈上,foo 函数返回时,&v 就不存在了,但是这段函数是能够正常运行的。Go 编译器发现 v 的引用脱离了 foo 的作用域,会将其分配在堆上。因此,main 函数中仍能够正常访问该值。
func foo() *int {
v := 11
return &v
}
func main() {
m := foo()
println(*m) // 11
}
new和make的区别
new 的作用是初始化一个指向类型的指针(*T)。new函数是内建函数,函数定义:func new(Type) *Type
使用new函数来分配空间。传递给
new
函数的是一个类型,不是一个值。返回值是 指向这个新分配的零值的指针。make 的作用是为 slice,map 或 chan 初始化并返回引用(T)。make函数是内建函数,函数定义:func make(Type, size IntegerType) Type。
make(T, args)
函数的目的与new(T)
不同。它仅仅用于创建 Slice, Map 和 Channel,并且返回类型是 T(不是T*)的一个初始化的(不是零值)的实例。
go命令
go env: #用于查看go的环境变量
go run: #用于编译并运行go源码文件
go build: #用于编译源码文件、代码包、依赖包
go get: #用于动态获取远程代码包
go install: #用于编译go文件,并将编译结构安装到bin、pkg目录
go clean: #用于清理工作目录,删除编译和安装遗留的目标文件
go version: #用于查看go的版本信息
go mod 包管理