<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Golang on Wimi's Space</title><link>https://wimi.space/tags/golang/</link><description>Recent content in Golang on Wimi's Space</description><generator>Hugo</generator><language>zh-cn</language><lastBuildDate>Fri, 20 Jun 2025 15:19:51 +0800</lastBuildDate><atom:link href="https://wimi.space/tags/golang/index.xml" rel="self" type="application/rss+xml"/><item><title>Go 语言的内存管理和垃圾回收</title><link>https://wimi.space/posts/2025/06/golang-memory-management/</link><pubDate>Fri, 20 Jun 2025 15:19:51 +0800</pubDate><guid>https://wimi.space/posts/2025/06/golang-memory-management/</guid><description>&lt;p>程序中的数据和变量都会被分配到程序所在的虚拟内存中，内存空间包含两个重要区域：栈区（Stack）和堆区（Heap）。Go 语言的内存分配机制采用自动内存管理，由编译器和运行时协作完成。&lt;/p>
&lt;ul>
&lt;li>编译器负责栈内存的分配和释放。&lt;/li>
&lt;li>堆内存的分配则由运行时的内存分配器和垃圾收集器共同管理。&lt;/li>
&lt;/ul>
&lt;h2 id="堆内存管理">堆内存管理&lt;/h2>
&lt;p>程序在运行期间可以主动从堆区申请内存空间，这些内存由内存分配器分配并由垃圾收集器负责回收。Go 语言的内存分配器使用了多种策略来优化内存分配和回收的效率。&lt;/p>
&lt;h3 id="内存分配器">内存分配器&lt;/h3>
&lt;p>内存分配器的主要任务是将用户程序的内存申请请求转换为堆区的实际内存分配操作。当用户程序申请内存时，会通过内存分配器申请新内存，而内存分配器会负责从堆中初始化相应的内存区域。内存分配器的核心是一个内存池，它将堆区划分为多个小块，并根据需要动态分配和释放这些小块。&lt;/p>
&lt;h4 id="分配方法">分配方法&lt;/h4>
&lt;p>内存分配器通常支持多种分配方法，如线性分配器和空闲链表分配器，以满足不同的内存分配需求。&lt;/p>
&lt;h5 id="线性分配器">线性分配器&lt;/h5>
&lt;p>线性分配器是一种简单高效的内存分配方法，它通过维护一个指针来跟踪当前分配的位置，每次分配时只需将指针向后移动一定的字节数即可。当用户程序向分配器申请内存时，分配器只需要检查剩余的空闲内存、返回分配的内存区域并修改指针在内存中的位置。&lt;/p>
&lt;ul>
&lt;li>优点是分配速度非常快，因为它只需要更新指针，而不需要进行复杂的内存管理操作。&lt;/li>
&lt;li>缺点是无法释放已分配的内存，因此适用于那些生命周期较短的内存分配场景。&lt;/li>
&lt;/ul>
&lt;h5 id="空闲链表分配器">空闲链表分配器&lt;/h5>
&lt;p>空闲链表分配器是一种更复杂的内存分配方法，它维护一个空闲链表来跟踪未使用的内存块。当需要分配内存时，分配器会从空闲链表中找到合适的内存块并将其分配给用户程序。&lt;/p>
&lt;ul>
&lt;li>优点是可以释放已分配的内存，从而更有效地利用堆区的内存资源。&lt;/li>
&lt;li>缺点是分配速度相对较慢，因为需要在空闲链表中查找合适的内存块。&lt;/li>
&lt;/ul>
&lt;h4 id="go-内存分配方法">Go 内存分配方法&lt;/h4>
&lt;p>Go 语言的内存分配器采用了 TCMalloc（Thread-Caching Malloc）线程缓存分配，这是一种线程缓存的内存分配算法。它的核心理念是使用多级缓存将对象根据大小分类，并按照类别实施不同的分配策略。&lt;/p>
&lt;p>内存分配管理的对象按照大小可以分为：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>类别&lt;/th>
 &lt;th>大小&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>微对象&lt;/td>
 &lt;td>(0, 16B)&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>小对象&lt;/td>
 &lt;td>[16B, 32KB]&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>大对象&lt;/td>
 &lt;td>(32KB, +∞)&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>通过为每个线程维护一个本地缓存来减少锁的竞争，每个线程在分配内存时，首先尝试从本地缓存中获取内存，如果本地缓存中没有足够的内存，则向全局内存池申请新的内存块。这样可以减少线程之间的锁竞争，提高内存分配的效率。&lt;/p>

