Below you will find pages that utilize the taxonomy term “golang”
January 16, 2023
Golang 中网络请求使用指定网卡
当用户发起一个网络请求时,流量会通过默认的网卡接口流出与流入,但有时需要将流量通过指定的网卡进行流出流入,这时我们可能需要进行一些额外的开发工作,对其实现主要用到了 Dialer.Control 配置项。
type Dialer struct { // If Control is not nil, it is called after creating the network // connection but before actually dialing. // // Network and address parameters passed to Control method are not // necessarily the ones passed to Dial. For example, passing "tcp" to Dial // will cause the Control function to be called with "tcp4" or "tcp6". Control func(network, address string, c syscall.
February 25, 2022
一文看懂Golang 定时器源码
计时器分 Timer 和 Ticker 两种,它们底层基本是一样的,两差的区别请参考 , 这里我们的介绍对象是 Timer 。golang timer
计时器结构体 // NewTimer creates a new Timer that will send // the current time on its channel after at least duration d. func NewTimer(d Duration) *Timer { c := make(chan Time, 1) t := &Timer{ C: c, r: runtimeTimer{ when: when(d), f: sendTime, arg: c, }, } startTimer(&t.r) return t } 通过调用 NewTimer() 函数创建一个 Timer,首先创建一个长度为1的有缓冲channel,再创建一个Timer的结构体,并将 channel 置于 Timer 结构体内。
注意这里的 runtimeTimer.
November 25, 2021
Golang常见编译参数
在执行 go build 命令的时候,经常需要添加一些参数,或许是为了调试,也或许是为了生成最终部署二进制文件。
在编译特定包时需要传递参数,格式应遵守“包名=参数列表”,如
go build -gcflags -gcflags='log=-N -l' main.go -gcflags go build 可以用 -gcflags 给_go_编译器传入参数,也就是传给 go tool compile 的参数,因此可以用 go tool compile –help 查看所有可用的参数。
其中 -m 可以检查代码的编译优化情况,包括逃逸情况和函数是否内联。
-ldflags go build用 -ldflags 给go链接器传入参数,实际是给go tool link的参数,可以用go tool link –help查看可用的参数。
常用-X来指定版本号等编译时才决定的参数值。例如代码中定义var buildVer string,然后在编译时用go build -ldflags “-X main.buildVer=1.0” … 来赋值。注意-X只能给string类型变量赋值。
November 25, 2021
Golang中的 CGO_ENABLED 环境变量
Golang中的编译参数
开发中经常使用 go build 命令来编译我们的程序源码,然后将生成二进制文件直接部署,极其方便。
对于 go build 有一些参数,对于针对程序源码进行一些编译优化,下面我们对经常使用的一些参数来介绍一下。
环境变量 环境变量需要在go命令前面设置,如果多个变量的话,中间需要用“空格”分隔。下面我们介绍一个非常常见到的一些环境变量
$ CGO_ENABLED=1 GOARCH=amd64 GOOS=linux go build -o myserver main.go 除了这里给出的这几个变量外,还有一些其它变量,如 GODEBUG、GOFLAGS、GOPROXY 等,所有支持环境变量都可以在 里找到,有兴趣的话可以看看他们的作用。
这里重点介绍一下 CGO_ENABLED 环境变量对我们程序的影响。 CGO_ENABLED是用来控制golang 编译期间是否支持调用 cgo 命令的开关,其值为1或0,默认情况下值为1,可以用 go env 查看默认值。
如果你的程序里调用了cgo 命令,此参数必须设置为1,否则将编译时出错。这里直接用文档 中的一个例子验证。
package main // #include <stdio.h> // #include <stdlib.h> // // static void myprint(char* s) { // printf("%sn", s); // } import "C" import "unsafe" func main() { cs := C.CString("Hello from stdio") C.myprint(cs) C.
May 23, 2021
Golang中的runtime.LockOSThread 和 runtime.UnlockOSThread
在runtime中有 [runtime.LockOSThread](https://github.com/golang/go/blob/go1.16.3/src/runtime/proc.go#L4248-L4278) 和 [runtime.UnlockOSThread](https://github.com/golang/go/blob/go1.16.3/src/runtime/proc.go#L4302-L4323) 两个函数,这两个函数有什么作用呢?我们看一下标准库中对它们的解释。
runtime.LockOSThread // LockOSThread wires the calling goroutine to its current operating system thread. // The calling goroutine will always execute in that thread, // and no other goroutine will execute in it, // until the calling goroutine has made as many calls to // UnlockOSThread as to LockOSThread. // If the calling goroutine exits without unlocking the thread, // the thread will be terminated. // // All init functions are run on the startup thread.
May 10, 2021
Runtime: goroutine的暂停和恢复源码剖析
上一节《 GC 对根对象扫描实现的源码分析》中,我们提到过在GC的时候,在对一些goroutine 栈进行扫描时,会在其扫描前触发 G 的暂停([suspendG](https://github.com/golang/go/blob/go1.16.2/src/runtime/preempt.go#L76-L254))和恢复([resumeG](https://github.com/golang/go/blob/go1.16.2/src/runtime/preempt.go#L256-L280))。
// markroot scans the i'th root. // // Preemption must be disabled (because this uses a gcWork). // // nowritebarrier is only advisory here. // //go:nowritebarrier func markroot(gcw *gcWork, i uint32) { baseFlushCache := uint32(fixedRootCount) baseData := baseFlushCache + uint32(work.nFlushCacheRoots) baseBSS := baseData + uint32(work.nDataRoots) baseSpans := baseBSS + uint32(work.nBSSRoots) baseStacks := baseSpans + uint32(work.nSpanRoots) end := baseStacks + uint32(work.nStackRoots) // Note: if you add a case here, please also update heapdump.
May 7, 2021
goroutine栈的申请与释放
对于提高对 stack 的使用效率,避免重复从heap中分配与释放,对其使用了 pool 的概念,runtime 里为共提供了两个pool, 分别为 stackpool ,另一个为 stackLarge。stack pool
stackpool: 16b~32k 对应通用的大小的stack。获取时通过调用 stackpoolalloc(), 释放时调用 stackpoolfree()。
stackLarge:对应 > 32K 的 stack
在程序全局调度器 初始化 时会通过调用 stackinit() 实现对 stack 初始化。
当我们执行一个 go func() 语句的时候,runtime 会通过调用 newproc() 函数来创建G。而内部真正创建G的函数为 [newproc1()](https://github.com/golang/go/blob/go1.16.3/src/runtime/proc.go#L3990-L4098),在没有G可以复用的情况下,会通过 newg = malg(_StackMin) 语句创建一个包含stack的G。
// Allocate a new g, with a stack big enough for stacksize bytes. func malg(stacksize int32) *g { newg := new(g) if stacksize >= 0 { stacksize = round2(_StackSystem + stacksize) systemstack(func() { newg.
May 7, 2021
Golang的GPM 模型在网络编程中存在的问题
现状 目前在网络编程中,golang采用的是一种 goroutine-per-connection 的模式,即为每一个连接都分配一个goroutine,一个连接就是一个goroutine,多个连接之间没有关系。
package main import ( "fmt" "io/ioutil" "net" "time" ) //模拟server端 func main() { tcpServer, _ := net.ResolveTCPAddr("tcp4", ":8080") listener, _ := net.ListenTCP("tcp", tcpServer) for { //当有新客户端请求时拿到与客户端的连接 conn, err := listener.Accept() if err != nil { fmt.Println(err) continue } // 处理逻辑 goroutine-per-connection go handle(conn) } } func handle(conn net.Conn) { defer conn.Close() //读取客户端传送的消息 go func() { response, _ := ioutil.ReadAll(conn) fmt.Println(string(response)) }() //向客户端发送消息 time.Sleep(1 * time.Second) now := time.
April 30, 2021
缓存池 bytebufferpool 库实现原理
上一节 《Runtime: Golang 之 sync.Pool 源码分析》 我们介绍了sync.Pool 的源码分析,本节介绍一个 fasthttp 中引用的一缓存池库 [bytebufferpool](https://github.com/valyala/bytebufferpool),这两个库是同一个开发者。对于这个缓存池库与同类型的几个库的对比,可以参考 https://omgnull.github.io/go-benchmark/buffer/。
建议大家了解一下[fasthttp](https://github.com/valyala/fasthttp) 这个库,性能要比直接使用内置的 net/http 高出很多,其主要原因是大量的用到了缓存池 sync.Pool 进行性能提升。
用法 // https://github.com/valyala/bytebufferpool/blob/18533face0/bytebuffer_example_test.go package bytebufferpool_test import ( "fmt" "github.com/valyala/bytebufferpool" ) func ExampleByteBuffer() { // 从缓存池取 Get() bb := bytebufferpool.Get() // 用法 bb.WriteString("first linen") bb.Write([]byte("second linen")) bb.B = append(bb.B, "third linen"...) fmt.Printf("bytebuffer contents=%q", bb.B) // 使用完毕,放回缓存池 Put() // It is safe to release byte buffer now, since it is no longer used. bytebufferpool.Put(bb) } 全局变量 我们先看一下与其相关的一些常量
April 12, 2021
Golang 内存组件之mspan、mcache、mcentral 和 mheap 数据结构
Golang中的内存组件关系如下图所示golang 内存分配组件
在学习golang 内存时,经常会涉及几个重要的数据结构,如果不熟悉它们的情况下,理解起来就显得格外的吃力,所以本篇主要对相关的几个内存组件做下数据结构的介绍。
在 Golang 中,mcache、mspan、mcentral 和 mheap 是内存管理的四大组件,mcache 管理线程在本地缓存的 mspan,而 mcentral 管理着全局的 mspan 为所有 mcache 提供所有线程。
根据分配对象的大小,内部会使用不同的内存分配机制,详细参考函数 [mallocgo()](https://github.com/golang/go/blob/go1.16.2/src/runtime/malloc.go#L902-L1171),所于内存分配与回收,参考文件介绍 malloc.go
<16KB 会使用微小对象内存分配器从 P 中的 mcache 分配,主要使用 mcache.tinyXXX 这类的字段 16-32KB 从 P 中的 mcache 中分配 >32KB 直接从 mheap 中分配 对于golang中的内存申请流程,大家应该都非常熟悉了,这里不再进行详细描述。Golang 内存组件关系
mcache 在GPM关系中,会在每个 P 下都有一个 mcache 字段,用来表示内存信息。
在 Go 1.2 版本以前调度器使用的是 GM 模型,将 mcache 放在了 M 里,但发现存在诸多问题,其中对于内存这一块存在着巨大的浪费。每个 M 都持有 mcache 和 stack alloc,但只有在 M 运行 Go 代码时才需要使用内存(每个 mcache 可以高达2mb),当 M 在处于 syscall 或 网络请求 的时候是不需要内存的,再加上 M 又是允许创建多个的,这就造成了内存的很大浪费。所以从go 1.
April 9, 2021
GC 对根对象扫描实现的源码分析
工作池gcWork 工作缓存池(work pool)实现了生产者和消费者模型,用于指向灰色对象。一个灰色对象在工作队列中被扫描标记,一个黑色对象表示已被标记不在队列中。
写屏障、根发现、栈扫描和对象扫描都会生成一个指向灰色对象的指针。扫描消费时会指向这个灰色对象,从而将先其变为黑色,再扫描它们,此时可能会产生一个新的指针指向灰色对象。这个就是三色标记法的基本知识点,应该很好理解。
gcWork 是为垃圾回收器提供的一个生产和消费工作接口。
它可以用在stack上,如
(preemption must be disabled) gcw := &getg().m.p.ptr().gcw .. call gcw.put() to produce and gcw.tryGet() to consume .. 在标记阶段使用gcWork可以防止垃圾收集器转换到标记终止,这一点很重要,因为gcWork可能在本地持有GC工作缓冲区。可以通过禁用抢占(systemstack 或 acquirem)来实现。
数据结构
type gcWork struct { wbuf1, wbuf2 *workbuf bytesMarked uint64 scanWork int64 flushedWork bool } wbuf1,wbuf2:这里 wbuf1 是主工作缓存区; wbuf2为次工作缓存区,两者要么都是nil,要么都不是。 这可以看作是两个工作缓冲区指针串联的堆栈。当我们弹出最后一个指针的时候,我们可以引入新的缓存区,并将指针向上移动一个空缓存区,从而丢失掉的缓存区;当我们填充两个缓存区的时,可以通过引入一个新的空缓冲区并丢弃一个满的缓冲区,同时将堆栈向下移动一个工作缓冲区。 bytesMarked 标记为黑色对象的累计大小 scanWork 扫描统计 flushedWork 表示自上次 gcMarkDone 终止检查以来,已将非空工作缓存区刷新到全局工作队列。表示是否gcWork可能传递给了另一个gcWork wbuf1 和 wbuf2 为 workbuf 数据类型,其数据结构
type workbuf struct { workbufhdr // account for the above fields obj [(_WorkbufSize - unsafe.
April 6, 2021
Golang中的切片与GC
今天再看 timer 源码的时候,在函数 [clearDeletedTimers()](https://github.com/golang/go/blob/go1.16.2/src/runtime/time.go#L904-L992) 里看到一段对切片的处理代码,实现目的就是对一个切片内容进行缩容。
// src/runtime/time.go // The caller must have locked the timers for pp. func clearDeletedTimers(pp *p) { timers := pp.timers ...... // 对无用的切片元素赋值 nil for i := to; i < len(timers); i++ { timers[i] = nil } atomic.Xadd(&pp.deletedTimers, -cdel) atomic.Xadd(&pp.numTimers, -cdel) atomic.Xadd(&pp.adjustTimers, -cearlier) timers = timers[:to] pp.timers = timers updateTimer0When(pp) ...... } 变量 to 指新切片的长度, len(timers)指原来切片的长度。
这里在其进行 timers = timers[:to] 操作前,先是将 to 数组索引后的值进行了赋值 nil。按照我们平常的用法,赋 nil 值是没有必要执行这一步的,那为什么这里要加这一步呢?主要还是与GC 有关。
March 29, 2021
Runtime: Golang 定时器实现原理及源码解析
定时器作为开发经常使用的一种数据类型,是每个开发者需要掌握的,对于一个高级开发很有必要了解它的实现原理,今天我们runtime源码来学习一下它的底层实现。
定时器分两种,分别为 Timer 和 Ticker,两者差不多,这里重点以Timer为例。
源文件位于 [src/time/sleep.go](https://github.com/golang/go/blob/go1.16.2/src/time/sleep.go) 和 [src/time/tick.go](https://github.com/golang/go/blob/go1.16.2/src/time/tick.go) 。 go version 1.16.2
数据结构 Timer 数据结构
// src/runtime/sleep.go // The Timer type represents a single event. // When the Timer expires, the current time will be sent on C, // unless the Timer was created by AfterFunc. // A Timer must be created with NewTimer or AfterFunc. type Timer struct { C <-chan Time r runtimeTimer } Timer 数据类型是表示单个事件。当计时器过期时,当前的时候将会发送到 Timer.C 通道,如果用 AfterFunc 创建计时器的话,则例外。
March 28, 2021
Golang中的CAS原子操作 和 锁
在高并发编程中,经常会出现对同一个资源并发访问修改的情况,为了保证最终结果的正确性,一般会使用 锁 和 CAS原子操作 来实现。
如要对一个变量进行计数统计,两种实现方式分别为
package main import ( "fmt" "sync" ) // 锁实现方式 func main() { var count int64 var wg sync.WaitGroup var mu sync.Mutex for i := 0; i < 10000; i++ { wg.Add(1) go func(wg *sync.WaitGroup) { defer wg.Done() mu.Lock() count = count + 1 mu.Unlock() }(&wg) } wg.Wait() // count = 10000 fmt.Println("count = ", count) } 与
package main import ( "fmt" "sync" "sync/atomic" ) // atomic CAS 原子操作 func main() { var count int64 var wg sync.
March 23, 2021
Golang并发同步原语之-信号量Semaphore
信号量是并发编程中比较常见的一种同步机制,它会保持资源计数器一直在0-N(N表示权重值大小,在用户初始化时指定)之间。当用户获取的时候会减少一点,使用完毕后再恢复过来。当遇到请求时资源不够的情况下,将会进入休眠状态以等待其它进程释放资源。
在 Golang 官方扩展库中为我们提供了一个基于权重的信号量 [semaphore](https://github.com/golang/sync/blob/master/semaphore/semaphore.go) 并发原语。
你可以将下面的参数 n 理解为资源权重总和,表示每次获取时的权重;也可以理解为资源数量,表示每次获取时必须一次性获取的资源数量。为了理解方便,这里直接将其理解为资源数量。
数据结构 [semaphoreWeighted](https://github.com/golang/sync/blob/master/semaphore/semaphore.go#L19-L33) 结构体
type waiter struct { n int64 ready chan<- struct{} // Closed when semaphore acquired. } // NewWeighted creates a new weighted semaphore with the given // maximum combined weight for concurrent access. func NewWeighted(n int64) *Weighted { w := &Weighted{size: n} return w } // Weighted provides a way to bound concurrent access to a resource. // The callers can request access with a given weight.
March 22, 2021
学习Golang GC 必知的几个知识点
对于gc的介绍主要位于 [src/runtime/mgc.go](https://github.com/golang/go/blob/go1.16.2/src/runtime/mgc.go),以下内容是对注释的翻译。
GC 四个阶段 通过源文件注释得知GC共分四个阶段:
GC 清理终止 (GC performs sweep termination) a. Stop the world, 每个P 进入GC safepoint(安全点),从此刻开始,万物静止。 b. 清理未被清理的span,如果GC被强制执行时才会出现这些未清理的span GC 标记阶段(GC performs the mark phase) a. 将gc标记从 _GCoff 修改为 _GCmark,开启写屏障(write barries)和 协助助手(mutator assists),将根对象放入队列。 在STW期间,在所有P都启用写屏障之前不会有什么对象被扫描。 b. Start the world(恢复STW)。标记工作线程和协助助手并发的执行。对于任何指针的写操作和指针值,都会被写屏障覆盖,使新分配的对象标记为黑色。 c. GC 执行根标记工作。包括扫描所有的栈,全局对象和不在堆数据结构中的堆指针。每扫描一个栈就会导致goroutine停止,把在栈上找到的所有指针置灰色,然后再恢复goroutine运行。 d. GC 遍历队列中的每个灰色对象,扫描完以后将灰色对象标记为黑色,并将其指向的对象标记为灰色。 e. 由于GC工作在分布本地缓存中,采用了一种 “分布式终止算法(distributed termination algorithm)” 来检测什么时候没有根对象或灰色对象。在这个时机GC会转为标记中止(mark termination)。 标记终止(GC performs mark termination) a. Stop the world,从此刻开始,万物静止 b. 设置阶段为 _GCmarktermination,并禁用 工作线程worker和协助助手 c. 执行清理,flush cache 清理阶段(GC performs the sweep phase) a.
March 20, 2021
Runtime: Golang 之 sync.Pool 源码分析
Pool 指一组可以单独保存和恢复的 临时对象。Pool 中的对象随时都有可能在没有收到任何通知的情况下被GC自动销毁移除。
多个goroutine同时操作Pool是并发安全的。
源文件为 [src/sync/pool.go](https://github.com/golang/go/blob/master/src/sync/pool.go) go version: 1.16.2
为什么使用Pool 在开发高性能应用时,经常会有一些完全相同的对象需要频繁的创建和销毁,每次创建都需要在堆中分配对象,等使用完毕后,这些对象需要等待GC回收。我们知道在Golang中使用三色标记法进行垃圾回收的,在回收期间会有一个短暂STW(stop the world)的时间段,这样就会导致程序性能下降。
那么能否实现类似数据库连接池这种效果,用来避免对象的频繁创建和销毁,达到尽可能的资源复用呢?为了实现这种需求,标准库中有了sync.Pool 这个数据结构。看名字很知道它是一个池。但是它和我们想象中的数据库连接池还是有些差别的。对于数据库连接池这种资源只要不手动释放就可以一直利用,但对于 sync.Pool 则不一样,主要是因为Pool里的对象是随时都有可能被销毁,即这些都 临时对象。只要进行了GC,就会出现对象销毁的情况。所以不用使用Pool当作数据库连接池。
总之记住一点:sync.Pool中的资源随时都有可能被销毁而消失,这是与我们日常所说的池最大的区别,切勿乱用。
sync.Pool 基本信息 与 Pool 相关的主要有三个常量,其中 allPoolsMu 是一个全局锁;对于 allPoos 和 oldPools 则是一个 *Pool 数组,主要用在当P数量发生变化(增加)时会导致一些P找不到自己对应的 localPool,会将当前 Pool 放入 allPools,这样便于当GC发生时对其进行清理。
var ( allPoolsMu Mutex // allPools is the set of pools that have non-empty primary // caches. Protected by either 1) allPoolsMu and pinning or 2) // STW. allPools []*Pool // oldPools is the set of pools that may have non-empty victim // caches.
March 19, 2021
Runtime: Golang同步原语Mutex源码分析
在 sync 包里提供了最基本的同步原语,如互斥锁 Mutex。除 Once 和 WaitGroup 类型外,大部分是由低级库提供的,更高级别的同步最好是通过 channel 通讯来实现。
Mutex 类型的变量默认值是未加锁状态,在第一次使用后,此值将不得复制,这点切记!!!
本文基于go version: 1.16.2
Mutex 锁实现了 Locker 接口。
// A Locker represents an object that can be locked and unlocked. type Locker interface { Lock() Unlock() } 锁的模式 为了互斥公平性,Mutex 分为 正常模式 和 饥饿模式 两种。
正常模式 在正常模式下,等待者 waiter 会进入到一个FIFO队列,在获取锁时waiter会按照先进先出的顺序获取。当唤醒一个waiter 时它被并不会立即获取锁,而是要与新来的goroutine竞争,这种情况下新来的goroutine比较有优势,主要是因为它已经运行在CPU,可能它的数量还不少,所以waiter大概率下获取不到锁。在这种waiter获取不到锁的情况下,waiter会被添加到队列的前面。如果waiter获取不到锁的时间超出了1毫秒,它将被切换为饥饿模式。
这里的 waiter 是指新来一个goroutine 时会尝试一次获取锁,如果获取不到我们就视其为watier,并将其添加到FIFO队列里。
饥饿模式 在正常模式下,每次新来的goroutine都会抢走锁,就这会导致一些 waiter 永远也获取不到锁,产生饥饿问题。所以为了应对高并发抢锁场景下的公平性,官方引入了饥饿模式。
在饥饿模式下,锁将直接交给队列最前面的waiter。新来的goroutine即使在锁未被持有情况下也不会参与竞争锁,同时也不会进行自旋,而直接将其添加到队列的尾部。
如果拥有锁的waiter发现有以下两种情况,它将切换回正常模式:
它是队列里的最后一个waiter,再也没有其它waiter 等待时间小于1毫秒 模式区别 正常模式 拥有更好的性能,因为即使等待队列里有抢锁的 waiter,由于新来的goroutine 正在CPU中运行,所以优先获取到锁。 饥饿模式 是对公平性和性能的一种平衡,它避免了某些 goroutine 长时间的等待锁。在饥饿模式下,优先处理的是那些一直在等待的 waiter。饥饿模式在一定机时会切换回正常模式。
March 5, 2021
Golang什么时候会触发GC
Golang采用了三色标记法来进行垃圾回收,那么在什么场景下会触发这个GC动作呢?
源码主要位于文件 [src/runtime/mgc.go](https://github.com/golang/go/blob/go1.16/src/runtime/mgc.go) go version 1.16
触发条件从大方面来说,分为 手动触发 和 系统触发 两种方式。手动触发一般很少用,主要通过开发者调用 runtime.GC() 函数来实现,而对于系统自动触发是 运行时 根据一些条件自行维护的,这也正是本文要介绍的内容。
不管哪种触发方式,底层回收机制是一样的,所以我们先看一下手动触发,看看能否根据它来找GC触发所需的条件。
// src/runtime/mgc.go // GC runs a garbage collection and blocks the caller until the // garbage collection is complete. It may also block the entire // program. func GC() { n := atomic.Load(&work.cycles) // 等待上一轮的标记终止 gcWaitOnMark(n) // We're now in sweep N or later. Trigger GC cycle N+1, which // will first finish sweep N if necessary and then enter sweep // termination N+1.
March 4, 2021
Golang 基于信号的异步抢占与处理
在Go1.14版本开始实现了 基于信号的协程抢占调度 模式,在此版本以前执行以下代码是永远也无法执行最后一条println语句。
本文基于go version 1.16
package main import ( "runtime" "time" ) func main() { runtime.GOMAXPROCS(1) go func() { for { } }() time.Sleep(time.Millisecond) println("OK") } 原因很简单:在main函数里只有一个CPU,从上到下执行到 time.Sleep() 函数的时候,会将 main goroutine 放入运行队列,出让了P,开始执行匿名函数,但匿名函数是一个for循环,没有任何 IO 语句,也就无法引起对 G 的调度,所以当前仅有的一个 P 永远被其占用,导致无法打印OK。
这个问题在1.14版本开始有所改变,主要是因为引入了基于信号的抢占模式。在程序启动时,初始化信号,并在 runtime.sighandler 函数注册了 SIGURG 信号的处理函数 runtime.doSigPreempt,然后在触发垃圾回收的栈扫描时或执行 sysmon 监控线程时,调用函数挂起goroutine,并向M发送信号,M收到信号后,会让当前goroutine陷入休眠继续执行其他的goroutine。
本篇从发送与接收信号并处理两方面来看一下它是如何实现的。
发送信号 在上篇文章( 认识sysmon监控线程)介绍 sysmon 的时候,我们知道监控线程会在无P的情况下一直运行,定期扫描所有的P,将长时间运行的G 进行解除。
// Always runs without a P, so write barriers are not allowed. // //go:nowritebarrierrec func sysmon() { .
March 1, 2021
Golang 的调度策略之G的窃取
我们上篇文章( Golang 的底层引导流程/启动顺序)介绍了一个golang程序的启动流程,在文章的最后对于最重要的一点“调度“ (函数 [schedule()](https://github.com/golang/go/blob/go1.15.6/src/runtime/proc.go#L2607-L2723)) 并没有展开来讲,今天我们继续从源码来分析一下它的调度机制。
在此之前我们要明白golang中的调度主要指的是什么?在 src/runtime/proc.go 文件里有一段注释这样写到
// Goroutine scheduler
// The scheduler’s job is to distribute ready-to-run goroutines over worker threads.
这里指如何找一个已准备好运行的 G 关联到PM 让其执行。对于G 的调度可以围绕三个方面来理解:
时机:什么时候关联(调度)。对于调度时机一般是指有空闲P的时候都会去找G执行 对象:选择哪个G进行调度。这是我们本篇要讲的内容 机制:如何调度。execute() 函数 理解了这三个问题,基本也就明白了它的调度策略了,本篇主要对G的获取。
源文件 [src/runtime/proc.go](https://github.com/golang/go/blob/go1.15.6/src/runtime/proc.go) , go version 1.15.6
获取G流程 下面我们看一下 schedule() 函数
// One round of scheduler: find a runnable goroutine and execute it. // Never returns. func schedule() {} 从注释我们可得知以下几点:
调度是一轮一轮执行的,并不是只执行一次 调度的工作就是找到一个 runnable 状态的G,对于G的选择可直接理解为调度(实际上这只是调试中的前半部分,找到G 还要把它放在一个位置去执行),至少大部分场景下我们谈论的是指G的调度 当前这个调度并无返回值,也就是说在当函数执行结束时就代表当前一轮的调度已结束(不严谨)。剩下的执行此函数后面的程序 func schedule() { // 获取当前 g0 _g_ := getg() if _g_.
February 27, 2021
Runtime: Golang是如何处理系统调用阻塞的?
我们知道在Golang中,当一个Goroutine由于执行 系统调用 而阻塞时,会将M从GPM中分离出去,然后P再找一个G和M重新执行,避免浪费CPU资源,那么在内部又是如何实现的呢?今天我们还是通过学习Runtime源码的形式来看下他的内部实现细节有哪些?
go version 1.15.6
我们知道一个P有四种运行状态,而当执行系统调用函数阻塞时,会从 _Prunning 状态切换到 _Psyscall,等系统调用函数执行完毕后再切换回来。P的状态切换
从上图我们可以看出 P 执行系统调用时会执行 [entersyscall()](https://github.com/golang/go/blob/go1.15.6/src/runtime/proc.go#L3134-L3142) 函数(另还有一个类似的阻塞函数 entersyscallblock() ,注意两者的区别)。当系统调用执行完毕切换回去会执行 exitsyscall() 函数,下面我们看一下这两个函数的实现。
进入系统调用 // Standard syscall entry used by the go syscall library and normal cgo calls. // // This is exported via linkname to assembly in the syscall package. // //go:nosplit //go:linkname entersyscall func entersyscall() { reentersyscall(getcallerpc(), getcallersp()) } 当通过Golang标准库 syscall 或者 cgo 调用时会执行 entersyscall() 函数,并通过 go:linkname 方式导出为标准包。此函数只是对 [reentersyscall()](https://github.com/golang/go/blob/go1.15.6/src/runtime/proc.go#L3037-L3132) 函数的封装,我们看下这个函数实现了什么。
函数注释比较多,这里只帖子重点的一部分
// The goroutine g is about to enter a system call.
February 26, 2021
Runtime: 当一个goroutine 运行结束后会发生什么
上一篇我们介绍了 创建一个goroutine 会经历些什么,今天我们再看下当一个goroutine 运行结束的时候,又会发生什么?
go version 1.15.6。
主要源文件为 [src/runtime/proc.go](https://github.com/golang/go/blob/go1.15.6/src/runtime/proc.go)。
当一个goroutine 运行结束的时候,默认会执行一个 [goexit1()](https://github.com/golang/go/blob/go1.15.6/src/runtime/proc.go#L2941-L2950) 的函数,这是一个只有八行代码的函数,其中最后以通过 [mcall()](https://github.com/golang/go/blob/go1.15.6/src/runtime/stubs.go#L34) 调用 [goexit0](https://github.com/golang/go/blob/go1.15.6/src/runtime/proc.go#L2952-L3011) 函数结束。因此我们主要关注 goexit0 函数即可。这里为了更好让大家理解,以此之前需要先介绍一下 mcall() 函数。
在 [stubs.go](https://github.com/golang/go/blob/go1.15.6/src/runtime/stubs.go#L20-L34) 源文件中只是对 [mcall()](https://github.com/golang/go/blob/go1.15.6/src/runtime/stubs.go#L34) 函数进行了声明,并无函数体,说明这个函数是使用汇编实现的。但这里我们并不需要关心它的具体实现,只要知道它的主要工作职责就可以了,所有信息我们都可以通过函数注释得知。
// mcall switches from the g to the g0 stack and invokes fn(g), // where g is the goroutine that made the call. // mcall函数用来从 g 切换到 g0 栈并调用 fn(g)函数,这里的 g 指发起调用的那个goroutine。 // mcall saves g's current PC/SP in g->sched so that it can be restored later.
February 17, 2021
Runtime: 创建一个goroutine都经历了什么?
我们都知道goroutine的在golang中发挥了很大的作用,那么当我们创建一个新的goroutine时,它是怎么一步一步创建的呢?都经历了哪些操作呢?今天我们通过源码来剖析一下创建goroutine都经历了些什么?go version 1.15.6
对goroutine最关键的两个函数是 [newproc()](https://github.com/golang/go/blob/go1.15.6/src/runtime/proc.go#L3535-L3564) 和 [newproc1()](https://github.com/golang/go/blob/go1.15.6/src/runtime/proc.go#L3566-L3674),而 newproc1() 函数是我们最需要关注的。
函数 newproc() 我们先看一个简单的创建goroutine的例子,找出来创建它的函数。
package main func start(a, b, c int64) { _ = a + b + c } func main() { go start(7, 2, 5) } 输出结果:
➜ gotest go tool compile -S main.go "".start STEXT nosplit size=1 args=0x18 locals=0x0 0x0000 00000 (main.go:3) TEXT "".start(SB), NOSPLIT|ABIInternal, $0-24 0x0000 00000 (main.go:3) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x0000 00000 (main.go:3) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x0000 00000 (main.
February 15, 2021
Runtime: 理解Golang中接口interface的底层实现
接口类型是Golang中是一种非常非常常见的数据类型,每个开发人员都很有必要知道它到底是如何使用的,如果了解了它的底层实现就对开发就更有帮助了。
接口的定义 在Golang中 interface 通常是指实现了一 组抽象方法的集合,它提供了一种无侵入式的方式。当你实现了一个接口中指定的所有方法的时候,那么就实现了这个接口,在Golang中对它的实现并不需要 implements 关键字。
有时候我们称这种模型叫做鸭子模型(Duck typing),维基百科对鸭子模型的定义是
”If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.“
翻译过来就是 ”如果它看起来像鸭子,像鸭子一样游泳,像鸭子一样嘎嘎叫,那他就可以认为是鸭子“。
Go 不同版本之间interface的结构可能不太一样,但整体都差不多,这里使用的Go版本为 1.15.6。
数据结构 Go 中 interface 在运行时可分 eface 和 iface 两种数据结构,我们先看一下对它们的定义:
一种是 eface, 表示空接口,指未包含任何方法的接口。如定义一个变量 var x interface{},还有我们经常使用的标准库中的 func Println(a ...interface{}) (n int, err error) {} 都必于 eface 类型,标准库中有许多这类的接口。
另一种是 iface,表示包含至少一个方法的接口。
根据两者的定义可知 iface 是 eface 的一个子集,那么为什么不直接使用eface呢? 主要原因还是为了做一些性能优化。
February 13, 2021
认识Golang中的sysmon监控线程
Go Runtime 在启动程序的时候,会创建一个独立的 M 作为监控线程,称为 sysmon,它是一个系统级的 daemon 线程。这个sysmon 独立于 GPM 之外,也就是说不需要P就可以运行,因此官方工具 go tool trace 是无法追踪分析到此线程( 源码)。sysmon
在程序执行期间 sysmon 每隔 20us~10ms 轮询执行一次( 源码),监控那些长时间运行的 G 任务, 然后设置其可以被强占的标识符,这样别的 Goroutine 就可以抢先进来执行。
// src/runtime/proc.go // forcegcperiod is the maximum time in nanoseconds between garbage // collections. If we go this long without a garbage collection, one // is forced to run. // // This is a variable for testing purposes. It normally doesn't change. var forcegcperiod int64 = 2 * 60 * 1e9 // Always runs without a P, so write barriers are not allowed.
February 11, 2021
g0 特殊的goroutine
在上篇 《golang中G、P、M 和 sched 三者的数据结构》文章中,我们介绍了G、M 和 P 的数据结构,其中M结构体中第一个字段是 g0,这个字段也是一个 goroutine,但和普通的 goroutine 有一些区别,它主要用来实现对 goroutine 进行调度,下面我们将介绍它是如何实现调度goroutine的。
另外还有一个 m0 , 它是一个全局变量,与 g0 的区别如下M0 与 g0的区别
本文主要翻译自 Go: g0, Special Goroutine 一文,有兴趣的可以查阅原文,作者有一系列高质量的文章推荐大家都阅读一遍。ℹ️ 本文基于 Go 1.13。
我们知道在Golang中所有的goroutine的运行都是由调度器来负责管理的,go调度器尝试为所有的goroutine来分配运行时间,当有goroutine被阻塞或终止时,调度器会通过对goroutine 进行调度以此来保证所有CPU都处于忙碌状态,避免有CPU空闲状态浪费时间。
goroutine 切换规则 在此之前我们需要记住一些goroutine切换规则。runtime源码
// src/runtime/stubs.go // mcall switches from the g to the g0 stack and invokes fn(g), // where g is the goroutine that made the call. // mcall saves g's current PC/SP in g->sched so that it can be restored later.
January 26, 2021
Golang环境变量之GODEBUG
GODEBUG 是 golang中一个控制runtime调度变量的变量,其值为一个用逗号隔开的 name=val对列表,常见有以下几个命名变量。
allocfreetrace 设置allocfreetrace = 1会导致对每个分配进行概要分析,并在每个对象的分配上打印堆栈跟踪并释放它们。
clobberfree 设置 clobberfree=1会使垃圾回收器在释放对象的时候,对象里的内存内容可能是错误的。
cgocheck cgo相关。
设置 cgocheck=0 将禁用当包使用cgo非法传递给go指针到非go代码的检查。如果值为1(默认值)会启用检测,但可能会丢失有一些错误。如果设置为2的话,则不会丢失错误。但会使程序变慢。
efence 设置 efence=1会使回收器运行在一个模式。每个对象都在一个唯一的页和地址,且永远也不会被回收。
gccheckmark GC相关。
设置 gccheckmark=1 启用验证垃圾回收器的并发标记,通过在STW时第二个标记阶段来实现,如果在第二阶段的时候,找到一个可达对象,但未找到并发标记,则GC会发生Panic。
gcpacertrace GC相关。
设置 gcpacertrace=1可使gc打印关于并发 pacer 内部状态的信息
gcshrinkstackoff GC相关。
设置 gcshrinkstackoff=1可禁止goroutine移动到小stack。这种模式下,goroutine stack只能增长。简单来说就是一个关闭收缩的开关。
gcstoptheworld GC 相关。
设置 gcstoptheworld=1可禁止并发垃圾回收,使每个gc就是一个事件。设置 gcstoptheworld=2 会在垃圾回收后进行并发扫描。
gctrace GC 相关。
设置 gctrade=1 会使垃圾回收器在每次GC打印单行标准错误,汇总收集的内存量和暂停的时间。格式可能会更换。
如果输出行以(forced)结尾,则说明此GC是通过调用 runtime.GC 来触发的。这个调试变量非常常见。如 GODEBUG=gotrace=1 go run main.go
输出字段意义: gc # @#s #%: #+#+# ms clock, #+#/#/#+# ms cpu, #->#-># MB, # MB goal, # P where the fields are as follows: gc # the GC number, incremented at each GC @#s time in seconds since program start #% percentage of time spent in GC since program start #+.
January 26, 2021
Golang中MemStats的介绍
平时在开发中,有时间需要通过查看内存使用情况来分析程序的性能问题,经常会使用到 MemStats 这个结构体。但平时用到的都是一些最基本的方法,今天我们全面认识一下MemStas。
相关文件为 src/runtime/mstats.go ,本文章里主要是与内存统计相关。
MemStats 结构体 // MemStats记录有关内存分配器的统计信息 type MemStats struct { // General statistics. Alloc uint64 TotalAlloc uint64 Sys uint64 Lookups uint64 Mallocs uint64 Frees uint64 // Heap memory statistics. HeapAlloc uint64 HeapSys uint64 HeapIdle uint64 HeapInuse uint64 HeapReleased uint64 HeapObjects uint64 // Stack memory statistics. StackInuse uint64 StackSys uint64 // Off-heap memory statistics. MSpanInuse uint64 MSpanSys uint64 MCacheInuse uint64 MCacheSys uint64 BuckHashSys uint64 GCSys uint64 OtherSys uint64 // Garbage collector statistics.
January 25, 2021
Golang中Stack的管理
栈的演变 在 Go1.13之前的版本,Golang 栈管理是使用的分段栈(Segment Stacks)机制来实现的,由于sgement stack 存在 热分裂(hot split)的问题,后面版本改为采用连续栈( [Contiguous stacks](https://docs.google.com/document/d/1wAaf1rYoM4S4gtnPh0zOlGzWtrZFQ5suE8qr2sD8uWQ/pub))机制( 说明)。
分段栈(Segment Stack) 分段栈是指开始时只有一个stack,当需要更多的 stack 时,就再去申请一个,然后将多个stack 之间用双向链接连接在一起。当使用完成后,再将无用的 stack 从链接中删除释放内存。segment stack
可以看到这样确实实现了stack 按需增长和收缩,在增加新stack时不需要拷贝原来的数据,系统使用率挺高的。但在一定特别的情况下会存在 热分裂(hot split) 的问题。
当一个 stack 即将用完的时候,任意一个函数都会导致堆栈的扩容,当函数执行完返回后,又要触发堆栈的收缩。如果这个操作是在一个for语句里执行的话,则过多的 malloc 和 free 重复操作将导致系统资源开销非常的大。
如果你对 Stack Frame 不了解的话,可能先阅读一下 Go 语言机制之栈和指针
连续栈(Contiguous stacks) Contiguous stacks 扩容与收缩 连续栈使用了另一种管理机制,每当一个stack 空间不够的时候,直接再申请一个2倍大小的空间,然后再将stack数据拷贝过去,同时修改指向原来stack 的指针到新stack,最后再将旧stack删除。
这种机制可以在当stack 空间快用尽的时候,避免在for语句里频繁触发扩容的问题。也正是官方采用这种机制的原因。
栈的初始化 在上篇文章《 Golang 的底层引导流程/启动顺序》中介绍过,在应用启动时会有一系列的初始化工作,其中就包括对栈的初始化 (源码),调用函数 [stackinit()](https://github.com/golang/go/blob/go1.15.6/src/runtime/stack.go#L158-L170)。
func stackinit() { if _StackCacheSize&_PageMask != 0 { throw("cache size must be a multiple of page size") } for i := range stackpool { stackpool[i].
January 22, 2021
Golang 的底层引导流程/启动顺序
在Golang中,程序的执行入口为 main() 函数,那么底层又是如何工作的呢? 这个问题的答案我们可以在runtime源码找到。对它的解释主要在 [src/runtime/proc.go](https://github.com/golang/go/blob/go1.15.6/src/runtime/proc.go) 文件,下面我们看一下它是如何一步一步开始执行的。go version 1.15.6
在文件头部有一段对 [Goroutine scheduler](https://github.com/golang/go/blob/go1.15.6/src/runtime/proc.go#L19) 的介绍,我们先了解一下。
调度器的工作是分发goroutines到工作线程让其运行。一句话指明了调度器的存在意义,就是指挥协调GPM干活。
主要包含三部分 G 指的是 goroutine M 工作线程,也叫machine P 处理器(逻辑CPU),执行 Go code 的一种资源。这里的Go code 其实就是 goroutine里的代码。
M必须被指派给P去执行 Go code, 但可以被阻塞或通过P进行系统调用。
设计文档 https://golang.org/s/go11sched
再往下会发现一段注释说明
// src/runtime/proc.go // The bootstrap sequence is: // // call osinit // call schedinit // make & queue new G // call runtime·mstart // // The new G calls runtime·main. 不错,这个说的就是我们今天的重点。共分四大阶段, 在第三阶段创建新G是通过调用 [runtime.main()](https://github.com/golang/go/blob/go1.15.6/src/runtime/proc.go#L113-L231) 函数实现的,主要用来创建一个main goroutine,然后再运行 [runtime.mstart](https://github.com/golang/go/blob/go1.15.6/src/runtime/proc.go#L1106-L1148) 并启动m0。整个流程如图所示runtime.
January 21, 2021
golang中G、P、M 和 sched 三者的数据结构
G、P、M 三者是golang实现高并发能的最为重要的概念,runtime 通过 调度器 来实现三者的相互调度执行,通过 p 将用户态的 g 与内核态资源 m 的动态绑定来执行,以减少以前通过频繁创建内核态线程而产生的一系列的性能问题,充分发挥服务器最大有限资源。GPM 协作
调度器的工作是将一个 G(需要执行的代码)、一个 M(代码执行的地方)和一个 P(代码执行所需要的权限和资源)结合起来。
所有的 g、m 和 p 对象都是分配在堆上且永不释放的,所以它们的内存使用是很稳定的。得益于此,runtime 可以在调度器实现中避免写屏障。当一个G执行完成后,可以放入pool中被再次使用,避免重复申请资源。
本节主要通过阅读runtime源码来认识这三个组件到底长的是什么样子,以此加深对 GPM 的理解。go version go1.15.6
理解下文前建议先阅读一下 src/runtime/HACKING.md 文件,中文可阅读 这里,这个文件内容是面向开发者理解runtime的很值得看一看。
本文若没有指定源码文件路径,则默认为 src/runtime/runtime2.go。
G G是英文字母goroutine的缩写,一般称为“协程”,其实这个词还是无法完整表达它的意思的,但这用的人的多了就成了统称。注意它与线程和进程的区别,这个应该很容易理解,每个gopher应该都知道。
每个 Goroutine 对应一个 g 结构体,它有自己的栈内存, G 存储 Goroutine 的运行堆栈、状态以及任务函数,可重复用,
Goroutine数据结构位于 src/runtime/runtime2.go 文件,注意此文件里有太多重要的底层数据结构,对于我们理解底层runtime非常的重要,建议大量多看看。不需要记住每一个数据结构,但需要的时候要能第一时间想到在哪里查找。
当一个 goroutine 退出时,g 对象会被放到一个空闲的 g 对象池中以用于后续的 goroutine 的使用, 以减少内存分配开销。
Goroutine 字段非常的多,我们这里分段来理解
type g struct { // Stack parameters. // stack describes the actual stack memory: [stack.
January 18, 2021
Runtime: Golang中channel实现原理源码分析
channel是golang中特有的一种数据结构,通常与goroutine一起使用,下面我们就介绍一下这种数据结构。
channel数据结构 channel 是Golang 中最重要的一个数据结构,源码里对应的结构体是hchan,当我们创建一个channel 的时候,实际上是创建了一个hchan结构体。
hchan结构体 // src/runtime/chan.go type hchan struct { qcount uint // total data in the queue dataqsiz uint // size of the circular queue buf unsafe.Pointer // points to an array of dataqsiz elements elemsize uint16 closed uint32 elemtype *_type // element type sendx uint // send index recvx uint // receive index recvq waitq // list of recv waiters sendq waitq // list of send waiters // lock protects all fields in hchan, as well as several // fields in sudogs blocked on this channel.
January 11, 2021
Runtime:源码解析Golang 的map实现原理
go version 1.15.6
map作为一种常见的 key-value 数据结构,不同语言的实现原理基本差不多。首先在系统里分配一段连接的内存地址作为数组,然后通过对map键进行hash算法(最终将键转换成了一个整型数字)定位到不同的桶bucket(数组的索引位置),然后将值存储到对应的bucket里
理想的情况下是一个bucket存储一个值,即数组的形式,时间复杂度为O(1)。
如果存在键值碰撞的话,可以通过 链表法 或者 开放寻址法 来解决。
链表法
开放寻址法
对于开放寻址法有多种算法,常见的有线性探测法,线性补偿探测法,随机探测法等,这里不再介绍。
map基本数据结构 hmap结构体 map的核心数据结构定义在 /runtime/map.go
// A header for a Go map. type hmap struct { // Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go. // Make sure this stays in sync with the compiler's definition. count int // # live cells == size of map. Must be first (used by len() builtin) flags uint8 B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items) noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details hash0 uint32 // hash seed buckets unsafe.
December 26, 2020
Golang并发模式之扇入FAN-IN和扇出FAN-OUT
在现实世界中,经常有一些工作是属于流水线类型的,它们每一个步骤都是紧密关联的,第一步先做什么,再做做么,最后做什么。特别是制造业这个行业,基本全是流水线生产车间。在我们开发中也经常遇到这类的业务场景。
假如我们有个流水线共分三个步骤,分别是 job1、job2和job3。代码: https://play.golang.org/p/e7ZlP9ofXB3
package main import ( "fmt" "time" ) func job1(count int) <-chan int { outCh := make(chan int, 2) go func() { defer close(outCh) for i := 0; i < count; i++ { time.Sleep(time.Second) fmt.Println("job1 finish:", 1) outCh <- 1 } }() return outCh } func job2(inCh <-chan int) <-chan int { outCh := make(chan int, 2) go func() { defer close(outCh) for val := range inCh { // 耗时2秒 time.
December 24, 2020
重新认识Golang中的空结构体
认识空结构体 低层实现原理 空结构体之内存对齐 应用场景 在golang中,如果我们想实现一个set集合的话,一般会使用map来实现,其中将set的值作为map的键,对于map的值一般使用一个空结构体来实现,当然对map值也可以使用一个bool类型或者数字类型等,只要符合一个键值对应关系即可。但我们一般推荐使用struct{}来实现,为什么呢?
package main import "fmt" func main() { m := make(map[int]struct{}) m[1] = struct{}{} m[2] = struct{}{} if _, ok := m[1]; ok { fmt.Println("exists") } } 上面这段代码是一个很简单的使用map实现的set功能,这里是采用空结构体struct{}来实现。
在分析为什么使用struct{}以前,我看先认识一个struct。
认识空结构体 struct 我们先看一个这段代码
package main import ( "fmt" "unsafe" ) type emptyStruct struct{} func main() { a := struct{}{} b := struct{}{} c := emptyStruct{} fmt.Println(a) fmt.Printf("%pn", &a) fmt.Printf("%pn", &b) fmt.Printf("%pn", &c) fmt.Println(a == b) fmt.Println(unsafe.Sizeof(a)) } {} // 值 0x586a00 // a 内存地址 0x586a00 // b 内存地址, 同a一样 0x586a00 // c 内存地址,别名类型变量,同a一样 true // 丙个结构体是否相等,很显示,上面打印的是同一个内存地址 0 // 占用内存大小 从打印结果里我们可以得出以下结论
December 17, 2020
Golang中的内存重排(Memory Reordering)
什么是内存重排 内存重排指的是内存的读/写指令重排。
为什么要内存重排 为了提升程序执行效率,减少一些IO操作,一些硬件或者编译器会对程序进行一些指令优化,优化后的结果可能会导致程序编码时的顺序与代码编译后的先后顺序不一致。
就拿做饭场景来说吧,是先蒸米还是先炒菜,这两者是没有冲突的,编译器在编译时有可能与你要求的顺序不一样。
编译器重排 如下面这段代码
X = 0 for i in range(100): X = 1 print X 要实现打印100次1,很显示在for里面每次都执行X=1语句有些浪费资源,如果将初始变量值修改为1,是不是要快的多。编译器也分析到了这一点,于是在编译时对代码做了以下优化
X = 1 for i in range(100): print X 最终输出结果是一样的,两段代码功能也一样。
但是如果此时有另一个线程里执行了一个 X=0 的赋值语句的话(两个线程同时运行),那么输出结果就可能与我们想要的不一样了。
优化前情况:第一个线程执行到了第3次print X 后,第二个线程执行了X=0,把X 的值进行了修改,结果就有可能是1110 1111(在线程执行第5次时,重新执行了X=1,而且之后一直都是 1)。这就有问题了。
优化后情况:按上面的逻辑来执行的话,结果就是11100000…, 后面全是0,再也没有机会将X贬值为1了
由此我们可以得出一个结果,在多线程下,是无法保证编译前后的代码功能是“待价”的。
所以要开发时,这一点我们一定要小心。搞不好在单线程运行没有问题的程序在多线程下会出现各种各样的问题。
每当我们提到内存重排的时候,其实真实在讨论的主题是 内存屏障Memory barrier。指令重排无法逾越内存屏障。
CPU 重排 参考: https://www.w3xue.com/exp/article/20196/40758.html
CPU 架构 Figure 1. CPU ArchitectureFigure 2. Store BufferFigure 3. MESI Protocol MESI 可参考 https://mp.weixin.qq.com/s/vnm9yztpfYA4w-IM6XqyIA
相关话题 锁、内存屏障与缓存一致性
memory barrier
[译] 什么是缓存 false sharing 以及如何解决(Golang 示例) CPU缓存体系对Go程序的影响
November 19, 2020
Golang中的并发原语 Singleflight
在Golang中有一个并发原语是 Singleflight,好像知道的开发者并不多。其中著名的 https://github.com/golang/groupcache 就用到了这个并发原语。
Golang版本 go1.15.5
相关知识点 map、Mutex、channel、
使用场景 一般用在对指定资源频繁操作的情况下,如高并发下的“缓存击穿”问题。
缓存击穿:一个存在的key,在缓存过期的瞬间,同时有大量的请求过来,造成所有请求都去DB读取数据,这些请求都会击穿缓存到DB,造成瞬时DB请求量大、压力瞬间骤增,导致数据库负载过高,影响整个系统正常运行。(缓存击穿不同于 缓存雪崩 和 缓存穿透)
怎么理解这个原语呢,简单的讲就是将对同一个资源的多个请求合并为一个请求。
举例说明,假如当有10万个请求来获取同一个key的值的时候,正常情况下会执行10万次get操作。而使用singleflight并发语后,只需要首次的地个请求执行一次get操作就可以了,其它请求再过来时,只需要只需要等待即可。待执行结果返回后,再把结果分别返回给等待中的请求,每个请求再返回给客户端,由此看看,在一定的高并发场景下可以大大减少系统的负载,节省大量的资源。
注意这个与 sync.Once是不一样的,sync.Once 是全局只能有一个,但本并发原语则是根据key来划分的,并且可以根据需求来决定什么情况下共用一个。
实现原理 主要使用 Mutext 和 Map 来实现,以 key 为键,值为 *call。每个*call中存储有一个请求 chans 字段,用来存储所有请求此key的客户端,等有返回结果的时候,再从chans字段中读取出来,分别写入即可。
源文件为 /src/internal/singleflight/singleflight.go
Singleflight 数据结构如下
Do() 这个方法是一个执行函数并返回执行结果 参数 key 要请求的key,多个请求可能请求的是同一个key,同时也只有一个函数在执行 fn 对应执行函数,此函数有三个返回值v, err, shared。其中shared表示当前返回结果是否为多个请求结果 DoChan() 类型与Do()方法,但返回的是个 ch 类型,等函数 fn 执行后,可以通过读取返回的ch来获取函数结果 Forget() 在官方库internal/singleflight/singleflight.go中这个名字是ForgetUnshared, 告诉 Group 忘记请求的这个key,下次再请求时,直接当作新的key来处理就可以了。其实就是将这个key从map中删除,后续再有这个key的操作的话,即视为新一轮的处理逻辑。 其中Do() 和 DoChan() 的功能一样,只是获取数据的方式有所区别,开发者可以根据自己的情况来选择使用哪一个。另外还包含一个由Do()方法调用的私有方法 doCall(),直接执行key处理方法的函数
func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) {.
September 19, 2020
Golang开发中中使用GitHub私有仓库
私有仓库地址为
github.com/cfanbo/websocket 一、设置私有环境变量 GOPRIVATE $ go env -w GOPRIVATE=github.com/cfanbo/websocket 对于为什么需要设置 GOPRIMARY 变量,可以参考 这里
对于GOPRIVATE值级别分为仓库级别和账号级别。
如果只有一个仓库,直接设置为仓库地址即可。如果有多个私有仓库的话,使用”,”分开,都在这个账号下,也可以将值设置为账号级别,这样账号下的所有私有仓库都可以正常访问。如 http://github.com/cfanbo
如果不想每次都重新设置,我们也可以利用通配符,例如:
$ go env -w GOPRIVATE="*.example.com" 这样子设置的话,所有模块路径为 example.com 的子域名(例如:git.example.com)都将不经过 Go module proxy 和 Go checksum database,需要注意的是不包括 example.com 本身。
国内用户访问仓库建议设置 GORPOXY为 https://proxy.golang.org,direct
二、设置凭证 使用私有仓库一定要绕不开权限设置这一步。访问仓库来常见的有两种方式,分别为SSH和 Https 。对于私有仓库来说,ssh可以设置rsa私钥来访问,https这种则可以使用用户名和密码,一般通过命令行访问的时候,会自动提示用户输入这些信息。
对于权限控制这一块可参考官方文档 。其实在官方文档里还提供了第三种访问仓库的方式,那就是 Personal access token,简称 PAT, 这种 Token 是专门为api调用提供的,常见于自动化工作流中,如 CICD场景。
这里我们就利用PAT 来实现
在Github.com 网站生成 Personal access tokens,新手可参考官方教程文档 本地配置token凭证 $ git config --global url."https://${username}:${access_token}@github.com".insteadOf / "https://github.com" 如果你在使用Github Actions部署时,遇到无法读取版本号问题,需要改写成 git config –global url.
May 27, 2020
golang中几种对goroutine的控制方法
我们先看一段代码
func listen() { ticker := time.NewTicker(time.Second) for { select { case <-ticker.C: fmt.Println(time.Now()) } } } func main() { go listen() time.Sleep(time.Second * 5) fmt.Println("main exit") } 非常简单的一个goroutine用法,想必每个gopher都看过的。
不过在实际生产中,我们几乎看不到这种用法的的身影,原因很简单,我们无法实现对goroutine的控制,而一般业务中我们需要根据不同情况对goroutine进行各种操作。
要实现对goroutine的控制,一般有以下两种。
一、手动发送goroutine控制信号 这里我们发送一个退出goroutine的信号。
// listen 利用只读chan控制goroutine的退出 func listen(ch <-chan bool) { ticker := time.NewTicker(time.Second) for { select { case <-ticker.C: fmt.Println(time.Now()) case <-ch: fmt.Println("goroutine exit") return } } } func main() { // 声明一个控制goroutine退出的chan ch := make(chan bool, 1) go listen(ch) // 只写chan func(ch chan<- bool) { time.
May 3, 2020
Golang遍历切片删除元素引起恐慌问题
删除一个切片的部分元素, 告知切片操作:Golang遍历切片恐慌时删除元素
问题描述 代码( 演示代码):
package main import ( "fmt" ) func main() { slice := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} for i, value := range slice { if value%3 == 0 { // remove 3, 6, 9 slice = append(slice[:i], slice[i+1:]...) } } fmt.Printf("%v", slice) } 运行结果
panic: runtime error: slice bounds out of range [8:6] goroutine 1 [running]: main.main() /tmp/sandbox2635969259/prog.go:11 +0x212 Program exited. 解决办法: 以下是网友想到的几种办法
April 30, 2020
Golang中select用法导致CPU占用100%的问题分析
上一节( golang中有关select的几个知识点)中介绍了一些对于select{}的一些用法,今天介绍一下有关select在 for语句 中由于使用不当引起的CPU占用100% 的案例。
先看代码
package main import ( "fmt" "time" ) func main() { ch := make(chan int, 10) // 读取chan go func() { for { select { case i := <-ch: // 只读取15次chan fmt.Println(i) default: // 读取15次chan以后的操作一直在这个空语句无任何IO操作的default条件里死循环,无法出让P,以保证一个GPM关系。 // 而如果无default条件的话,则系统当读取完15次chan后,当前goroutine会发生 chan IO 阻塞, Go调度器根据GPM的调度关系,会将当前执行关系中的G切换出去,再从LRQ队列中取一个新的G,重新组成一个GPM继续执行,以实现合理利用计算机资源,提高GO的高并发性能 } } }() // 写入10个值到chan for i := 0; i < 15; i++ { ch <- i } // 模拟程序效果使用 time.Sleep(time.Minute) } 实现功能 通过操作chan来实现消费者
April 21, 2020
基于 GitHub Actions 实现 Golang 项目的自动构建部署
前几天 GitHub官网宣布 GitHub 的所有核心功能对所有人都免费开放,不得不说自从微软收购了GitHub后,确实带来了一些很大的改变。
以前有些项目考虑到协作关系的原因,虽然放在github上面,但对于一些项目的持续构建和部署一般是通过自行抢建Travis CI、jenkins等系统来实现。虽然去年推出了Actions用来代替它类三方系统,但感觉着还是不方便,必须有些核心功能无法使用,此消息的发布很有可能将这种格局打破。
本篇教程将介绍使用github的系列产品来实现项目的发布,构建,测试和部署,当然这仅仅是一个非常小的示例,有些地方后期可能会有更好的瞿恩方案。
GitHub Actions 是一款持续集成工具,包括clone代码,代码构建,程序测试和项目发布等一系列操作。更多内容参考:
如果你对CI/CD不了解的话,建议先找些文档看看。
项目源文件见
GitHub Actions 术语 GitHub Actions 相关的术语。
(1)workflow (工作流程):持续集成一次运行的过程,就是一个 workflow。
(2)job (任务):一个 workflow 由一个或多个 jobs 构成,含义是一次持续集成的运行,可以完成多个任务。
(3)step(步骤):每个 job 由多个 step 构成,一步步完成。
(4)action (动作):每个 step 可以依次执行一个或多个命令(action)。
一、创建workflow 文件 在项目里创建一个workflow文件,文件格式为yaml类型。文件名可以随意起,文件后缀可以为yml 或 .yaml, 这里我们创建文件 .github/workflows/deploy.yaml,注意这里的路径。
如果你的仓库中有项目文件的话,当你点击“Actions”时,系统会自动根据你的开发语言推荐一个常用的actions,在页面的右侧也会推荐一些相应的actions.
二、在部署服务器上生成部署用户密钥 部署时需要用到用户的私钥,所以先登录到部署服务器获取私钥,这里为了方便,单独创建了一对公钥和公钥,
$ cd ~/.ssh $ ssh-keygen Generating public/private rsa key pair. Enter file in which to save the key (/root/.ssh/id_rsa): /root/.ssh/id_rsa_actions Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in /root/.
March 27, 2020
Golang中的限速器 time/rate
在高并发的系统中,限流已作为必不可少的功能,而常见的限流算法有:计数器、滑动窗口、令牌桶、漏斗(漏桶)。其中滑动窗口算法、令牌桶和漏斗算法应用最为广泛。
常见限流算法 这里不再对 计数器算法 和 滑动窗口 算法一一介绍,有兴趣的同学可以参考其它相关文章。
漏斗算法 漏斗算法很容易理解,它就像有一个漏斗容器一样,漏斗上面一直往容器里倒水(请求),漏斗下方以固定速率一直流出(消费)。如果漏斗容器满的情况下,再倒入的水就会溢出,此时表示新的请求将被丢弃。可以看到这种算法在应对大的突发流量时,会造成部分请求弃用丢失。
可以看出漏斗算法能强行限制数据的传输速率。漏斗算法
令牌桶算法 从某种意义上来说,令牌算法是对漏斗算法的一种改进。对于很多应用场景来说,除了要求能够限制数据的平均传输速率外,还要求允许某种程度的突发情况。这时候漏桶算法可能就不合适了,令牌桶算法更为适合。
令牌桶算法是指一个固定大小的桶,可以存放的令牌的最大个数也是固定的。此算法以一种固定速率不断的往桶中存放令牌,而每次请求调用前必须先从桶中获取令牌才可以。否则进行拒绝或等待,直到获取到有效令牌为止。如果桶内的令牌数量已达到桶的最大允许上限的话,则丢弃令牌。
Golang中的限制算法 Golang标准库中的限制算法是基于令牌桶算法(Token Bucket) 实现的,库名为golang.org/x/time/rate
对于限流器的消费方式有三种,分别为 Allow()、 Wait()和 Reserve()。前两种内部调用的都是Reserve() ,每个都对应一个XXXN()的方法。如Allow()是AllowN(t, 1)的简写方式。
结构体 type Limiter struct { limit Limit burst int mu sync.Mutex tokens float64 // last is the last time the limiter's tokens field was updated last time.Time // lastEvent is the latest time of a rate-limited event (past or future) lastEvent time.Time } 主要用来限速控制并发事件,采用令牌池算法实现。
创建限速器 使用 NewLimiter(r Limit, b int) 函数创建限速器,令牌桶容量为b。初始化状态下桶是满的,即桶里装有b 个令牌,以后再以每秒往里面填充 r 个令牌。
March 19, 2020
Golang中的两个定时器 ticker 和 timer
Golang中time包有两个定时器,分别为 ticker 和 timer。两者都可以实现定时功能,但各自都有自己的使用场景。
Ticker定时器 package main import ( "fmt" "time" ) func main() { // Ticker 包含一个通道字段C,每隔时间段 d 就向该通道发送当时系统时间。 // 它会调整时间间隔或者丢弃 tick 信息以适应反应慢的接收者。 // 如果d <= 0会触发panic。关闭该 Ticker 可以释放相关资源。 ticker1 := time.NewTicker(5 * time.Second) // 一定要调用Stop(),回收资源 defer ticker1.Stop() go func(t *time.Ticker) { for { // 每5秒中从chan t.C 中读取一次 <-t.C fmt.Println("Ticker:", time.Now().Format("2006-01-02 15:04:05")) } }(ticker1) time.Sleep(30 * time.Second) fmt.Println("ok") } 执行结果
开始时间: 2020-03-19 17:49:41 Ticker: 2020-03-19 17:49:46 Ticker: 2020-03-19 17:49:51 Ticker: 2020-03-19 17:49:56 Ticker: 2020-03-19 17:50:01 Ticker: 2020-03-19 17:50:06 结束时间: 2020-03-19 17:50:11 ok 可以看到每次执行的时间间隔都是一样的。
January 18, 2020
Golang中关于defer语句理解的一道题
示例 我们先看一下源代码
package main import "fmt" func f(n int) (r int) { defer func() { r += n recover() }() var fc func() defer fc() fc = func() { r += 2 } return n + 1 } func main() { fmt.Println(f(3)) } 大家感觉着打印的值是多少呢?5、9还是7?执行完以后发现是7。好像与多数理解的有些出入,为什么是7,而不是9呢。下面我们来分析一下。
问题分析 对于defer执行的顺序是FIFO这一点都很清楚,我们只需要看搞懂f()函数的执行顺序就行了。
执行顺序为:
注册第1个defer 函数, 这里为匿名函数,函数体为 “func() { r += n recover() }()”,内部对应一个函数指针。这里延时函数所有相关的操作一步完成。 注册第2个defer函数,函数名为fc(),无函数体, 函数指针为nil(也有可能指针不会空,但指针指向的内容非函数体类型)。由于只是注册操作还未执行,所以并不会产生错误,继续执行。 对上面声明的函数进行函数体定义 执行return 语句 处理defer语句,根据FIFO原则,首先执行第二个函数fc(),发现函数指针为nil,此时会抛出一个恐慌,并继续操作。 执行第一个defer函数,对r值进行操作,同时处理恐慌。由于是最后一个defer语句,所以直接将r的值真正返回 可以看到上面第2、3步骤,是先注册的defer函数(函数不存在,所以指针为nil),再进行的函数体定义,导致第二个defer延时函数执行时产生恐慌,后面对函数体的单独定义没有任何意义,大家可以将此函数删除再次运行会发生没有任何问题,直到第一个defer函数对此处理并返回r值结束。
如果打印恐慌错误信息的话,会输出“runtime error: invalid memory address or nil pointer dereference”。
January 14, 2020
golang中有关select的几个知识点
golang中的select语句格式如下
select { case <-ch1: // 如果从 ch1 信道成功接收数据,则执行该分支代码 case ch2 <- 1: // 如果成功向 ch2 信道成功发送数据,则执行该分支代码 default: // 如果上面都没有成功,则进入 default 分支处理流程 } 可以看到select的语法结构有点类似于switch,但又有些不同。
select里的case后面并不带判断条件,而是一个信道的操作,不同于switch里的case,对于从其它语言转过来的开发者来说有些需要特别注意的地方。
golang 的 select 就是监听 IO 操作,当 IO 操作发生时,触发相应的动作每个case语句里必须是一个IO操作,确切的说,应该是一个面向channel的IO操作。
注:Go 语言的 select 语句借鉴自 Unix 的 select() 函数,在 Unix 中,可以通过调用 select() 函数来监控一系列的文件句柄,一旦其中一个文件句柄发生了 IO 动作,该 select() 调用就会被返回(C 语言中就是这么做的),后来该机制也被用于实现高并发的 Socket 服务器程序。Go 语言直接在语言级别支持 select 关键字,用于处理并发编程中通道之间异步 IO 通信问题。
注意:如果 ch1 或者 ch2 信道都阻塞的话,就会立即进入 default 分支,并不会阻塞。但是如果没有 default 语句,则会阻塞直到某个信道操作成功为止。
知识点 select语句只能用于信道的读写操作 select中的case条件(非阻塞)是并发执行的,select会选择先操作成功的那个case条件去执行,如果多个同时返回,则随机选择一个执行,此时将无法保证执行顺序。对于阻塞的case语句会直到其中有信道可以操作,如果有多个信道可操作,会随机选择其中一个 case 执行 对于case条件语句中,如果存在信道值为nil的读写操作,则该分支将被忽略,可以理解为从select语句中删除了这个case语句 如果有超时条件语句,判断逻辑为如果在这个时间段内一直没有满足条件的case,则执行这个超时case。如果此段时间内出现了可操作的case,则直接执行这个case。一般用超时语句代替了default语句 对于空的select{},会引起死锁 对于for中的select{}, 也有可能会引起cpu占用过高的问题 下面列出每种情况的示例代码
January 11, 2020
golang中的sync.Pool对象缓存
参考文章 Golang 的 协程调度机制 与 GOMAXPROCS 性能调优 深入Golang之sync.Pool详解 golang sync.Pool 分析 [译] Go: 理解 Sync.Pool 的设计 视频 sync.pool对象缓存 知识点 Pool只是一个缓存,一个缓存,一个缓存。由于生命周期受GC的影响,一定不要用于数据库连接池这类的应用场景,它只是一个缓存。 golang1.13版本对 Pool 进行了优化,结构体添加了两个字段 victim 和 victimSize。 适应于通过复用,降低复杂对象的创建和GC代价的场景 因为init()的时候会注册一个PoolCleanup函数,他会在gc时清除掉sync.Pool中的所有的缓存的对象。所以每个sync.Pool的生命周期为两次GC中间时段才有效,可以手动进行gc操作 runtime.GC() 由于要保证协程安全,所以会有锁的开销 每个Pool都有一个私有池(协程安全)和共享池(协程不安全),其中私有池只有存放一个值。 每次Get()时会先从当前P的私有池private中获取( 类似MPG模型中的G) 如果获取失败,再从当前P的共享池share中获取 如果仍失败,则从其它P中共享池中拿一个,需要加锁保证协程安全 如果还失败,则表示所有P中的池(也有可能只是共享池)都为空,则需要New()一个并直接返回(此时不会被放入池中) 每次取值出来后,会从原来存储的地方将该值删除。
January 11, 2020
golang 的编程模式之“功能选项”
最近在用go重构iot中的一个服务时,发现库 [email protected] 在初始化消费客户端实现时,实现的极其优雅,代码见 https://github.com/apache/rocketmq-client-go/blob/v2.0.0-rc1/examples/consumer/simple/main.go#L32
c, _ := rocketmq.NewPushConsumer( consumer.WithGroupName("testGroup"), consumer.WithNameServer([]string{"127.0.0.1:9876"}), ) err := c.Subscribe("test", consumer.MessageSelector{}, func(ctx context.Context, msgs ...*primitive.MessageExt) (consumer.ConsumeResult, error) { for i := range msgs { fmt.Printf("subscribe callback: %v n", msgs[i]) } return consumer.ConsumeSuccess, nil }) 这里创建结构体 rocketmq.NewPushConsumer() 的时候,与我们平时的写法不同并没有写死结构体的字段名和值,而是每个属性都使用了一个函数来实现了,同时也不用考虑属性字段的位置关系,比起以前写kv键值对的方法实在是太灵活了。 我们再看一下其中一个 WithGroupName() 函数的实现方法
func WithGroupName(group string) Option { return func(opts *consumerOptions) { if group == "" { return } opts.GroupName = group } } 传递的参数为consumerOptions指针类型,这里用到了一个匿名函数,返回的类型为Option(定义 *type Option func(consumerOptions) )。看到这里大概明白实现原理了吧。 为了确认我们的判断,我们再看一下 rocketmq.
November 8, 2019
Golang中的goroutine泄漏问题
goroutine作为Go中开发语言中的一大利器,在高并发中发挥着无法忽略的作用。但东西虽好,真正做到用好还是有一些要注意的地方,特别是对于刚刚接触这门开发语言的新手来说,稍有不慎,就极有可能导致goroutine 泄漏。
什么是goroutine Leak goroutine leak 的意思是go协程泄漏,那么什么又是协程泄漏呢?我们知道每次使用go关键字开启一个gorountine任务,经过一段时间的运行,最终是会结束,从而进行系统资源的释放回收。而如果由于操作不当导致一些goroutine一直处于阻塞状态或者永远运行中,永远也不会结束,这就必定会一直占用系统资源。最球的情况下是随着系统运行,一直在创建此类goroutine,那么最终结果就是程序崩溃或者系统崩溃。这种情况我们一般称为goroutine leak。
出现的问题 先看一段代码:
package main import ( "fmt" "math/rand" "runtime" "time" ) func query() int { n := rand.Intn(100) time.Sleep(time.Duration(n) * time.Millisecond) return n } // 每次执行此函数,都会导致有两个goroutine处于阻塞状态 func queryAll() int { ch := make(chan int) go func() { ch <- query() }() go func() { ch <- query() }() go func() { ch <- query() }() // <-ch // <-ch return <-ch } func main() { // 每次循环都会泄漏两个goroutine for i := 0; i < 4; i++ { queryAll() // main()也是一个主groutine fmt.
July 11, 2019
golang内存对齐(进阶必看)
先看一个结构体
// 写法一 type T1 struct { a int8 b int64 c int16 } // 写法二 type T2 struct { a int8 c int16 b int64 } 对于这两个结构体,都有a、b、c三个定义完全一样的字段,只是在定义结构体的时候字段顺序不一样而已,那么两种写法有什么影响吗?
对于新手来说,感觉着没有什么区别的,只是一个书写顺序不同而已,但对于go编译器来说,则有着很大的区别,特别是在不同架构上(32位/64位)的编译器,在一定程度上对内存的使用大小和执行效率有着一定的不同。这里的主要知识点就是golang语言中的内存对齐概念(alignment guarantee),
类型的尺寸和结构体字节填充(structure padding) Go白皮书只对以下种类的类型的尺寸进行了明确规定。
类型种类 尺寸(字节数) ------ ------ byte, uint8, int8 1 uint16, int16 2 uint32, int32, float32 4 uint64, int64 8 float64, complex64 8 complex128 16 uint, int 取决于编译器实现。通常在 32位架构上为4,在64位 架构上为8。 uintptr 取决于编译器实现。但必须 能够存下任一个内存地址。 Go白皮书没有对其它种类的类型的尺寸最初明确规定。 请阅读值复制成本一文来获取标准编译器使用的各种其它类型的尺寸。
标准编译器(和gccgo编译器)将确保一个类型的尺寸为此类型的对齐保证的倍数。
为了满足上一节中规定的地址对齐保证要求,Go编译器可能会在结构体的相邻字段之间填充一些字节。 这使得一个结构体类型的尺寸并非等于它的各个字段类型尺寸的简单相加之和。
下面是一个展示了一些字节是如何填充到一个结构体中的例子。 首先,从上面的描述中,我们已得知(对于标准编译器来说):
March 23, 2019
goroutine和线程区别
从调度上看,goroutine的调度开销远远小于线程调度开销。
OS的线程由OS内核调度,每隔几毫秒,一个硬件时钟中断发到CPU,CPU调用一个调度器内核函数。这个函数暂停当前正在运行的线程,把他的寄存器信息保存到内存中(暂时保存线程状态),查看线程列表并决定接下来运行哪一个线程,再从内存中恢复线程的注册表信息,最后继续执行选中的线程。这种线程切换需要一个完整的上下文切换:即保存一个线程的状态到内存,再恢复另外一个线程的状态,最后更新调度器的数据结构。某种意义上,这种操作还是很慢的。OS 线程调度器
Go运行的时候包涵一个自己的调度器,这个调度器使用一个称为一个M:N调度技术,m个goroutine到n个os线程(可以用GOMAXPROCS来控制n的数量),Go的调度器不是由硬件时钟来定期触发的,而是由特定的go语言结构来触发的,他不需要切换到内核语境,所以调度一个goroutine比调度一个线程的成本低很多。
从栈空间上,goroutine的栈空间更加动态灵活。
每个OS的线程都有一个固定大小的栈内存,通常是2MB,栈内存用于保存在其他函数调用期间哪些正在执行或者临时暂停的函数的局部变量。这个固定的栈大小,如果对于goroutine来说,可能是一种巨大的浪费。作为对比,goroutine在生命周期开始只有一个很小的栈,典型情况是2KB, 在go程序中,一次创建十万左右的goroutine也不罕见(2KB*100,000=200MB)。而且goroutine的栈不是固定大小,它可以按需增大和缩小,最大限制可以到1GB。
参考: https://time.geekbang.org/course/detail/160-86799
goroutine没有一个特定的标识。
在大部分支持多线程的操作系统和编程语言中,线程有一个独特的标识,通常是一个整数或者指针,这个特性可以让我们构建一个线程的局部存储,本质是一个全局的map,以线程的标识作为键,这样每个线程可以独立使用这个map存储和获取值,不受其他线程干扰。
goroutine中没有可供程序员访问的标识,原因是一种纯函数的理念,不希望滥用线程局部存储导致一个不健康的超距作用,即函数的行为不仅取决于它的参数,还取决于运行它的线程标识。
October 20, 2018
Golang中struct结构体的的值方法和指针方法
推荐:Go的方法集详解(360云计算)
平时我们在写struct的时候,经常会用到一些方法,有些方法是我们熟悉的普通方法,在golang中我们称之为值方法,而另一种则是指针方法。
type Person struct { Firstname string Lastname string Age uint8 } // 值方法 func (p Person) show() { fmt.Println(p.Firstname) } // 指针方法 func (p *Person) show2() { fmt.Println(p.Firstname) } 可以看到所谓的值方法与指针方法在编写的时候,只是有无*****号的区别,这个*就是指针的意思。
那么用法又有何不同呢?
// 值方法 func (p Person) setFirstName(name string) { p.Firstname = name } // 指针方法 func (p *Person) setFirstName2(name string) { p.Firstname = name } func main() { p := Person{"sun", "xingfang", 30} //不一致的情况 p.show() // sun 修改前 p.
October 19, 2018
Golang中的unsafe.Sizeof()简述
测试环境: 系统 win7 64位 go version: go1.10 windows/amd64
我们先看一下代码的输出
package main import "unsafe" func main() { // string str1 := "abc" println("string1:", unsafe.Sizeof(str1)) // 16 str2 := "abcdef" println("string2:", unsafe.Sizeof(str2)) // 16 // 数组 arr1 := [...]int{1, 2, 3, 4} println("array1:", unsafe.Sizeof(arr1)) // 32 = 8 * 4 arr2 := [...]int{1, 2, 3, 4, 5} println("array2:", unsafe.Sizeof(arr2)) // 40 = 8 * 5 // slice 好多人分不清切片和数组的写法区别,其实只要记住[]中间是空的就是切片,反之则是数组即可 slice1 := []int{1, 2, 3, 4} println("slice1:", unsafe.
October 9, 2018
Golang中的调度器
golang实现的协程调度器,其实就是在维护一个G、P、M三者间关系的队列。
介绍(Introduction) ——————— Go 1.1最大的特色之一就是这个新的调度器,由Dmitry Vyukov贡献。新调度器让并行的Go程序获得了一个动态的性能增长,针对它我不能再做点更好的工作了,我觉得我还是为它写点什么吧。
这篇博客里面大多数东西都已经被包含在了[原始设计文档]( https://docs.google.com/document/d/1TTj4T2JO42uD5ID9e89oa0sLKhJYD0Y_kqxDv3I3XMw)中了,这个文档的内容相当广泛,但是过于技术化了。
关于新调度器,你所需要知道的都在那个设计文档中,但是我这篇博客有图片,所以更加清晰易懂。
带调度器的Go runtime需要什么?(What does the Go runtime need with a scheduler?) ——————————————————————————- 但是在我们开始看新调度器之前,我们需要理解为什么需要调度器。为什么既然操作系统能为我们调度线程了,我们又创造了一个用户空间调度器?
POSIX线程API是对现有Unix进程模型的一个非常大的逻辑扩展,而且线程获得了非常多的跟进程相同的控制。比如,线程有它自己的信号掩码,线程能够被赋予CPU affinity功能(就是指定线程只能在某个CPU上运行),线程能被添加到cgroups中,线程所用到的资源也可以被查询到。所有的这些控制增大了Go程序使用goroutine时根本不需要的特性(features)的开销,当你的程序有100,000个线程的时候,这些开销会急剧增长。
另外一个问题是,基于Go模型,操作系统不能给出特别好的决策。比如,当运行一次垃圾收集的时候,Go的垃圾收集器要求所有线程都被停止而且要求内存要处于一致状态(consistent state)。这个涉及到要等待全部运行时线程(running threads)到达一个点(point),我们事先知道在这个地方内存是一致的。
当很多被调度的线程分散在随机的点(random point)上的时候,结果就是你不得不等待他们中的大多数到达一致状态。Go调度器能够作出这样的决策,就是只在内存保持一致的点上进行调度。这就意味着,当我们为垃圾收集而停止的时候,我们只须等待在一个CPU核(CPU core)上处于活跃运行状态的线程即可。
来看看里面的各个角色(Our Cast of Characters) —————————————– 目前有三个常见的线程模型。一个是N:1的,即多个用户空间线程运行在一个OS线程M上。这个模型可以很快的进行上下文切换,但是不能利用多核系统(multi-core systems)的优势。另一个模型是1:1的,即可执行程序的一个线程匹配一个OS线程M。这个模型能够利用机器上的所有核心的优势,但是上下文切换非常慢,因为它不得不陷入OS(trap through the OS)。
Go试图通过M:N的调度器去获取这两个世界的全部优势。它在任意数目的OS线程M上调用任意数目的goroutines G。你可以快速进行上下文切换(P角色),并且还能利用你系统上所有的核心的优势。这个模型主要的缺点是它增加了调度器的复杂性。
为了完成调度任务,Go调度器使用了三个实体:
[
三角形M 表示OS线程,`它是由OS管理的可执行程序的一个线程`,就是平常提到的操作系统中的线程,而且工作起来特别像你的标准POSIX线程。在运行时代码里,它被成为M,即机器(machine)。
圆形G 表示一个goroutine。它包括栈、指令指针以及对于调用goroutines很重要的其它信息,比如阻塞它的任何channel。在可执行代码里,它被称为G。
矩形P 表示用于调用的上下文。你可以把它看作在一个单线程上运行代码的调度器的一个本地化版本。它是让我们从N:1调度器转到M:N调度器的重要部分。在运行时代码里,它被叫做P,即处理器(processor)。这部分后面会多说点。
[
我们可以从上面的图里看到两个系统线程(M),每个线程都拥有一个上下文(P),每个线程都正在运行一个goroutine(G)。为了运行goroutines,一个线程必须拥有一个上下文。
上下文(P)的数目在启动时被设置为环境变量GOMAXPROCS的值或者通过运行时函数GOMAXPROCS()来设置。通常,在你的程序执行时它不会发生变化。上下文的数目被固定的意思是,只有GOMAXPROCS个上下文正在任意点上运行Go代码。我们可以使用GOMAXPROCS调整Go进程的调用使其适合于一个单独的计算机,比如一个4核的PC中可以在4个线程上运行Go代码。
外部的灰色goroutines没在运行,但是已经准备好被调度了。它们被安排成一个叫做runqueue的列表。当go创建一个goroutine的时候,goroutine就被添加到runqueue的末端。一旦一个上下文已经运行一个goroutine到了一个点上,它就会把一个goroutine从它的runqueue给pop出来,设置栈和指令指针并且开始运行这个goroutine。
为了降低mutex竞争,每一个上下文都有它自己的runqueue。Go调度器曾经的一个版本只有一个通过mutex来保护的全局runqueue,线程们经常被阻塞来等待mutex被解除阻塞。当你有许多32核的机器而且想尽可能地压榨它们的性能时,情况就会变得相当坏。
只要所有的上下文都有goroutines要运行,调度器就能在一个稳定的状态下保持调度。但是有几个你能改变的场景。
通俗地讲,G要运行,需要绑定一个P(放在P的本地队列里),然后由与P绑定的操作系统线程M真正执行。 G切换时,只是M从G1切到G2而已,都是在用户态进行着,非常轻量,不像操作系统切换M时比较重。
你打算(系统)调用谁?(Who you gonna (sys)call?) —————————————————— 你现在可能想知道,为什么一定要有上下文?我们能不能丢掉上下文而仅仅把runqueue放到线程上?不尽然。我们用上下文的原因是如果正在运行的线程因为某种原因需要阻塞的时候,我们可以把这些上下文移交给其它线程。
我们需要阻塞的一个例子是,当我们需要调用一个系统调用的时候。因为一个线程不能既执行代码同时又阻塞到一个系统调用上,我们需要移交给对应于这个线程的上下文,以让这个上下文进行调度。
[
从上图我们能够看出,一个线程放弃了它的上下文以让另外的线程可以运行它。调度器确保有足够的线程来运行所有的上下文。上图中的M1 可能仅仅为了让它处理图中的系统调用而被创建出来,或者它可能来自一个线程池(thread cache)。这个处于系统调用中的线程将会保持在这个导致系统调用的goroutine上,因为从技术上来说,它仍然在执行,虽然阻塞在OS里了。
当这个系统调用返回的时候,这个线程必须尝试获取一个上下文来运行这个返回的goroutine,操作的正常模式是从其它所有线程中的其中一个线程中“偷”一个上下文。如果“偷盗”不成功,它就会把它的goroutine放到一个全局runqueue中,然后把自己放到线程池中或者转入睡眠状态。
August 28, 2018
[译]Go里面的unsafe包详解
unsafe包位置: src/unsafe/unsafe.go
指针类型: ***类型:**普通指针,用于传递对象地址,不能进行指针运算。 **unsafe.Pointer:**通用指针,用于转换不同类型的指针,不能进行指针运算。 **uintptr:**用于指针运算,GC 不把 uintptr 当指针,uintptr 无法持有对象。uintptr 类型的目标会被 GC 回收。
unsafe.Pointer 可以和 普通指针 进行相互转换。 unsafe.Pointer 可以和 uintptr 进行相互转换。 也就是说 unsafe.Pointer 是桥梁,可以让任意类型的指针实现相互转换,也可以将任意类型的指针转换为 uintptr 进行指针运算。
一般使用流程: 第一步:将结构体 -> 通用指针unsafe.Pointer(struct) -> uintptr(通用指针)获取内存段的起始位置start_pos,并记录下来,第二步使用。 第二步:使用start_pos + unsafe.Offsetof(s.b) -> 将地址转为能用指针unsafe.Pointer(new_pos)->转为普通指针np = (*byte)(p)->赋值 *np = 20 第三步:打印结构体,发现内容发生了更改。
推荐:unsafe.Sizeof() 针对不同数据类型的情况
August 28, 2018
golang中slice切片理解总结
首先我们对切片有一个大概的理解,先看一下slice的内部结构,共分三部分,一个是指向底层数组的时候,一个是长度len,另一个就是slice的容量cap了。如cap不足以放在新值的时候,会产生新的内存地址申请。
先看代码
package main import "fmt" func main() { // 创建一个切片,长度为9,容量为10 fmt.Println("----- 1.测试切片变量append的影响(未申请新的内存空间)-----") a := make([]int, 9,10) fmt.Printf( "%p len=%d cap=%d %vn" , a, len(a), cap(a), a) // 切片进行append操作,由于原来len(a)长度为9,而cap(a)容量为10,未达到扩展内存的要求,此时新创建的切片变量还指向原来的底层数组,只是数组的后面添加一个新值 // 此时一共两个切片变量,一个是a,另一个是s4。但共指向的一个内存地址 s4 := append(a,4) fmt.Printf("%p len=%d cap=%d %vnn" , s4, len(s4), cap(s4), s4) // 测试上面提到的切片变量a和s4共指向同一个内存地址, 发现切片数组的第一个值都为7,而唯一不同的是len的长度,而cap仍为10 fmt.Println("----- 2.测试切片变量共用一个底层数组(内存地址一样)-----") a[0] = 7 fmt.Printf("%p len=%d cap=%d %vn" , a, len(a), cap(a), a) fmt.Printf("%p len=%d cap=%d %vnn" , s4, len(s4), cap(s4), s4) // 切片进行append操作后,发现原来的cap(a)的长度已用完了(因为a和s4共用一个底层数组,你也可以理解为cap(s4)),此时系统需要重新申请原cap*2大小的内存空间,所以cap值为10*2=20,将把原来底层数组的值复制到新的内存地址 // 此时有两个底层数组,一个是切片变量a和s4指向的数组,另一个就是新的切片变量s4 fmt.
July 2, 2018
Go中复制文件的3种方式
https://opensource.com/article/18/6/copying-files-go
更多: https://opensource.com/tags/go
June 11, 2018
Linux下对进程通信管理的信号机制概述
今天看到了篇使用golang实现的系统无感重启的文章, https://gravitational.com/blog/golang-ssh-bastion-graceful-restarts/,一般用来平滑处理一些系统服务,避免先停止再启用导致的服务不可用的情况。其中用到了信号机制,这里找了一些文章主要有来介绍这方面的文章,以便加深理解。 https://blog.csdn.net/junyucsdn/article/details/50519248 https://blog.csdn.net/tiany524/article/details/17048069 https://my.oschina.net/chenliang165/blog/125825
March 8, 2018
Go中slice作为参数传递的一些“坑”
看明白了这篇文章,下面的例子基本也就明白了
package main import "fmt" func main() { a := []int{1,2,3} abc(a) fmt.Println(a) } func abc(a []int) { a[0] = 2 //修改后还是原来的a a = append(a, 4) // 此a非原a,使用append导致了重新分配内存地址(存储空间不足,系统自动分配一块新的足够大的内存地址,此时a的物理内存地址已经发行了变化,并将原来a的值copy一份到新的内存地址,所以这里修改的只是新内存地址的值,原来内存地址的值并没有改变),试着删除这行运行一次再看结果 fmt.Println(a) a[0] = 7 // 新a,因为上面执行了append fmt.Println(a) fmt.Printf("\n===\n") } 解释: [][1]
March 3, 2018
golang中string rune byte 三者的关系
Go 语言中 byte 和 rune 实质上就是 uint8 和 int32 类型。 byte 用来强调数据是 raw data,而不是数字;而 rune 用来表示 Unicode 的 code point。参考 规范.
在Golang中 string 底层是用byte字节数组存储的,并且是不可以修改的。
Go语言中的byte和rune区别、对比 例如
s:="Go编程" fmt.Println(len(s)) //输出结果应该是8因为中文字符是用3个字节存的(2+3*2=8)。 fmt.Printf("%d", len(string(rune('编')))) //经测试一个汉字确实占用3个字节,所以结果是3 如果想要获得字符个数的话,需要先转换为rune切片再使用内置的len函数
fmt.Println(len([]rune(s))) // 结果就是4了。 所以用string存储unicode的话,如果有中文,按下标是访问不到的,因为你只能得到一个byte。 要想访问中文的话,还是要用rune切片,这样就能按下表访问。
总结: rune 能操作任何字符 byte 不支持中文的操作
示例: https://blog.haohtml.com/archives/14903
在极客时间的 go语言核心36讲 专栏里有一篇文章“ unicode与字符编码”对此介绍的比较详情,包含底层字符集编码。
在 Go 语言中,一个string类型的值既可以被拆分为一个包含多个字符的序列,也可以被拆分为一个包含多个字节的序列。前者可以由一个以 rune 为元素类型的切片来表示,而后者则可以由一个以 byte 为元素类型的切片代表。 rune是 Go 语言特有的一个基本数据类型,它的一个值就代表一个字符,即:一个 Unicode 字符。比如,’G’、’o’、’爱’、’好’、’者’代表的就都是一个 Unicode 字符。
April 3, 2016
[翻译]理解 GO 语言的内存使用
许多人在刚开始接触 Go 语言时,经常会有的疑惑就是“为什么一个 Hello world 会占用如此之多的内存?”。 Understanding Go Lang Memory Usage 很好的解释了这个问题。不过“简介”就是“简介”,更加深入的内容恐怕要读者自己去探索了。另外,文章写到最后,作者飘了,估计引起了一些公愤,于是又自己给自己补刀,左一刀,右一刀……
————翻译分隔线————
理解 Go 语言的内存使用 2014年12月22日,星期一
温馨提示:这仅是关于 Go 语言内存的简介,俗话说不入虎穴、焉得虎子,读者可以进行更加深入的探索。
大多数 Go 开发者都会尝试像这样简单的 hello world 程序:
package main import ( "fmt" "time" ) func main() { fmt.Println("hi") time.Sleep(30 * time.Second) } 然后他们就完全崩溃了。
这个笔记本也只有 16 G 内存!
虚拟内存 vs 常驻内存 Go 管理内存的方式可能与你以前使用的方式不太一样。它会在一开始就保留一大块 VIRT,而 RSS 与实际内存用量接近。
RSS 和 VIRT 之间有什么区别呢?
VIRT 或者虚拟地址空间大小是程序映射并可以访问的内存数量。
RSS 或者常驻大小是实际使用的内存数量。
如果你对 Go 到底是怎么实现的感兴趣,来看看这个:
https://github.com/golang/go/blob/master/src/runtime/malloc1.go
// 在 64 位设备中,从单一的连续保留地址中分配。
March 25, 2016
Profiling Go Programs
转自: http://blog.golang.org/profiling-go-programs (需翻墙)
The Go Blog Profiling Go Programs 24 June 2011
At Scala Days 2011, Robert Hundt presented a paper titled Loop Recognition in C++/Java/Go/Scala. The paper implemented a specific loop finding algorithm, such as you might use in a flow analysis pass of a compiler, in C++, Go, Java, Scala, and then used those programs to draw conclusions about typical performance concerns in these languages. The Go program presented in that paper runs quite slowly, making it an excellent opportunity to demonstrate how to use Go’s profiling tools to take a slow program and make it faster.
March 24, 2016
golang中并发实例
package main import ( "fmt" //"runtime" "os" "runtime/pprof" // 引用pprof package "time" ) func main() { f, _ := os.Create("profile_file") pprof.StartCPUProfile(f) // 开始cpu profile,结果写到文件f中 defer pprof.StopCPUProfile() // 结束profile startTime := time.Now().Second() //runtime.GOMAXPROCS(runtime.NumCPU()) // 注意这里的缓存大小 ch := make(chan int, 100) quit := make(chan bool) // 注意这里把读取chan操作放在了写入chan之前了(为了安全建议对chan的goroutines读取放在前面,写入放在后面) // 如果把这行放在了for逻辑后面,则上面声明的chan缓冲区大小一定要大于2000才可以,否则由 // 于缓存区一次放不下2000个元素,从而产生deadlock go read(ch, quit) for i := 0; i < 2000; i++ { ch <- i fmt.Println(i) } quit <- true fmt.Println("\r\n====MAIN END====") endTime := time.
March 24, 2016
在Golang中使用json
由于要开发一个小型的web应用,而web应用大部分都会使用json作为数据传输的格式,所以有了这篇文章。
包引用 import ( “encoding/json” “github.com/bitly/go-simplejson” // for json get )
用于存放数据的结构体 type MyData struct { Name string json:"item" Other float32 json:"amount" }
这里需要注意的就是后面单引号中的内容。
json:"item"
这个的作用,就是Name字段在从结构体实例编码到JSON数据格式的时候,使用item作为名字。算是一种重命名的方式吧。
编码JSON var detail MyData
detail.Name = “1” detail.Other = “2”
body, err := json.Marshal(detail) if err != nil { panic(err.Error()) }
我们使用Golang自带的encoding/json包对结构体进行编码到JSON数据。
json.Marshal(…)
JSON解码 由于Golang自带的json包处理解码的过程较为复杂,所以这里使用一个第三方的包simplejson进行json数据的解码操作。
js, err := simplejson.NewJson(body) if err != nil { panic(err.Error()) } fmt.Println(js) 有关simplejson的更多用法见: http://1.guotie.sinaapp.com/?p=400
更多用法参考: http://blog.haohtml.com/archives/16849
转自: http://www.cnblogs.com/sitemanager/p/3419970.html
March 24, 2016
golang的json操作
package main import ( "encoding/json" "fmt" "os" ) type ConfigStruct struct { Host string `json:"host"` Port int `json:"port"` AnalyticsFile string `json:"analytics_file"` StaticFileVersion int `json:"static_file_version"` StaticDir string `json:"static_dir"` TemplatesDir string `json:"templates_dir"` SerTcpSocketHost string `json:"serTcpSocketHost"` SerTcpSocketPort int `json:"serTcpSocketPort"` Fruits []string `json:"fruits"` } type Other struct { SerTcpSocketHost string `json:"serTcpSocketHost"` SerTcpSocketPort int `json:"serTcpSocketPort"` Fruits []string `json:"fruits"` } func main() { jsonStr := `{"host": "http://localhost:9090","port": 9090,"analytics_file": "","static_file_version": 1,"static_dir": "E:/Project/goTest/src/","templates_dir": "E:/Project/goTest/src/templates/","serTcpSocketHost": ":12340","serTcpSocketPort": 12340,"fruits": ["apple", "peach"]}` //json str 转map var dat map[string]interface{} if err := json.
March 23, 2016
golang中的md5的用法
代码
package main import ( "crypto/md5" "encoding/hex" "fmt" ) func main() { // md5 加密的第一种方法 srcData := []byte("iyannik0215") cipherText1 := md5.Sum(srcData) fmt.Printf("md5 encrypto is "iyannik0215": %x n", cipherText1) // md5 加密的第二种方法 hash := md5.New() hash.Write(srcData) cipherText2 := hash.Sum(nil) hexText := make([]byte, 32) hex.Encode(hexText, cipherText2) fmt.Println("md5 encrypto is "iyannik0215":", string(hexText)) } # 执行结果 md5 encrypto is "iyannik0215": b6b20c73e6bc53bc691a6bb559cf9ca9 md5 encrypto is "iyannik0215": b6b20c73e6bc53bc691a6bb559cf9ca9 不同
解释一下两种加密方式的不一样之处. 第一种加密方法: 第一种加密方法所调用的函数
//Source file src/crypto/md5/md5.go 19 // The size of an MD5 checksum in bytes.
June 15, 2015
golang中chan实例
package main import "fmt" func main() { data := make(chan int) // 数据交换队列 exit := make(chan bool) // 退出通知 go func() { for d := range data { // 从队列迭代接收数据,直到 close 。 fmt.Println(d) } fmt.Println("recv over.") exit <- true // 发出退出通知。 }() data <- 1 // 发送数据。 data <- 2 data <- 3 close(data) // 关闭队列。 fmt.Println("send over.") <-exit // 等待退出通知。 } 输出结果:
1 2 3 send over. recv over. 而如果将上面与 exit chan有关的三行删除掉,则结果为:
June 13, 2015
golang中chan的理解与使用教程
对于 chan 介绍见: https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/02.7.md
这里我们主要通过实例来介绍对chan的理解及用法.
无Buffer的Channels 实例1:
func main() { ci := make(chan int) ci <- 4 value := <-ci fmt.Println(value) } 执行结果错误为:
fatal error: all goroutines are asleep - deadlock! goroutine 1 [chan send]:
从上面“fatal error: all goroutines are asleep - deadlock!” 这句我们可以看出是groutings 阻塞了,这里为写阻塞,从“goroutine 1 [chan send]”可以看出来。
这一点文档里已经说明阻塞的原因了:
默认情况下,channel接收和发送数据都是阻塞的,除非另一端已经准备好,这样就使得Goroutines同步变的更加的简单,而不需要显式的lock。所谓阻塞,也就是如果读取(value := <-ch)它将会被阻塞,直到有数据接收。其次,任何发送(ch<-5)将会被阻塞,直到数据被读出。无缓冲channel是在多个goroutine之间同步很棒的工具。
解决办法:
我们只要将其中发送方(写)或者读取方(读)放到一个goroutine里就可以了,这样主程序main()里与goroutine通过channel来通讯即可。
func main() { ci := make(chan int) go write(ci) value := <-ci fmt.Println(value) } func write(c chan int) { c <- 4 } -–-–-–-–
June 9, 2015
golang中flag包的用法
golang中flag包主要用来CLI下,获取命令参数,示例如下mysql.go:
package main import ( "flag" "fmt" ) func main() { host := flag.String("h", "localhost", "请指定一个主机") user := flag.String("u", "root", "请指定数据库用户") port := flag.Int("P", 3306, "Port number to use for commection or 0 for default to, in port 3306") //var name string //flag.StringVar(&name, "u", "root", "请指定用户名") flag.Parse() //参数解析 fmt.Println("主机地址:", *host) fmt.Println("用户名:", *user) fmt.Println("端口:", *port) } 像flag.Int、flag.Bool、flag.String这样的函数格式都是一样的,第一个参数表示参数名称,第二个参数表示默认值,第三个参数表示使用说明和描述。flag.StringVar这样的函数第一个参数换成了变量地址,后面的参数和flag.String是一样的。
使用flag来操作命令行参数,支持的格式如下:
go run mysql.go -h="127.0.0.1" -u="sxf" -P=3307 go run mysql.go --h="127.0.0.1" --u="sxf" --P=3307 go run mysql.
April 8, 2015
Golang语言的GOPATH与工作目录详解
这篇文章主要介绍了Go语言的GOPATH与工作目录详解,本文详细讲解了GOPATH设置、应用目录结构、编译应用等内容,需要的朋友可以参考下
GOPATH设置
go 命令依赖一个重要的环境变量:$GOPATH
(注:这个不是Go安装目录( GOROOT)。下面以笔者的工作目录为说明,请替换自己机器上的工作目录。)
在类似 Unix 环境大概这样设置:
export GOPATH=/home/apple/mygo 为了方便,应该把新建以上文件夹,并且把以上一行加入到 .bashrc 或者 .zshrc 或者自己的 sh 的配置文件中。
Windows 设置如下,新建一个环境变量名称叫做GOPATH:
GOPATH=c:mygo GOPATH允许多个目录,当有多个目录时,请注意分隔符,多个目录的时候Windows是分号,Linux系统是冒号,当有多个GOPATH时,默认会将go get的内容放在第一个目录下
以上 $GOPATH 目录约定有三个子目录:
1.src 存放源代码(比如:.go .c .h .s等)
2.pkg 编译后生成的文件(比如:.a)
3.bin 编译后生成的可执行文件(为了方便,可以把此目录加入到 $PATH 变量中,如果有多个gopath,那么使用${GOPATH//://bin:}/bin添加所有的bin目录)
以后我所有的例子都是以mygo作为我的gopath目录
应用目录结构
建立包和目录:$GOPATH/src/mymath/sqrt.go(包名:”mymath”)
以后自己新建应用或者一个代码包都是在src目录下新建一个文件夹,文件夹名称一般是代码包名称,当然也允许多级目录,例如在src下面新建了目录$GOPATH/src/github.com/astaxie/beedb 那么这个包路径就是“github.com/astaxie/beedb”,包名称是最后一个目录beedb
cd $GOPATH/src mkdir mymath 新建文件sqrt.go,内容如下:
// $GOPATH/src/mymath/sqrt.go源码如下: package mymath func Sqrt(x float64) float64 { z := 0.0 for i := 0; i &lt; 1000; i++ { z -= (z*z - x) / (2 * x) } return z } 这样我的应用包目录和代码已经新建完毕,注意:一般建议package的名称和目录名保持一致
June 20, 2014
golint—golang代码质量检测
github: https://github.com/golang/lint
golint是类似javascript中的jslint的工具,主要功能就是检测代码中不规范的地方。golint用于检测go代码。
使用 $ go get github.com/golang/lint $ go install github.com/golang/lint golint 文件名或者目录 检测对应的代码。
golint会输出一些代码存在的问题: 比如:
recorder.go:55:5: exported var RecordBind should have comment or be unexported recorder.go:158:1: exported function Record_ErrorRecord should have comment or be unexported recorder.go:173:6: don't use underscores in Go names; type Data_MemStats should be DataMemStats recorder.go:179:2: struct field FreeRam should be FreeRAM 上面的输出中文件recorder.go,179行,在struct中字段 FreeRam 应该是 FreeRAM,输出信息非常的详细
golint 会检测的方面:
变量名规范 变量的声明,像var str string = “test”,会有警告,应该var str = “test” 大小写问题,大写导出包的要有注释 x += 1 应该 x++ 等等 详细可以看官方库示例, https://github.
March 8, 2014
golang中for循环方法的差异
用for循环遍历字符串时,也有 byte 和 rune 两种方式.第一种试为byte,第二种rune。
golang中string rune byte 三者的关系 https://blog.haohtml.com/archives/17646
package main import ( "fmt" ) func main() { s := "abc汉字" for i := 0; i < len(s); i++ { fmt.Printf("%c,", s[i]) } fmt.Println() for _, r := range s { fmt.Printf("%cn", r) } } 输出结果:
a,b,c,æ,±,,å,,, a b c 汉 字
October 31, 2013
Golang import使用说明
我们在写Go代码的时候经常用到import这个命令用来导入包文件,而我们经常看到的方式参考如下:
import( "fmt" ) 然后我们代码里面可以通过如下的方式调用
fmt.Println("hello world") 上面这个fmt是Go语言的标准库,他其实是去goroot下去加载该模块,当然Go的import还支持如下两种方式来加载自己写的模块:
1.相对路径
import “./model” //当前文件同一目录的model目录,但是不建议这种方式来import 2.绝对路径
import “shorturl/model” //加载gopath/src/shorturl/model模块
上面展示了一些import常用的几种方式,但是还有一些特殊的import,让很多新手很费解,下面我们来一一讲解一下到底是怎么一回事
1.点操作
我们有时候会看到如下的方式导入包
import( . "fmt" ) 这个点操作的含义就是这个包导入之后在你调用这个包的函数时,你可以省略前缀的包名,也就是前面你调用的fmt.Println(“hello world”)可以省略的写成Println(“hello world”)
2.别名操作
别名操作顾名思义我们可以把包命名成另一个我们用起来容易记忆的名字
import( f "fmt" ) 别名操作的话调用包函数时前缀变成了我们的前缀,即f.Println(“hello world”)
3._操作
这个操作经常是让很多人费解的一个操作符,请看下面这个import
import ( "database/sql" _ "github.com/ziutek/mymysql/godrv" ) _操作其实是引入该包,而不直接使用包里面的函数,而是调用了该包里面的init函数,要理解这个问题,需要看下面这个图,理解包是怎么按照顺序加载的:
程序的初始化和执行都起始于main包。如果main包还导入了其它的包,那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到fmt包,但它只会被导入一次,因为没有必要导入多次)。当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包中的包级常量和变量进行初始化,接着执行init函数(如果有的话),依次类推。等所有被导入的包都加载完毕了,就会开始对main包中的包级常量和变量进行初始化,然后执行main包中的init函数(如果存在的话),最后执行main函数。下图详细地解释了整个执行过程:
通过上面的介绍我们了解了import的时候其实是执行了该包里面的init函数,初始化了里面的变量,_操作只是说该包引入了,我只初始化里面的init函数和一些变量,但是往往这些init函数里面是注册自己包里面的引擎,让外部可以方便的使用,就很多实现database/sql的引起,在init函数里面都是调用了sql.Register(name string, driver driver.Driver)注册自己,然后外部就可以使用了。
这样我们就介绍完了全部import的情况,希望对你理解Go的import有一定的帮助。
October 22, 2013
golang实现验证电子信箱和手机格式合法性(正则表达式)
邮箱验证
func checkEmail(email string) (b bool) { if m, _ := regexp.MatchString("^([a-zA-Z0-9\_-])+@([a-zA-Z0-9\_-])+(.[a-zA-Z0-9_-])+", email); !m { return false } return true } 手机验证
package main import ( "regexp" ) const ( regular = "^(13[0-9]|14[57]|15[0-35-9]|18[07-9])\\d{8}$" ) func validate(mobileNum string) bool { reg := regexp.MustCompile(regular) return reg.MatchString(mobileNum) } func main() { if validate("13888888888") { println("是手机号") return } println("不是手机号") }
October 22, 2013
[必读]golang语言报错信息fatal error: all goroutines are asleep – deadlock!
出错信息fatal error: all goroutines are asleep – deadlock! 出错信息的意思是: 在main goroutine线,期望从管道中获得一个数据,而这个数据必须是其他goroutine线放入管道的 但是其他goroutine线都已经执行完了(all goroutines are asleep),那么就永远不会有数据放入管道。 所以,main goroutine线在等一个永远不会来的数据,那整个程序就永远等下去了。 这显然是没有结果的,所以这个程序就说“算了吧,不坚持了,我自己自杀掉,报一个错给代码作者,我被deadlock了” 例子:
package main func main() { c := make(chan bool) go func() { c <- true }() <-c //这里从c管道,取到一个true <-c //这行导致deadlock,因为这时的c管道,永远都取不到数据(注释掉这行就不报错) }
October 22, 2013
golang日志模块测试
package main import ( "fmt" "log" "os" ) func main(){ logfile,err := os.OpenFile("d:/workspace/golang/test.log",os.O\_RDWR|os.O\_CREATE|os.O_APPEND,0); if err!=nil { fmt.Printf("%s\r\n",err.Error()); os.Exit(-1); } defer logfile.Close(); logger := log.New(logfile,"\r\n",log.Ldate|log.Ltime|log.Llongfile); logger.Println("hello"); logger.Println("oh…."); logger.Fatal("test"); logger.Fatal("test2"); } 这里打开日志文件的时候,同时使用了””os.O_APPEND”这个,这样的话,日志会一直在文件最后面打印出来.
October 7, 2013
[翻译]绝妙的 channel
在编写 golang 程序的过程中,channel 会经常使用。本文对 channel 的使用的确很特别,同时也非常实用。
原文在此: http://dave.cheney.net/2013/04/30/curious-channels
在编写 golang 程序的过程中,channel 会经常使用。本文对 channel 的使用的确很特别,同时也非常实用。
原文在此:http://dave.cheney.net/2013/04/30/curious-channels
翻译:http://mikespook.com/2013/05/%E7%BF%BB%E8%AF%91%E7%BB%9D%E5%A6%99%E7%9A%84-channel/#more-1635
October 4, 2013
VIM编辑器下go语法高亮显示
Go in Vim The standard Go distribution includes a Go syntax file for Vim in go/misc/vim/.
Installation Instructions Place $GOROOT/misc/vim/syntax/go.vim in ~/.vim/syntax/ and put the following in ~/.vim/ftdetect/go.vim:
在go的安装目录里有/misc/vim/syntax 他 /misc/vim/ftdetect 两个目录,将里面的文件复制到~/.vim/相应的目录里即可。
au BufRead,BufNewFile *.go set filetype=go
Extras and Alternative Files An alternative indent file for Vim by Alecs King can be found here.
Autocompletion The gocode daemon by nsf includes a vim script to do autocompletion.
Other See the vim-golang repo in Github for alternative syntax highlight, auto-indentation, gofmt and other useful scripts and plugins.
October 2, 2013
gozmq的安装与使用教程(zeromq分布式消息队列+golang)
实现功能:用go实现消息队列的写入与读取(打算用在发送邮件服务)
环境工具: Centos 64X 6.4 zeromq 3.2.4: zeromq.org golang: http://golang.org/
一.安装golang( http://golang.org/doc/install) 这一步很简单,只需要从 http://code.google.com/p/go/downloads 下载到服务器,解压到/usr/local/go目录,再设置一下系统变量就可以了.
wget https://go.googlecode.com/files/go1.1.2.linux-amd64.tar.gz tar -C /usr/local -xzf go1.1.2.linux-amd64.tar.gz 设置系统变量GOROOT
Add /usr/local/go/bin to the PATH environment variable. You can do this by adding this line to your /etc/profile (for a system-wide installation) or $HOME/.profile:
export PATH=$PATH:/usr/local/go/bin 执行命令 #source /etc/profile 使环境变量生效.
设置项目环境变量GOPATH
下面我们需要设置开发项目使用的环境变量GOPATH的路径.可直接在命令行下执行下面的命令,也可以将下面的命令写入/etc/profile(或者 $HOME/.profile)中,这样下次使用的时候变量不会丢失.参考:
$ mkdir $HOME/go For convenience, add the workspace’s bin subdirectory to your PATH:
$ export GOPATH=$HOME/go $ export PATH=$PATH:$GOPATH/bin 也可以将上面两条命令写到/etc/profile里,然后再执行 #source /etc/profile 命令,使环境变量生效.
August 12, 2013
golang中结构体的初始化方法的不同用法(new方法)
自定义一个结构体
type Rect struct { x, y float64 width, height float64 } 初始化方法:
rect1 := new(Rect) rect2 := &Rect{} rect3 := &Rect{0, 0, 100, 200} rect4 := &Rect{width:100, height:200} 注意这几个变量全部为指向Rect结构的指针(指针变量),因为使用了new()函数和&操作符。
而如果使用方法
a := Rect{} 则表示这个是一个Rect{}结构类型.两者是不一样的.参考代码:
func main() { a := Rect{} a.x = 15 rect1 := &Rect{0, 0, 100, 200} rect1.x = 10 fmt.Printf("%v\n%T\n", a, a) fmt.Printf("%v\n%T\n", rect1, rect1) } 运行结果为:
{15 0 0 0} main.Rect &{10 0 100 200} *main.
August 11, 2013
golang中的文档管理
foo.go
// CopyRight 2013 The Go Author. All Right reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE fifle. /* Package foo implements a set of simple mathematical functions. These comments are for demonstration purpose only. Nothing more. If you have any questions,please don’t hesitate to add yourself to [email protected]. you can alse visit golang.org for full Go documentation.
August 11, 2013
golang中的map数据类型操作实例
package main import ( "fmt" ) type stu struct { Name string Age int } func main() { // 声明一个map变量student,键名为string,值为stu var student map[string]stu // 给map变量创建值,同时指定最多可以存储5个stu值 student = make(map[string]stu, 5) // map元素赋值 student["stu1"] = stu{"zhao", 25} student["stu2"] = stu{"zhang", 28} student["stu3"] = stu{"sun", 32} student["stu4"] = stu{"li", 40} student["stu5"] = stu{} //上面方式的简写方法 /* student := map[string]stu{ "stu1": stu{"zhao", 25}, "stu2": stu{"zhang", 28}, "stu3": stu{"sun", 32}, "stu4": stu{"li", 40}, "stu5": stu{}, } */ // 打印map信息 fmt.
August 9, 2013
go语言单元测试
Go本身提供了一套轻量级的测试框架.符合规则的测试代码会在运行测试时被自动识别并执行.单元测试源文件的命名规则如平衡点:在需要测试的包下面创建以”_test”结尾的go文件,开如[^.]*_test.go
Go单元测试函数分为两在类.功能测试函数和性能测试函数,分别以Test和Benchmark为函数名前缀并以*testing.T 和 *testing.B 为单一参数的函数。
func TestAdd1(t *testing.T) func BenchmarkAdd1(t *testing.T) 测试工具会根据函数中的实际执行动作得到不同的测试结果。
功能测试函数会根据测试代码执行过程中是否发生错误来反馈结果; 性能测试函数仅仅打印出来测试所花费时间,用来判断程序性能;
准备 新建一个文件,命名为 go_test.go
package go_test import "testing" func Add(a, b int) int { return a + b } 功能测试 在go_test.go文件里添加以下代码
func TestAdd1(t *testing.T) { r := Add(1, 2) if r != 2 { //这里为了测试,写成2 t.Errorf("Add(1,2) failed. Got %d, expected 3.", r) } } 执行功能单元测试非常的简单,直接执行
go test 输出以下内容
--- FAIL: TestAdd1 (0.00s) go_test.go:12: Add(1,2) failed. Got 3, expected 3.
July 23, 2013
用golang发送邮件
配置文件 conf.json
{ "Username": "[email protected]", "Password": "123456", "Smtphost":"smtp.163.com:25" } 主程序 sendmail.go
package main import ( "encoding/json" "fmt" "io" "log" "net/smtp" "os" "strings" ) type cfgmail struct { Username string Password string Smtphost string } type cfg struct { Name, Text string } func main() { // 从json文件中读取发送邮件服务器配置信息 cfgjson := getConf() var cfg cfgmail dec := json.NewDecoder(strings.NewReader(cfgjson)) if err := dec.Decode(&cfg); err == io.EOF { break } else if err != nil { log.
June 18, 2013
测试golang中的多核多线程
“并发 (concurrency)” 和 “并行 ( parallelism)” 是不同的。在单个 CPU 核上,线程通过时间片或者让出控制权来实现任务切换,达到 “同时” 运行多个任务的⺫的,这就是所谓的并发。但实际上任何时刻都只有一个任务被执行,其他任务通过某种算法来排队。
多核 CPU 可以让同个进程内的 “多个线程” 做到真正意义上的同时运,它们之间不需要排队 (依然会发生排队,因为线程数量可能超出 CPU 核数量,还有其他的进程等等。这里说的是一个理想状况),这才是并行。除了多核,并行计算还可能是多台机器上部署运行。
package main import ( "fmt" "runtime" ) func test(c chan bool, n int) { x := 0 for i := 0; i < 1000000000; i++ { x += i } println(n, x) if n == 9 { c <- true } } func main() { runtime.GOMAXPROCS(1) //设置cpu的核的数量,从而实现高并发 c := make(chan bool) for i := 0; i < 10; i++ { go test(c, i) } <-c fmt.
June 17, 2013
golang中的Array 、Slices 和 Maps
**注意slice和数组在声明时的区别:**声明数组时,方括号内写明了数组的长度或使用...自动计算长度,而声明slice时,方括号内没有任何字符。
arr1 := [10]int{1,2,3,4} //数组,长度为10,只有4个元素指定,其它的元素值默认为0 arr2 := [...]string{"a","b","c"} //数组,长度自适应,这里长度为3 s1 := []int{1,2,3,4} //slice,目前长度为4,可能通过append来动态添加元素个数 示例:
package main import ( "fmt" ) func main() { //array example arr := [10]int{1, 2, 3} //array 指定前三个值,其它值使用默认类型值0 fmt.Println(len(arr)) fmt.Println(arr) //a1 := append(arr, 4, 5) //数组不支持append,只有slice才支持append //fmt.Println(a1) //slice example slice := []string{"a", "b", "c"} // slice s1 := append(slice, "d") fmt.Println(s1) slice2 := []string{"x", "y", "x"} len1 := len(slice2) cap1 := cap(slice2) fmt.Println(slice2) // [x y x] fmt.
June 4, 2013
golang中包的用法
将d:/gotest/ 目录加入到GOPATH中.这里会涉及到包和结构体还有一些方法的用法,可以再深入的了了解一下
注意一下一些struct和 func 名称的大小写问题.
首先要在 $GOPATH/src 目录里创建一个包名目录,这里包名目录为stu,与文件名一样(也可以不一样),大概流程参考: d:/gotest/src/main/main.go
package main import ( "fmt" "stu" ) func main() { //sxf := new(stu.Stu) sxf := &stu.Stu{} sxf.SetName("zhangli") a := sxf.GetName() fmt.Println(a) } d:/gotest/src/stu/stu.go
package stu type Stu struct { name string //age int } func (s *Stu) SetName(name string) { s.name = name } func (s *Stu) GetName() string { return s.name } 在cmd中执行
go run main.go 会看到输出结果 **zhangli **
April 23, 2013
golang中实现自定义数据类型struct
可以参考: golang中的函数
func.go
package main import ( "fmt" ) type stu struct { Name string //首字母大写,允许其它包直接使用,可以直接使用 stu.Name = 'test' 也可以使用 setName和getName age int //不允许外面的包使用,可以使用 setAge和getAge方法 } func main() { perl := new(stu) perl.Name = "zhang" // age setAge(perl, 30) age := getAge(perl) fmt.Printf("%v\n", age) //name var name string perl.setName("sun") name = perl.getName() fmt.Printf("%i\n", name) //print struct fmt.Printf("%v\n", perl) } func setAge(s *stu, age int) { s.age = age } func getAge(s *stu) int { return s.
December 31, 2012
golang中的函数
函数是构建Go程序的基础部件;所遇有趣的事情都是在它其中发生的。函数 的定义看起来像这样: Listing 3.1. 函数定义
type mytype int 新的类型,参阅第 5 章 0 保留字func用于定义一个函数;
1 函数可以定义用于特定的类型,这类函数更加通俗的称呼是method。这 部分称作receiver而它是可选的(可参考: http://blog.haohtml.com/archives/13766)。如下图:
2 funcname是你函数的名字; 3 int类型的变量q作为输入参数。参数用pass-by-value方式传递,意味着它 们会被复制; 4 变量r和s是这个函数的命名返回值。在Go的函数中可以返回多个值。 参阅第32页的“多值返回”。如果不想对返回的参数命名,只需要提供类 型:(int,int)。如果只有一个返回值,可以省略圆括号。如果函数是一 个子过程,并且没有任何返回值,也可以省略这些内容; 5 这是函数体,注意return是一个语句,所以包裹参数的括号是可选的。
这里有两个例子,左边的函数没有返回值,右边的只是简单的将输入返回。
[java]func subroutine(in int) { return } func identity(in int) int { return in }[/java]
可以随意安排函数定义的顺序,编译器会在执行前扫描每个文件。所以函数原 型在Go中都是过期的旧物。Go不允许函数嵌套。然而你可以利用匿名函数实 现它,参阅本章第35页的“函数作为值”。
December 27, 2012
[golang]将函数作为值
就像其它在Go中的几乎所有的东西,函数也同样是值而已.它们可以像下面这样赋值给变量:
package main import "fmt" func main() { f := func() { fmt.Println("func") } // 下面才开始调用函数 f() } 结果会打印出 func 字符串。
另一种用法是立即调用函数,但是要求匿名函数要有返回值才可以,不然会提示错误信息.
December 15, 2012
windows 下搭建 GoLang 语言开发环境
golang官方二进制分发包包括FreeBSD, Linux, Mac OS X (Snow Leopard/Lion), and Windows等平台,包括32位、64位等版本。
我自己使用的是windows 32位分发包,MSI格式的,下载地址为: http://code.google.com/p/go/downloads/list
golang支持交叉编译,也就是说你在32位平台的机器上开发,可以编译生成64位平台上的可执行程序。
环境变量说明: $GOROOT 指向golang安装之后的根目录,windows平台下默认为c:\go,会在安装过程中由安装程序自动写入系统环境变量。 $GOARCH 目标平台(编译后的目标平台)的处理器架构(386、amd64、arm) $GOOS 目标平台(编译后的目标平台)的操作系统(darwin、freebsd、linux、windows) $GOBIN 指向安装之后根目录下的bin目录,即$GOROOT/bin,windows平台下默认为c:\go\bin,会在安装过程中由安装程序自动添加到PATH变量中
配置windows环境变量:
(1). 新建系统变量 变量名: GOROOT 变量值:d:\go
(2). 新建系统变量 变量名:GOBIN 变量值 :%GOROOT%\bin
(3). 编辑系统变量 Path 在Path的变量值的最后加上 ;%GOBIN%
(4). 确定保存变量配置
测试安装结果: 创建hello.go文件,内容如下:
package main import "fmt" func main() { fmt.Printf(“hello, world\n”) } 执行命令并显示结果 D:\godev>go run hello.go hello, world 如果正确输出结果,则表明安装成功。接下来就可以使用了。
=================================================
首先从网上下载 windows golang 环境
64 和 32 分别下载 amd64 和 386的 压缩包。