&lt;details>
 &lt;summary>点击展开&lt;/summary>
 &lt;p>&lt;img src="https://static.wimi.space/blog/golang-memory-alloc.webp" alt="">
「图片来自&lt;a href="https://cloud.tencent.com/developer/article/2395092">一文搞懂 Go 内存分配器&lt;/a>」&lt;/p>
&lt;/details>

&lt;h3 id="垃圾收集器">垃圾收集器&lt;/h3>
&lt;p>为了解决原始标记清除算法带来的长时间 STW，多数现代的追踪式垃圾收集器都会实现三色标记算法的变种以缩短 STW 的时间。&lt;/p>
&lt;p>Go 语言的 GC 主要经历了以下三个重要阶段：&lt;/p>
&lt;ul>
&lt;li>Go1.3 的标记清除。&lt;/li>
&lt;li>Go1.5 并发三色标记法和插入写屏障。&lt;/li>
&lt;li>Go1.8 三色标记和混合写屏障机制。&lt;/li>
&lt;/ul>
&lt;p>其核心思想是优化 STW 时间，实现低停顿的并发垃圾回收，是其并发能力的核心支撑之一。&lt;/p>
&lt;h4 id="三色标记清除法">三色标记清除法&lt;/h4>
&lt;p>该算法的核心思想是通过对对象进行标记和分类来确定哪些对象是可达的，哪些对象是不可达的，从而进行垃圾回收。根据每个对象的颜色，分到不同的颜色集合中，对象的颜色是在标记阶段完成的。&lt;/p>
&lt;p>将内存对象分为三类颜色：&lt;/p>
&lt;ul>
&lt;li>白色（待回收态）：表示对象还没有被标记，如果在垃圾回收结束时仍然是白色的，那么它将被回收。&lt;/li>
&lt;li>灰色（中间态）：表示对象已经被发现并标记，但其引用的子对象还没有被扫描，灰色对象需要进一步处理。&lt;/li>
&lt;li>黑色（存活态）：表示对象已经被扫描，并且所有引用的子对象也已经被标记，黑色对象为存活对象，不会被回收。&lt;/li>
&lt;/ul>
&lt;p>三色标记清除法的工作步骤：&lt;/p>
&lt;ol>
&lt;li>初始化阶段
&lt;ul>
&lt;li>所有对象开始时都被标记为白色。&lt;/li>
&lt;li>根对象被标记为灰色，并放入一个待处理队列中。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>标记阶段
&lt;ul>
&lt;li>重复以下步骤，直到待处理队列为空：
&lt;ul>
&lt;li>从待处理队列中取出一个灰色对象，并将其标记为黑色。&lt;/li>
&lt;li>遍历该对象的所有引用。如果被引用的对象是白色的，将其标记为灰色，并放入待处理队列中。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>清除阶段
&lt;ul>
&lt;li>遍历所有对象：
&lt;ul>
&lt;li>如果对象是白色的，说明它是不可达的，可以被回收。&lt;/li>
&lt;li>如果对象是黑色的，说明它是可达的，保留不动。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;h4 id="混合写屏障">混合写屏障&lt;/h4>
&lt;p>在用户协程与 GC 协程并发执行的场景下，可能导致部分存活对象未被正确标记的情况。为了支持能够并发进行垃圾回收，Go 语言在垃圾回收过程中采用了混合屏障技术，在指针修改时触发，确保三色不变式不被破坏（没看明白，暂不做展开🤨）。&lt;/p></description></item><item><title>Go 语言的协程调度模型</title><link>https://wimi.space/posts/2025/06/golang-gpm-model/</link><pubDate>Thu, 19 Jun 2025 10:27:40 +0800</pubDate><guid>https://wimi.space/posts/2025/06/golang-gpm-model/</guid><description>&lt;h2 id="进程线程和协程">进程、线程和协程&lt;/h2>
&lt;p>在操作系统中，进程是资源分配的基本单位，线程是 CPU 调度的基本单位，而协程则是用户态的轻量级线程。&lt;/p>
&lt;p>协程通过调度器在单个线程中实现多任务并发执行。协程的调度器负责管理协程的生命周期和执行顺序，通常使用协作式调度或抢占式调度。&lt;/p>
&lt;h2 id="gpm-调度模型">GPM 调度模型&lt;/h2>
&lt;p>GPM 调度模型中的 G、P、M 分别代表 Goroutine、Processor 和 Machine，它通过将 Goroutine 映射到 Processor 上，并在 Machine 上执行，实现了高效的并发执行。&lt;/p>
&lt;p>Goroutine 的轻量级特性和协作式调度使得 Go 语言能够轻松处理数以万计的并发任务。GPM 模型的设计理念和实现方式为 Go 语言的高性能并发编程提供了强大的支持。&lt;/p>
&lt;p>&lt;img src="http://tonybai.com/wp-content/uploads/goroutine-scheduler-model.png" alt="">
「图片来自 &lt;a href="https://tonybai.com/2017/06/23/an-intro-about-goroutine-scheduler/">TonyBai&lt;/a>」&lt;/p>
&lt;h3 id="底层数据结构">底层数据结构&lt;/h3>
&lt;p>go1.23 对应结构体定义如下，省略部分字段和方法，完整源代码见 &lt;a href="https://github.com/golang/go/blob/6885bad7dd86880be6929c02085e5c7a67ff2887/src/runtime/runtime2.go">Github&lt;/a>&lt;/p>
&lt;pre>&lt;code class="language-go">type g struct {
 stack stack // offset known to runtime/cgo
 stackguard0 uintptr // offset known to liblink
 stackguard1 uintptr // offset known to liblink

 _panic *_panic // innermost panic - offset known to liblink
 _defer *_defer // innermost defer
 m *m // current m; offset known to arm liblink
 sched gobuf

 goid uint64

 // 省略其他字段 ...
}

type m struct {
 g0 *g // Goroutine with scheduling stack
 curg *g // current running Goroutine

 p puintptr // attached p for executing go code (nil if not executing go code)
 nextp puintptr
 oldp puintptr // the p that was attached before executing a syscall

 // 省略其他字段 ...
}

type p struct {
 m muintptr // back-link to associated m (nil if idle)

 // Queue of runnable Goroutines. Accessed without lock.
 runqhead uint32
 runqtail uint32
 runq [256]guintptr
 runnext guintptr

 // 省略其他字段 ...
}

type schedt struct {
 midle muintptr // idle m's waiting for work
 nmidle int32 // number of idle m's waiting for work
 nmidlelocked int32 // number of locked m's waiting for work
 mnext int64 // number of m's that have been created and next M ID
 maxmcount int32 // maximum number of m's allowed (or die)
 nmsys int32 // number of system m's not counted for deadlock
 nmfreed int64 // cumulative number of freed m's

 // Global runnable queue.
 runq gQueue
 runqsize int32

 // 省略其他字段 ...
}
&lt;/code>&lt;/pre>
&lt;h4 id="ggoroutine">G（Goroutine）&lt;/h4>
&lt;p>Goroutine 是 Go 语言的轻量级线程，具有以下特点：&lt;/p></description></item><item><title>Go 语言的切片和哈希表</title><link>https://wimi.space/posts/2025/06/golang-slice-and-map/</link><pubDate>Wed, 18 Jun 2025 10:11:53 +0800</pubDate><guid>https://wimi.space/posts/2025/06/golang-slice-and-map/</guid><description>&lt;h2 id="数组">数组&lt;/h2>
&lt;p>Go 语言中的数组是一个由固定长度的相同类型元素组成的序列。&lt;/p>
&lt;ul>
&lt;li>数组类型定义了长度和元素类型，在内存中连续存储，通过索引的方式访问。&lt;/li>
&lt;li>数组变量属于值类型，当一个数组变量被赋值或者传递时会复制整个数组。为了避免复制数组，一般会传递指向数组的指针。&lt;/li>
&lt;li>数组的长度是固定的，长度是数组类型的一部分。长度不同的两个数组是不可以相互赋值的，因为这两个数组属于不同的类型。&lt;/li>
&lt;/ul>
&lt;h3 id="底层数据结构">底层数据结构&lt;/h3>
&lt;p>Go 语言数组类型的底层数据结构定义如下，完整代码见 &lt;a href="https://github.com/golang/go/blob/6885bad7dd86880be6929c02085e5c7a67ff2887/src/cmd/compile/internal/types/type.go#L424">Github&lt;/a>（go1.23）：&lt;/p>
&lt;pre>&lt;code class="language-go">// Array contains Type fields specific to array types.
type Array struct {
	Elem *Type // element type
	Bound int64 // number of elements; &amp;lt;0 if unknown yet
}
&lt;/code>&lt;/pre>
&lt;p>该类型包含两个字段，分别是元素类型 Elem 和数组的大小 Bound，这两个字段共同构成了数组类型，而当前数组是否应该在堆栈中初始化也在编译期就确定了。&lt;/p>
&lt;h2 id="切片">切片&lt;/h2>
&lt;p>Go 的切片是在数组之上的抽象数据类型。数组固定长度，缺少灵活性，大部分场景下会选择使用基于数组构建的切片类型，切片类型为处理同类型数据序列提供一个方便而高效的方式。&lt;/p>
&lt;h3 id="底层数据结构-1">底层数据结构&lt;/h3>
&lt;p>Go 语言切片的底层数据结构包含三个字段，完整代码见 &lt;a href="https://github.com/golang/go/blob/6885bad7dd86880be6929c02085e5c7a67ff2887/src/runtime/slice.go#L15">Github&lt;/a>（go1.23）：&lt;/p>
&lt;pre>&lt;code class="language-go">type slice struct {
 array unsafe.Pointer
 len int
 cap int
}
&lt;/code>&lt;/pre>
&lt;ul>
&lt;li>&lt;code>array&lt;/code>：指向底层数组类型的指针。&lt;/li>
&lt;li>&lt;code>len&lt;/code>：切片的长度，表示切片中元素的个数。&lt;/li>
&lt;li>&lt;code>cap&lt;/code>：切片的容量，表示切片可以容纳的最大元素个数。&lt;/li>
&lt;/ul>
&lt;p>切片的长度和容量可以通过内置函数 &lt;code>len()&lt;/code> 和 &lt;code>cap()&lt;/code> 获取。&lt;/p>
&lt;h3 id="常用操作">常用操作&lt;/h3>
&lt;p>切片操作并不复制切片指向的元素，创建一个新的切片会复用原来切片的底层数组，因此切片操作是非常高效的。&lt;/p></description></item><item><title>Go 语言通道的常见用法</title><link>https://wimi.space/posts/2025/06/golang-channel/</link><pubDate>Tue, 17 Jun 2025 16:37:27 +0800</pubDate><guid>https://wimi.space/posts/2025/06/golang-channel/</guid><description>&lt;blockquote>
&lt;p>『不要通过共享内存来通信，我们应该通过通信来共享内存』&lt;/p>
&lt;p>Do not communicate by sharing memory; instead, share memory by communicating.&lt;/p>&lt;/blockquote>
&lt;h2 id="go-语言推崇的并发解决方案csp">Go 语言推崇的并发解决方案（CSP）&lt;/h2>
&lt;p>在很多环境中，并发编程带来的很多问题都是因为没有正确实现访问共享内存的逻辑，虽然 Go 语言中也能使用共享内存加互斥锁进行通信，但是 Go 语言提供了一种不同的并发模型，即通信顺序进程（Communicating sequential processes，CSP）。&lt;/p>
&lt;p>Goroutine 和 Channel 分别对应 CSP 中的实体和传递信息的媒介，Goroutine 之间会通过 Channel 传递数据。这种方式可以保证在同一时间只有一个 Goroutine 能够访问对应的数据，所以数据冲突和线程竞争的问题在设计上就不可能出现。&lt;/p>
&lt;h2 id="通道的数据结构">通道的数据结构&lt;/h2>
&lt;p>Go 语言中 Channel 是 Goroutine 间重要通信的方式，是并发安全的，通道内的数据遵循 FIFO 的队列特性。&lt;/p>
&lt;p>底层的数据结构定义如下，完整代码见 &lt;a href="https://github.com/golang/go/blob/6885bad7dd86880be6929c02085e5c7a67ff2887/src/runtime/chan.go#L33">Github&lt;/a>（go1.23）：&lt;/p>
&lt;pre>&lt;code class="language-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
 timer *timer // timer feeding this chan
 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.
 //
 // Do not change another G's status while holding this lock
 // (in particular, do not ready a G), as this can deadlock
 // with stack shrinking.
 lock mutex
}
&lt;/code>&lt;/pre>
&lt;ul>
&lt;li>qcount 记录着通道内数据个数。&lt;/li>
&lt;li>dataqsiz 是缓存型通道的大小。&lt;/li>
&lt;li>buf 指向一个数组，用来实现循环队列。对于有缓冲通道，长度为 dataqsiz，对于无缓冲通道，buf 为 nil。&lt;/li>
&lt;li>sendx 是循环队列的队尾指针。&lt;/li>
&lt;li>recvx 是循环队列的队头指针。&lt;/li>
&lt;li>lock 是互斥锁，用来保护 hchan 的数据结构，保证操作的原子性。&lt;/li>
&lt;/ul>
&lt;p>hchan 的结构图如下：
&lt;img src="https://static.cyub.vip/images/202011/channel_struct.jpg" alt="">
「图片来自 &lt;a href="https://go.cyub.vip/concurrency/channel/">深入 Go 语言之旅&lt;/a>」&lt;/p></description></item><item><title>Go 语言的互斥锁和读写锁</title><link>https://wimi.space/posts/2025/06/golang-mutex-and-rwmutex/</link><pubDate>Mon, 16 Jun 2025 15:08:29 +0800</pubDate><guid>https://wimi.space/posts/2025/06/golang-mutex-and-rwmutex/</guid><description>&lt;h2 id="临界区">临界区&lt;/h2>
&lt;p>临界区是指访问共享资源的代码段，这些资源无法被多个线程或协程同时安全地访问。若多个并发执行单元同时进入临界区操作共享资源，则可能发生竞态条件，导致数据竞争和不可预测的行为。&lt;/p>
&lt;p>我们可以使用互斥锁，限定临界区只能同时由一个线程持有。当临界区由一个线程持有的时候，其它线程如果想进入这个临界区，就会返回失败，或者是等待。直到持有的线程退出临界区，这些等待线程中的某一个才有机会接着持有这个临界区。&lt;/p>
&lt;h2 id="锁的分类">锁的分类&lt;/h2>
&lt;p>在并发编程中，从锁的逻辑思想上来看，分为悲观锁和乐观锁两种类型，这两种锁代表了在并发控制中两种截然不同的哲学，核心在于处理冲突的时机。&lt;/p>
&lt;h3 id="悲观锁">悲观锁&lt;/h3>
&lt;p>总是假设最坏情况会发生。在访问数据之前就认为其他线程很可能也会修改这份数据。因此，在读取数据时就先加锁，确保在自己操作期间，任何其他线程都无法修改这份数据，直到当前操作完成并释放锁。&lt;/p>
&lt;p>具体子类型有以下几种：&lt;/p>
&lt;h4 id="互斥锁">互斥锁&lt;/h4>
&lt;p>最基本的锁类型，同一时间只允许一个线程持有锁。其他尝试获取锁的线程会被阻塞，进入锁等待或直接失败，同时线程会释放 CPU 给其他线程。互斥锁适合写操作频繁、冲突概率高的场景，能有效避免脏写。&lt;/p>
&lt;h4 id="自旋锁">自旋锁&lt;/h4>
&lt;p>与互斥锁相同，同一时间只允许一个线程持有锁；区别在与线程在未获取锁时进入忙等待，持续循环检查而非立即阻塞。适用于锁持有时间短的场景，避免了线程阻塞和唤醒过程中的内核操作和上下文切换带来的开销成本。&lt;/p>
&lt;h4 id="读写锁">读写锁&lt;/h4>
&lt;p>将锁分为读锁和写锁。读锁只要没有写锁存在，允许多个线程同时持有读锁。写锁就是互斥锁，一旦一个线程持有写锁，其他任何线程都无法获取锁。读写锁提高了线程的并发度，适用于读多写少的场景。&lt;/p>
&lt;h3 id="乐观锁">乐观锁&lt;/h3>
&lt;p>总是假设最好的情况。认为在自己操作期间，其他线程不太可能修改同一份数据，因此，在读取数据时不加锁，直接进行操作。但在提交更新前，会检查数据是否被其他事务修改过。如果没被修改，则提交成功；如果被修改了，则放弃本次修改。&lt;/p>
&lt;p>『乐观锁本质上不是真正的锁机制，不需要传统意义上的阻塞其他线程的互斥锁』。常见的乐观锁实现机制有以下几种：&lt;/p>
&lt;h4 id="版本号机制">版本号机制&lt;/h4>
&lt;p>为数据项添加一个版本号或时间戳字段。每次读取数据时，同时读取当前版本号；在更新数据前，先检查当前内存中的版本号是否与之前读取的版本号一致，不一致则说明数据已经被其他线程所修改，更新操作失败。可参考『&lt;a href="https://wimi.space/posts/2025/06/business-version-number/">版本号机制在业务中的应用&lt;/a>』。&lt;/p>
&lt;h4 id="compare-and-swapcas">Compare And Swap（CAS）&lt;/h4>
&lt;p>CAS 是一种由现代 CPU 直接支持的原子操作，是现代并发编程的基石，尤其是实现无锁数据结构和非阻塞算法中经常被用到。它的核心功能是：在一条不可分割的指令中，完成读取、比较、写入这三个步骤，确保操作的原子性。&lt;/p>
&lt;h2 id="go-语言的悲观锁">Go 语言的悲观锁&lt;/h2>
&lt;p>Go 语言在 sync 包中提供了 sync.Mutex 和 sync.RWMutex 两种锁，前者是互斥锁，后者是读写锁。Go 语言并没有直接提供自旋锁的类型，通常使用 sync/atomic 包中的 CAS 原子操作来模拟实现，留作后续补充。&lt;/p>
&lt;p>在 Go 语言中，悲观锁是一种相对原始的同步机制，在多数情况下，我们都应该使用抽象层级更高的 Channel 实现同步。&lt;/p>
&lt;h3 id="syncmutex">sync.Mutex&lt;/h3>
&lt;p>互斥锁 Mutex 提供两个方法 Lock 和 Unlock，在进入临界区之前调用 Lock 方法，退出临界区的时候调用 Unlock 方法。当一个 Goroutine 通过调用 Lock 方法获得了这个锁的拥有权后，其它请求锁的 Goroutine 就会阻塞在 Lock 方法的调用上，直到锁被释放并且自己获取到了这个锁的拥有权。&lt;/p>

&lt;details>
 &lt;summary>代码示例&lt;/summary>
 &lt;p>对临界区数据并发访问，加锁和不加锁示例，&lt;a href="https://github.com/qmdx00/playground/blob/master/golang-sync-example/mutex_test.go">完整示例代码&lt;/a>：&lt;/p>
&lt;pre>&lt;code class="language-go">package main

import (
 &amp;quot;fmt&amp;quot;
 &amp;quot;sync&amp;quot;
 &amp;quot;testing&amp;quot;
)

func TestConcurrencyAdd(t *testing.T) {
 var count = 0
 var wg sync.WaitGroup

 for range 10 {
 wg.Add(1)
 go func() {
 defer wg.Done()
 for range 100000 {
 count++
 }
 }()
 }

 wg.Wait()
 fmt.Printf(&amp;quot;Want %d, Got %d\n&amp;quot;, 1000000, count)
}

func TestConcurrencyAddWithMutex(t *testing.T) {
 var count = 0
 var wg sync.WaitGroup
 var mutex sync.Mutex

 for range 10 {
 wg.Add(1)
 go func() {
 defer wg.Done()
 for range 100000 {
 mutex.Lock()
 count++
 mutex.Unlock()
 }
 }()
 }

 wg.Wait()
 fmt.Printf(&amp;quot;Want %d, Got %d\n&amp;quot;, 1000000, count)
}
&lt;/code>&lt;/pre>
&lt;p>运行结果：&lt;/p></description></item><item><title>Go 语言项目布局（转载文章）</title><link>https://wimi.space/posts/2021/06/golang-project-layout/</link><pubDate>Tue, 29 Jun 2021 11:18:01 +0800</pubDate><guid>https://wimi.space/posts/2021/06/golang-project-layout/</guid><description>&lt;blockquote>
&lt;p>转载文章，&lt;a href="https://github.com/golang-standards/project-layout/blob/master/README_zh.md">原文链接&lt;/a>&lt;/p>&lt;/blockquote>
&lt;p>这是 Go 应用程序项目的基本布局。它不是核心 Go 开发团队定义的官方标准；然而，它是 Go 生态系统中一组常见的老项目和新项目的布局模式。其中一些模式比其他模式更受欢迎。它还具有许多小的增强，以及对任何足够大的实际应用程序通用的几个支持目录。&lt;/p>
&lt;h2 id="go-目录">Go 目录&lt;/h2>
&lt;h3 id="cmd">/cmd&lt;/h3>
&lt;p>本项目的主干。&lt;/p>
&lt;p>每个应用程序的目录名应该与你想要的可执行文件的名称相匹配（例如，/cmd/myapp）。&lt;/p>
&lt;p>不要在这个目录中放置太多代码。如果你认为代码可以导入并在其他项目中使用，那么它应该位于 /pkg 目录中。如果代码不是可重用的，或者你不希望其他人重用它，请将该代码放到 /internal 目录中。你会惊讶于别人会怎么做，所以要明确你的意图!&lt;/p>
&lt;p>通常有一个小的 main 函数，从 /internal 和 /pkg 目录导入和调用代码，除此之外没有别的东西。&lt;/p>
&lt;h3 id="internal">/internal&lt;/h3>
&lt;p>私有应用程序和库代码。这是你不希望其他人在其应用程序或库中导入代码。请注意，这个布局模式是由 Go 编译器本身执行的。注意，你并不局限于顶级 internal 目录。在项目树的任何级别上都可以有多个内部目录。&lt;/p>
&lt;p>你可以选择向 internal 包中添加一些额外的结构，以分隔共享和非共享的内部代码。这不是必需的（特别是对于较小的项目），但是最好有有可视化的线索来显示预期的包的用途。你的实际应用程序代码可以放在 /internal/app 目录下（例如 /internal/app/myapp），这些应用程序共享的代码可以放在 /internal/pkg 目录下（例如 /internal/pkg/myprivlib）。&lt;/p>
&lt;h3 id="pkg">/pkg&lt;/h3>
&lt;p>外部应用程序可以使用的库代码（例如 /pkg/mypubliclib）。其他项目会导入这些库，希望它们能正常工作，所以在这里放东西之前要三思） 注意，internal 目录是确保私有包不可导入的更好方法，因为它是由 Go 强制执行的。/pkg 目录仍然是一种很好的方式，可以显式地表示该目录中的代码对于其他人来说是安全使用的好方法。&lt;/p>
&lt;p>当根目录包含大量非 Go 组件和目录时，这也是一种将 Go 代码分组到一个位置的方法，这使得运行各种 Go 工具变得更加容易。&lt;/p>
&lt;p>如果你的应用程序项目真的很小，并且额外的嵌套并不能增加多少价值（除非你真的想要），那就不要使用它。当它变得足够大时，你的根目录会变得非常繁琐时（尤其是当你有很多非 Go 应用组件时），请考虑一下。&lt;/p>
&lt;h3 id="vendor">/vendor&lt;/h3>
&lt;p>应用程序依赖项（手动管理或使用你喜欢的依赖项管理工具，如新的内置 Go Modules 功能）。go mod vendor 命令将为你创建 /vendor 目录。请注意，如果未使用默认情况下处于启用状态的 Go 1.14，则可能需要在 go build 命令中添加 -mod=vendor 标志。&lt;/p></description></item><item><title>Go 语言反射使用示例</title><link>https://wimi.space/posts/2021/05/golang-reflect-examples/</link><pubDate>Fri, 28 May 2021 09:57:14 +0800</pubDate><guid>https://wimi.space/posts/2021/05/golang-reflect-examples/</guid><description>&lt;h2 id="反射常用操作示例">反射常用操作示例&lt;/h2>
&lt;h3 id="判断-struct-是否实现了指定的-interface">判断 Struct 是否实现了指定的 Interface&lt;/h3>
&lt;pre>&lt;code class="language-go">type I interface {
 Foo()
 Bar()
}

type S struct {}

func (*S) Foo() {
 fmt.Println(&amp;quot;foo&amp;quot;)
}

func (*S) Bar() {
 fmt.Println(&amp;quot;bar&amp;quot;)
}

func main() {

 iType := reflect.TypeOf((*I)(nil)).Elem()

 fmt.Println(reflect.TypeOf(S{}).Implements(iType)) // output: false
 fmt.Println(reflect.TypeOf(&amp;amp;S{}).Implements(iType)) // output: true
}
&lt;/code>&lt;/pre>
&lt;h3 id="反射调用-struct-的指定方法">反射调用 Struct 的指定方法&lt;/h3>
&lt;pre>&lt;code class="language-go">type S struct{}

func (*S) Hello() {
 fmt.Println(&amp;quot;hello world&amp;quot;)
}

func main() {
 ref := reflect.New(reflect.TypeOf(S{}))
 method := ref.MethodByName(&amp;quot;Hello&amp;quot;)
 if method.IsValid() {
 method.Call(nil)
 }
}
&lt;/code>&lt;/pre>
&lt;h3 id="通过函数签名动态调用函数">通过函数签名动态调用函数&lt;/h3>
&lt;pre>&lt;code class="language-go">// foo sum of nums
func foo(nums ...int) int {
 var sum int
 for _, num := range nums {
 sum += num
 }
 return sum
}

// println string
func bar(s string) {
 fmt.Println(s)
}

func exec(call interface{}, args ...interface{}) ([]reflect.Value, error) {
 f := reflect.ValueOf(call)
 if f.Kind() != reflect.Func {
 return nil, errors.New(&amp;quot;call must be func&amp;quot;)
 }

 n := len(args)
 params := make([]reflect.Value, n)
 for i := 0; i &amp;lt; n; i++ {
 params[i] = reflect.ValueOf(args[i])
 }
 return f.Call(params), nil
}

func main() {
 // foo(1, 2, 3, 4, 5) return 15
 res, _ := exec(foo, 1, 2, 3, 4, 5)
 fmt.Println(res[0].Int()) // 15

 // bar(&amp;quot;hello world&amp;quot;)
 _, _ = exec(bar, &amp;quot;hello world&amp;quot;)
}
&lt;/code>&lt;/pre>
&lt;h3 id="通过-tag-修改对应字段的默认值">通过 Tag 修改对应字段的默认值&lt;/h3>
&lt;pre>&lt;code class="language-go">type T struct {
 X int `default:&amp;quot;10&amp;quot;`
 S string `default:&amp;quot;hello&amp;quot;`
 B bool `default:&amp;quot;true&amp;quot;`
}

func (t T) String() string {
 return fmt.Sprintf(&amp;quot;[ %v, %v, %v ] &amp;quot;, t.X, t.S, t.B)
}

func setDefault(tag string, value reflect.Value, field reflect.StructField) {
 // 获取 tag 内容
 df := field.Tag.Get(tag)

 // 判断是否可改（私有属性无法设置）
 if value.CanSet() {
 // 根据类型修改为不同的值
 switch field.Type.Kind() {
 case reflect.String:
 value.SetString(df)
 case reflect.Int:
 i, err := strconv.Atoi(df)
 if err != nil {
 value.SetInt(0)
 } else {
 value.SetInt(int64(i))
 }
 case reflect.Bool:
 value.SetBool(df == &amp;quot;true&amp;quot;)
 case reflect.Float64:
 i, err := strconv.Atoi(df)
 if err != nil {
 value.SetFloat(0)
 } else {
 value.SetFloat(float64(i))
 }
 default:
 }
 }
}

func main() {
 t := T{}
 fmt.Println(t) // output: [ 0, , false ]

 // 反射设置默认值
 tt := reflect.TypeOf(t)
 vt := reflect.ValueOf(&amp;amp;t)
 for i := 0; i &amp;lt; tt.NumField(); i++ {
 field := tt.Field(i)
 value := vt.Elem().Field(i)
 setDefault(&amp;quot;default&amp;quot;, value, field)
 }

 fmt.Println(t) // output: [ 10, hello, true ]
}
&lt;/code>&lt;/pre>
&lt;h2 id="参考文档">参考文档&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.golang.org/laws-of-reflection">The Laws of Reflection&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://halfrost.com/go_reflection/">Go reflection 三定律与最佳实践&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://draveness.me/golang/docs/part2-foundation/ch04-basic/golang-reflect">Go 语言设计与实现｜反射&lt;/a>&lt;/li>
&lt;/ul></description></item></channel></rss>