<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	
	xmlns:georss="http://www.georss.org/georss"
	xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#"
	>

<channel>
	<title>Golang &#8211; Blog of Code</title>
	<atom:link href="https://www.cztcode.com/category/golang/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.cztcode.com</link>
	<description></description>
	<lastBuildDate>Sat, 11 May 2024 05:45:14 +0000</lastBuildDate>
	<language>zh-Hans</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	

<image>
	<url>https://www.cztcode.com/wp-content/uploads/2024/02/cropped-logo-32x32.webp</url>
	<title>Golang &#8211; Blog of Code</title>
	<link>https://www.cztcode.com</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">217219486</site>	<item>
		<title>Golang for range 中 Slice 安全删除元素</title>
		<link>https://www.cztcode.com/2024/5332/</link>
					<comments>https://www.cztcode.com/2024/5332/#respond</comments>
		
		<dc:creator><![CDATA[Jellow]]></dc:creator>
		<pubDate>Tue, 30 Apr 2024 04:15:43 +0000</pubDate>
				<category><![CDATA[Golang]]></category>
		<guid isPermaLink="false">https://www.cztcode.com/?p=5332</guid>

					<description><![CDATA[常见的Golang循环中删除Slice元素方法]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><h1>为什么简单append不安全？</h1>
<p>平时开发经常遇到删除Slice中的元素，但像下面这样是不行的</p>
<pre><code class="language-golang">package main
import &quot;fmt&quot;
func main() {
        // 初始化包含重复数字的切片
        a := []int{1, 2, 3, 4, 5, 5, 6, 5, 7, 8, 9, 10}

        fmt.Println(&quot;初始切片:&quot;, a)

        // 使用 range 遍历切片，尝试删除所有的5
        for i, value := range a {
                if value == 5 {
                        // 删除当前元素
                        a = append(a[:i], a[i+1:]...)
                        fmt.Printf(&quot;删除元素后的切片（当前删除的元素5的索引: %d）: %v\n&quot;, i, a)
                }
        }

        fmt.Println(&quot;最终切片:&quot;, a)
}</code></pre>
<p>运行后输出：</p>
<pre><code class="language-golang">初始切片: [1 2 3 4 5 5 6 5 7 8 9 10]
删除元素后的切片（当前删除的元素5的索引: 4）: [1 2 3 4 5 6 5 7 8 9 10]
删除元素后的切片（当前删除的元素5的索引: 6）: [1 2 3 4 5 6 7 8 9 10]
最终切片: [1 2 3 4 5 6 7 8 9 10]</code></pre>
<p>漏了一个元素5没有删除<br />
原因就在遍历中删除元素后，后续的元素都向前移动了一位，但 range 循环的索引 i 仍然递增，导致实际上跳过了一个本应该检查的元素（因为它现在位于前一个索引位置）。</p>
<h1>解决方法</h1>
<h2>1. 使用索引操作</h2>
<p>可以在遍历时使用额外的索引来控制修改操作，以确保不会因为切片变化而影响遍历结果。</p>
<pre><code class="language-golang">package main

import &quot;fmt&quot;

func main() {
        a := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
        n := len(a) // 获取初始长度
        for i := 0; i &lt; n; i++ {
                if a[i] == 5 {
                        // 删除元素5，并调整切片长度
                        a = append(a[:i], a[i+1:]...)
                        // 减小n和i以适应新的切片长度
                        n--
                        i--
                }
        }

        for _, item := range a {
                fmt.Println(item)
        }
}</code></pre>
<h2>2. 逆序遍历</h2>
<p>另一种方法是从后向前遍历和修改数组，这样即使数组大小变化，也不会影响到未遍历到的元素。</p>
<pre><code class="language-golang">package main

import &quot;fmt&quot;

func main() {
        a := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
        for i := len(a) - 1; i &gt;= 0; i-- {
                if a[i] == 5 {
                        a = append(a[:i], a[i+1:]...)
                }
        }

        for _, item := range a {
                fmt.Println(item)
        }
}</code></pre>
<h2>3. 新建一个数组</h2>
<p>这种方法缺点是要额外空间，但是更好理解和实现</p>
<pre><code class="language-golang">package main

import &quot;fmt&quot;

func main() {
        a := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
        // 创建一个新的切片用于存放删除特定元素后的结果
        var b []int

        for _, item := range a {
                if item != 5 {
                        b = append(b, item)
                }
        }

        // 输出新的切片，已经不包含元素5
        for _, item := range b {
                fmt.Println(item)
        }
}</code></pre>
<p>我比较喜欢第二种，简单的改成倒序就可以，也无需额外空间<br />
关于golang slice的更多细节和扩容介绍可以看我的博客<br />
<a href="https://www.cztcode.com/2023/5094/">https://www.cztcode.com/2023/5094/</a></p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.cztcode.com/2024/5332/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5332</post-id>	</item>
		<item>
		<title>Golang中的切片</title>
		<link>https://www.cztcode.com/2023/5094/</link>
					<comments>https://www.cztcode.com/2023/5094/#respond</comments>
		
		<dc:creator><![CDATA[Jellow]]></dc:creator>
		<pubDate>Tue, 26 Dec 2023 07:52:36 +0000</pubDate>
				<category><![CDATA[Golang]]></category>
		<guid isPermaLink="false">https://www.cztcode.com/?p=5094</guid>

					<description><![CDATA[本文主要深入讨论了Golang语言中切片的特性、定义和动态扩容机制，并对比了切片与数组的主要区别。]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><h1>Golang中的切片</h1>
<h3>1. 定义与特性</h3>
<ul>
<li><strong>切片（Slice）</strong> 是Go语言中一个关键的数据类型，它提供了一个比数组更灵活、更强大的序列接口。</li>
<li>切片并不存储任何数据，它只是对底层数组的引用。</li>
<li>切片可以动态增长和收缩，提供了比数组更高的灵活性。</li>
</ul>
<h3>2. 创建切片</h3>
<ul>
<li><strong>直接声明</strong>：例如 <code>var s []int</code>，这创建了一个nil切片。</li>
<li><strong>从数组创建</strong>：例如 <code>s := arr[start:end]</code>，这根据数组 <code>arr</code> 的一个区间创建切片。</li>
<li><strong>使用make函数</strong>：例如 <code>s := make([]int, length, capacity)</code>，这创建了一个具有指定长度和容量的切片。</li>
</ul>
<h3>3. 切片的长度和容量</h3>
<ul>
<li><strong>长度（Length）</strong>：切片中元素的数量。</li>
<li><strong>容量（Capacity）</strong>：从切片的第一个元素到其底层数组末尾的元素数量。</li>
<li>使用 <code>len(s)</code> 可以获取切片的长度，使用 <code>cap(s)</code> 可以获取切片的容量。</li>
</ul>
<h3>4. 切片操作</h3>
<ul>
<li><strong>重新切片</strong>：可以通过 <code>s = s[start:end]</code> 来调整切片的长度，只要它在原始切片的容量范围内。</li>
<li><strong>追加元素</strong>：<code>append</code> 函数可以向切片追加元素，如果超出容量，会自动扩展。</li>
<li><strong>拷贝切片</strong>：<code>copy</code> 函数可以用来拷贝切片中的元素到另一个切片。</li>
<li><strong>删除元素</strong>：
<ul>
<li>删除切片中的第i位元素: <code>s = append(s[:i], s[i+1:]...)</code></li>
<li>删除切片中的首个元素: <code>s = s[1:]</code></li>
<li>删除切片中的最后一个元素: <code>s = s[:len(s)-1]</code></li>
<li>删除切片中的第i个到j个元素: <code>s = append(s[:i], s[j:]...)</code></li>
</ul>
</li>
</ul>
<h3>5. 切片的内部结构</h3>
<ul>
<li>切片在Go的内部实现中是一个结构体，包含三个字段：
<ul>
<li><strong>指针</strong>：指向底层数组中切片指定的开始位置。</li>
<li><strong>长度</strong>：切片中元素的数量。</li>
<li><strong>容量</strong>：从切片的开始位置到底层数组的末尾的元素数量。</li>
</ul>
</li>
</ul>
<h3>6. 切片的注意事项</h3>
<ul>
<li>切片是引用类型，对切片的修改会影响到底层数组。</li>
<li>切片之间可以共享底层数组，修改一个切片可能会影响另一个切片。</li>
<li>切片的零值是nil，一个nil切片的长度和容量都是0，但它没有底层数组。</li>
</ul>
<h3>7. 使用场景</h3>
<ul>
<li>切片非常适合用于处理动态序列数据。</li>
<li>在函数间传递切片比传递数组更高效，因为切片是引用传递。</li>
<li>切片广泛用于Go标准库中，如字符串处理、文件操作、网络编程等。</li>
</ul>
<h3>示例代码</h3>
<pre><code class="language-go">package main

import &quot;fmt&quot;

func main() {
    // 创建切片
    s := make([]int, 0, 5)

    // 追加元素
    s = append(s, 1, 2, 3)

    // 获取长度和容量
    fmt.Println(&quot;Length:&quot;, len(s), &quot;Capacity:&quot;, cap(s))

    // 切片操作
    s2 := s[1:3]
    fmt.Println(s2)
}
</code></pre>
<p>当多个切片共享同一个底层数组时，对其中一个切片所做的修改可能会影响其他切片。这是因为切片仅是对数组片段的引用，并不拥有数组中的数据。下面详细介绍这个特性：</p>
<h3>共享底层数组的特性</h3>
<ol>
<li><strong>创建共享底层数组的切片</strong>：
<ul>
<li>当你从一个已存在的切片或数组创建新切片时，新切片会引用相同的底层数组。</li>
<li>例如，<code>newSlice := originalSlice[start:end]</code> 创建了一个新切片 <code>newSlice</code>，它引用 <code>originalSlice</code> 的底层数组。</li>
</ul>
</li>
<li><strong>影响范围</strong>：
<ul>
<li>修改任一切片中的元素，如果这些元素在共享的底层数组中，其他切片也会受到影响。</li>
<li>例如，如果 <code>newSlice[0]</code> 是 <code>originalSlice[2]</code> 的同一元素，修改 <code>newSlice[0]</code> 也会改变 <code>originalSlice[2]</code> 的值。</li>
</ul>
</li>
<li><strong>容量与边界</strong>：
<ul>
<li>切片的容量是指从其起始元素到底层数组末尾的元素个数。因此，即使两个切片共享相同的底层数组，它们的容量可能不同，这取决于各自的起始位置。</li>
<li>一个切片对底层数组的修改可能超出其长度但在容量范围内，这可能会影响到其他切片。</li>
</ul>
</li>
</ol>
<h3>示例：共享底层数组的影响</h3>
<pre><code class="language-go">package main

import &quot;fmt&quot;

func main() {
    // 创建一个数组
    arr := [5]int{1, 2, 3, 4, 5}

    // 创建切片，共享相同的底层数组
    slice1 := arr[1:4]  // 包含元素 2, 3, 4
    slice2 := arr[2:5]  // 包含元素 3, 4, 5

    // 修改slice1的一个元素
    slice1[1] = 100  // 将元素3改为100

    // 打印两个切片和原始数组
    fmt.Println(&quot;slice1:&quot;, slice1)  // 输出：slice1: [2 100 4]
    fmt.Println(&quot;slice2:&quot;, slice2)  // 输出：slice2: [100 4 5]
    fmt.Println(&quot;arr:&quot;, arr)        // 输出：arr: [1 2 100 4 5]
}
</code></pre>
<h3>注意事项</h3>
<ul>
<li>当函数接收切片参数时，如果函数内部修改了切片元素，调用该函数的外部环境中相应的切片也会受到影响。</li>
<li>在需要独立修改数据而不影响原始数据时，应考虑使用 <code>copy</code> 函数来创建一个新的切片副本。</li>
</ul>
<p>理解这一点对于避免数据共享带来的意外副作用非常重要，尤其是在并发编程和复杂数据结构处理中。</p>
<h1>数组与切片的区别</h1>
<h3><strong>数组的定义</strong></h3>
<p>数组的定义需要指定元素的数量（长度固定），格式为 <strong><code>[n]T</code></strong>，其中 <strong><code>n</code></strong> 是数组的长度，<strong><code>T</code></strong> 是数组中元素的类型。例如：</p>
<ul>
<li>定义一个长度为5的整数数组：<strong><code>var arr [5]int</code></strong></li>
<li>初始化数组时指定所有元素的值：<strong><code>arr := [5]int{1, 2, 3, 4, 5}</code></strong></li>
<li>让编译器根据初始化值的数量自动确定数组长度：<strong><code>arr := [...]int{1, 2, 3, 4, 5}</code></strong></li>
</ul>
<h3><strong>切片的定义</strong></h3>
<p>切片的定义不需要指定元素的数量，格式为 <strong><code>[]T</code></strong>，只指定元素类型 <strong><code>T</code></strong>。切片的长度和容量在运行时可以改变。例如：</p>
<ul>
<li>定义一个整数切片：<strong><code>var s []int</code></strong></li>
<li>使用 <strong><code>make</code></strong> 创建切片并指定长度和容量：<strong><code>s := make([]int, length, capacity)</code></strong></li>
<li>从数组或另一个切片初始化切片：<strong><code>s := arr[start:end]</code></strong></li>
</ul>
<h3><strong>关键区别</strong></h3>
<ul>
<li><strong>长度</strong>：数组的长度是其类型的一部分，一旦声明就不能改变。切片的长度是动态的，可以通过 <strong><code>append</code></strong> 和重新切片操作来改变。</li>
<li><strong>类型表示</strong>：在类型表示上，数组需要在类型前指定长度（例如 <strong><code>[5]int</code></strong>），而切片不需要（例如 <strong><code>[]int</code></strong>）。</li>
</ul>
<p>这些区别导致了数组和切片在Go语言中的使用方式和场景上的不同。<em>arrays</em> 通常用于较小的或固定大小的数据集合，而 <em>slices</em> 用于更加动态和灵活的数据处理。</p>
<p>数组和切片在Go语言中都用于存储序列数据，但它们在使用上有明显的区别：</p>
<ol>
<li><strong>长度的固定性</strong>：
<ul>
<li><strong>数组</strong>：长度固定，声明时需指定数组的大小，且大小不能改变。</li>
<li><strong>切片</strong>：长度可变，可以动态地增加或减少元素。</li>
</ul>
</li>
<li><strong>数据类型</strong>：
<ul>
<li><strong>数组</strong>：是值类型，赋值和作为参数传递时会复制整个数组。</li>
<li><strong>切片</strong>：是引用类型，赋值和作为参数传递时只会复制切片本身（底层数组不会被复制）。</li>
</ul>
</li>
<li><strong>内存分配</strong>：
<ul>
<li><strong>数组</strong>：在编译时分配固定大小的内存。</li>
<li><strong>切片</strong>：可以在运行时动态分配和调整大小。</li>
</ul>
</li>
<li><strong>性能考虑</strong>：
<ul>
<li><strong>数组</strong>：由于是值类型，较大的数组在作为参数传递时可能会有性能问题。</li>
<li><strong>切片</strong>：更适合作为参数传递，因为只传递引用。</li>
</ul>
</li>
</ol>
<h2>作为函数参数传递</h2>
<h3>数组</h3>
<p>当数组作为参数传递给函数时，会发生数组的整体拷贝。也就是对于数组的修改不会影响到原来的数组。对于较大的数组，这可能导致性能问题。</p>
<h3>切片</h3>
<p>当切片作为函数参数传递时，实际上传递的是切片的副本。这个副本引用的是同一个底层数组。这意味着：</p>
<ul>
<li><strong>传递值</strong>：虽然切片本身是以值的形式传递的（即传递切片的副本），但由于切片内部包含对底层数组的引用，所以对切片内容的修改会影响原始切片。</li>
<li><strong>改变元素</strong>：如果在函数内部改变了切片的元素，这些改变会反映到原始切片上。</li>
<li><strong>改变大小</strong>：如果尝试改变切片的大小（例如，通过 <code>append</code> 函数添加元素），情况就会变得复杂：
<ul>
<li>如果改变后的大小不超过原切片的容量，那么这个改变也会反映到原始切片上。</li>
<li>如果改变导致切片扩容（即超出原有容量），则会分配一个新的底层数组。这时，函数内部的切片副本会指向这个新的数组，而外部的原始切片仍然指向旧的数组。</li>
</ul>
</li>
</ul>
<h3>传递切片的引用</h3>
<p>在Go中，没有直接的方式来传递切片的“引用”。但如果您想在函数内部改变切片的长度并反映到原始切片，您可以：</p>
<ul>
<li>返回修改后的切片，并在调用函数后使用这个返回值。</li>
<li>使用指向切片的指针作为参数（虽然这不是常见的做法）。</li>
</ul>
<h3>示例</h3>
<p>这个例子展示了当切片作为函数参数传递时，如何改变其大小：</p>
<pre><code class="language-go">package main

import &quot;fmt&quot;

func appendSlice(slice []int, values ...int) []int {
    return append(slice, values...)
}

func main() {
    originalSlice := []int{1, 2, 3}
    modifiedSlice := appendSlice(originalSlice, 4, 5)

    fmt.Println(&quot;Original Slice:&quot;, originalSlice)
    fmt.Println(&quot;Modified Slice:&quot;, modifiedSlice)
}
</code></pre>
<p>在这个例子中，<code>appendSlice</code> 函数返回一个新的切片，这个切片可能引用了一个新的底层数组（如果进行了扩容的话）。这样，可以在函数外部接收这个改变后的切片。</p>
<p>总结来说，切片作为函数参数传递时，虽然传递的是切片的副本，但由于切片内部包含对底层数组的引用，所以对其元素的修改会影响到原始切片。但是，如果需要改变切片的大小，并希望这种改变反映到原切片上，需要特别处理，比如通过返回新切片或使用指向切片的指针。</p>
<h1>切片的扩容机制</h1>
<p>切片的扩容是Go语言切片最重要的特性之一。切片的扩容过程如下：</p>
<ol>
<li><strong>初始容量</strong>：当使用 <code>make</code> 函数创建切片时，可以指定切片的初始容量。如果未指定，切片的容量默认为其长度。</li>
<li><strong>追加元素</strong>：当使用 <code>append</code> 函数向切片追加元素，且当前切片容量不足以容纳更多元素时，切片会自动扩容。</li>
</ol>
<h2>Go 1.17版本切片扩容</h2>
<p>Go 1.17切片扩容时会进行内存对齐，这个和内存分配策略相关。进行内存对齐之后，新 slice 的容量是要 大于等于老 slice 容量的 2倍或者1.25倍。</p>
<ul>
<li>当新切片需要的容量cap大于两倍扩容的容量，则直接按照新切片需要的容量扩容；</li>
<li>当原 slice 容量 &lt; 1024 的时候，新 slice 容量变成原来的 2 倍；</li>
<li>当原 slice 容量 &gt; 1024，进入一个循环，每次容量变成原来的1.25倍,直到大于期望容量。</li>
</ul>
<h2>Go 1.18版本切片扩容</h2>
<p>Go1.18不再以1024为临界点，而是设定了一个值为256的<code>threshold</code>，以256为临界点；超过256，不再是每次扩容1/4，而是每次增加（旧容量+3*256）/4；</p>
<ul>
<li>当新切片需要的容量cap大于两倍扩容的容量，则直接按照新切片需要的容量扩容；</li>
<li>当原 slice 容量 &lt; threshold 的时候，新 slice 容量变成原来的 2 倍；</li>
<li>当原 slice 容量 &gt; threshold，进入一个循环，每次容量增加（旧容量+3*threshold）/4。</li>
</ul>
<p>最后还有一个内存对齐步骤，实际容量会比这个公式计算出来的大。</p>
<h3>内存对齐</h3>
<p>内存对齐是计算机硬件对内存访问的一种优化。对于现今的计算机硬件设计，内存是按照一个固定大小（如4 bytes, 8 bytes, 16 bytes等）的块来读取或者写入的。如果数据没有按照这样的块边界来存放，硬件可能需要多次访问内存才能读取或者写入数据，这会降低程序的性能。</p>
<p>以4 bytes为例，如果我们要读取一个4 bytes的数据，在内存中的起始地址如果不是4的倍数，硬件需要两次读取操作才能获取到完整的4 bytes数据。第一次读取会获取到数据的一部分，然后第二次读取才能获取到剩余的数据。这是因为硬件只能从地址是4的倍数的地方开始读取4 bytes的数据。</p>
<p>反之，如果数据在内存中的起始地址是4的倍数，硬件只需要一次读取操作就能获取到完整的4 bytes数据。这就是为什么内存对齐可以提高性能的原因。</p>
<p>因此在Go 1.18版本中，切片的扩容操作在确定扩容容量后，还会做一个内存对齐的步骤，确保扩容后的切片在内存中的存放位置能满足硬件的最优访问需求。</p>
<h3>源代码注释</h3>
<pre><code class="language-jsx">growth formula a bit smoother
Instead of growing 2x for &lt; 1024 elements and 1.25x for &gt;= 1024 elements,
use a somewhat smoother formula for the growth factor. Start reducing
the growth factor after 256 elements, but slowly.

starting cap    growth factor
256             2.0
512             1.63
1024            1.44
2048            1.35
4096            1.30

(Note that the real growth factor, both before and now, is somewhat
larger because we round up to the next size class.)

This CL also makes the growth monotonic (larger initial capacities
make larger final capacities, which was not true before). See discussion
at https://groups.google.com/g/golang-nuts/c/UaVlMQ8Nz3o

256 was chosen as the threshold to roughly match the total number of
reallocations when appending to eventually make a very large
slice. (We allocate smaller when appending to capacities [256,1024]
and larger with capacities [1024,...]).</code></pre>
<p>通过减小阈值并固定增加一个常数，使得优化后的扩容的系数在阈值前后不再会出现从2到1.25的突变，该commit作者给出了几种原始容量下对应的“扩容系数”：</p>
<table>
<thead>
<tr>
<th>oldcap</th>
<th>扩容系数</th>
</tr>
</thead>
<tbody>
<tr>
<td>256</td>
<td>2.0</td>
</tr>
<tr>
<td>512</td>
<td>1.63</td>
</tr>
<tr>
<td>1024</td>
<td>1.44</td>
</tr>
<tr>
<td>2048</td>
<td>1.35</td>
</tr>
<tr>
<td>4096</td>
<td>1.30</td>
</tr>
</tbody>
</table>
<p>可以看到，Go1.18的扩容策略中，随着容量的增大，其扩容系数是越来越小的，可以更好地节省内存。注意这个不是一个间断函数，而是连续函数，上面的表只是在oldcap取不同值时计算出来的“扩容系数”，实际上对于不同的oldcap，扩容系数是逐渐变化的。当oldcap远大于256的时候，扩容系数将会无限接近1.25。</p>
<h3>测试扩容</h3>
<pre><code class="language-go">package main

import (
    &quot;testing&quot;
)

func TestCap(t *testing.T) {
    var s []int
    lastCap := cap(s)

    for i := 0; i &lt; 1000; i++ {
        s = append(s, i)
        newCap := cap(s)

        if newCap != lastCap {
            expectedCap := calculateExpectedCap(lastCap, i+1) // i+1 是因为添加了一个新元素
            t.Logf(&quot;Added element %d, length: %d, actual capacity: %d, expected capacity: %d\n&quot;, i, len(s), newCap, expectedCap)
            lastCap = newCap
        }
    }
}

func calculateExpectedCap(oldcap, cap int) int {
    threshold := 256
    if cap &gt; oldcap*2 {
        return cap
    }
    if oldcap &lt; threshold {
        return oldcap * 2
    }
    return oldcap + (oldcap+3*threshold)/4
}</code></pre>
<p>输出结果</p>
<pre><code class="language-jsx">    cap_test.go:17: Added element 0, length: 1, actual capacity: 1, expected capacity: 1
    cap_test.go:17: Added element 1, length: 2, actual capacity: 2, expected capacity: 2
    cap_test.go:17: Added element 2, length: 3, actual capacity: 4, expected capacity: 4
    cap_test.go:17: Added element 4, length: 5, actual capacity: 8, expected capacity: 8
    cap_test.go:17: Added element 8, length: 9, actual capacity: 16, expected capacity: 16
    cap_test.go:17: Added element 16, length: 17, actual capacity: 32, expected capacity: 32
    cap_test.go:17: Added element 32, length: 33, actual capacity: 64, expected capacity: 64
    cap_test.go:17: Added element 64, length: 65, actual capacity: 128, expected capacity: 128
    cap_test.go:17: Added element 128, length: 129, actual capacity: 256, expected capacity: 256
    cap_test.go:17: Added element 256, length: 257, actual capacity: 512, expected capacity: 512
    cap_test.go:17: Added element 512, length: 513, actual capacity: 848, expected capacity: 832
    cap_test.go:17: Added element 848, length: 849, actual capacity: 1280, expected capacity: 1252
--- PASS: TestCap (0.00s)
PASS</code></pre>
<h3>为什么实际扩容与预期不符呢？</h3>
<p>实际上，growslice 的后半部分还有更进一步的优化（内存对齐等），靠的是 roundupsize 函数，在计算完 newcap 值之后，还会有一个步骤计算最终的容量：</p>
<pre><code class="language-go">capmem = roundupsize(uintptr(newcap) * ptrSize)
newcap = int(capmem / ptrSize)</code></pre>
<p><strong><code>growslice</code></strong> 函数在 Go 中的实现不仅仅是简单的容量增长算法，它还涉及到内存对齐的优化，这是通过 <strong><code>roundupsize</code></strong> 函数来实现的。<strong><code>roundupsize</code></strong> 函数确保分配的内存大小是特定大小类的倍数，这有助于减少内存碎片和提高内存分配效率。我们无法直接模拟 <strong><code>roundupsize</code></strong> 函数的行为，因为这需要深入 Go 的内存分配器的实现细节。</p>
<p>但是确定的是，最终的容量可能会比计算出的 <strong><code>newcap</code></strong> 更大，因为要适应内存对齐的需求。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.cztcode.com/2023/5094/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5094</post-id>	</item>
		<item>
		<title>Golang中的map与线程安全</title>
		<link>https://www.cztcode.com/2023/5075/</link>
					<comments>https://www.cztcode.com/2023/5075/#respond</comments>
		
		<dc:creator><![CDATA[Jellow]]></dc:creator>
		<pubDate>Wed, 13 Dec 2023 12:28:26 +0000</pubDate>
				<category><![CDATA[Golang]]></category>
		<guid isPermaLink="false">https://www.cztcode.com/?p=5075</guid>

					<description><![CDATA[golang中的map与线程安全 map介绍 ​ Golang中Map存储的是kv键值对，采用哈希表作为底层实现，用拉链法解决hash冲突。 基本特性 键值对存储：map 用于存储键值对，其中每个键都是唯一的，且映射到一个特定的值。 动态类型：在 Go 中，map 的键和值可以是任意类型，但所有键必须是相同的类型，所有值也必须是相同的类型。 动态大小：map 的大小是动态的，可以根据需要增长或缩减 [&#8230;]]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><h1>golang中的map与线程安全</h1>
<h2>map介绍</h2>
<p>​   Golang中Map存储的是kv键值对，采用哈希表作为底层实现，用拉链法解决hash冲突。</p>
<h3>基本特性</h3>
<ol>
<li><strong>键值对存储</strong>：<code>map</code> 用于存储键值对，其中每个键都是唯一的，且映射到一个特定的值。</li>
<li><strong>动态类型</strong>：在 Go 中，<code>map</code> 的键和值可以是任意类型，但所有键必须是相同的类型，所有值也必须是相同的类型。</li>
<li><strong>动态大小</strong>：<code>map</code> 的大小是动态的，可以根据需要增长或缩减。这意味着你不需要事先知道 <code>map</code> 将存储多少元素。</li>
<li><strong>无序集合</strong>：<code>map</code> 是一个无序的数据结构，这意味着你不能指望键值对的存储或检索顺序。在理想情况下（即哈希函数分布均匀），键的访问（检索和更新）操作的时间复杂度是 O(1)。这意味着无论 <code>map</code> 中存储了多少元素，访问一个键所需的时间大致相同。</li>
<li><strong>高效的键访问</strong>：<code>map</code> 提供了高效的键访问。通过键，你可以快速检索或更新对应的值。</li>
<li><strong>哈希表实现</strong>：底层使用哈希表来实现，这使得 <code>map</code> 在大多数情况下能够提供快速的查找、添加和删除操作。
<ol>
<li>查找操作：通常是 O(1)。但在最坏的情况下（所有键都映射到同一个桶中），时间复杂度可能会退化到 O(n)。</li>
<li>添加操作：同样，理想情况下是 O(1)，但在需要扩容时（即当存储元素的数量超过当前容量的负载因子时），时间复杂度可能会暂时增加，因为需要重新哈希现有的元素到新的存储空间。</li>
<li>删除操作：理想情况下也是 O(1)。</li>
</ol>
</li>
<li><strong>并发使用注意</strong>：Go 的 <code>map</code> 在没有额外同步机制的情况下并不是并发安全的。如果你需要在多个协程（goroutine）中并发访问 <code>map</code>，需要使用互斥锁（mutex）或者其他同步技术来避免并发冲突。</li>
<li><strong>内存效率</strong>：由于 <code>map</code> 使用动态扩容机制，因此在使用时相对内存高效，只有在需要更多空间时才会扩展。</li>
</ol>
<p>对于map的实现，golang源码是这样介绍的</p>
<pre><code class="language-golang">// A map is just a hash table. The data is arranged
// into an array of buckets. Each bucket contains up to
// 8 key/value pairs. The low-order bits of the hash are
// used to select a bucket. Each bucket contains a few
// high-order bits of each hash to distinguish the entries
// within a single bucket.
//
// If more than 8 keys hash to a bucket, we chain on
// extra buckets.</code></pre>
<h2>基础使用</h2>
<h3>插入方法</h3>
<pre><code class="language-golang">// 声明一个 map
var map1 map[keyType]valueType

// 使用 make 函数初始化
map1 = make(map[keyType]valueType)

//初始化时指定大小，超过时依然会自动扩容
m := make(map[keyType]valueType, initialCapacity)

// 简洁声明并初始化
map2 := make(map[keyType]valueType)

// 初始化时直接填充数据
map3 := map[keyType]valueType{
    key1: value1,
    key2: value2,
    // ...
}</code></pre>
<h3>读取方法</h3>
<p>读取Map中的元素时，Go提供了一种独特的方式来检查元素是否存在。例如：</p>
<pre><code>golang
value, ok := map1[key]</code></pre>
<p>这里，<code>value</code> 是与键对应的值，而 <code>ok</code> 是一个布尔值，表示该键是否在Map中。如果键存在，<code>ok</code> 为 <code>true</code>，否则为 <code>false</code>。</p>
<h3>遍历方法</h3>
<p>遍历Map的常见方法是使用 <code>for</code> 循环：</p>
<pre><code class="language-golang">for key, value := range map1 {
    fmt.Println(&quot;Key:&quot;, key, &quot;Value:&quot;, value)
}</code></pre>
<p>但是，Go中的Map是无序的。如果你需要按顺序遍历，可以使用两种方法：使用 <code>orderedmap</code> 包或先读取键，对它们排序，然后按顺序遍历。</p>
<h4>使用 orderedmap</h4>
<p><code>orderedmap</code> 是一个第三方包，允许你按照插入顺序遍历Map。使用之前，需要先安装这个包。</p>
<pre><code class="language-golang">import &quot;github.com/wk8/go-ordered-map&quot;

omap := orderedmap.New()
// 后续操作与普通map类似</code></pre>
<h4>读取键并排序</h4>
<p>如果不想使用第三方库，可以先提取所有键，对它们排序，然后按顺序访问Map：</p>
<pre><code class="language-golang">var keys []int
for k := range map1 {
    keys = append(keys, k)
}
sort.Ints(keys) // 对键进行排序

for _, k := range keys {
    fmt.Println(&quot;Key:&quot;, k, &quot;Value:&quot;, map1[k])
}</code></pre>
<h2>桶</h2>
<p>​   Go <code>map</code> 的核心是由一系列称为“桶”的数据结构组成的。每个桶可以存储多个键值对（通常是 8 个）。这些桶的目的是通过哈希函数将键均匀地分散到各个桶中，从而减少哈希冲突。</p>
<p>bmap结构，即桶，是map中最重要的底层实现之一，其设计要点如下：</p>
<ul>
<li><strong>桶是map中最小的挂载粒度</strong>：map中不是每一个key都申请一个结构通过链表串联，而是每8个kv键值对存放在一个桶中，然后桶再通以链表的形式串联起来，这样做的原因就是减少对象的数量，减轻gc的负担。</li>
<li><strong>桶串联实现拉链法</strong>：当某个桶数量满了，会申请一个新桶，挂在这个桶后面形成链表，新桶优先使用预分配的桶。</li>
<li><strong>哈希高8位优化桶查找key</strong> : 将key哈希值的高8位存储在桶的tohash数组中，这样查找时不用比较完整的key就能过滤掉不符合要求的key，tohash中的值相等，再去比较key值</li>
<li><strong>桶中key/value分开存放</strong> ： 桶中所有的key存一起，所有的value存一起，目的是为了方便内存对齐</li>
<li><strong>根据k/v大小存储不同值</strong> ： 当k或v大于128字节时，其存储的字段为指针，指向k或v的实际内容，小于等于128字节，其存储的字段为原值</li>
<li><strong>桶的搬迁状态</strong> ： 可以根据tohash字段的值，是否小于minTopHash，来表示桶是否处于搬迁状态</li>
</ul>
<h2>负载因子（Load Factor）</h2>
<h3>传统负载因子</h3>
<h4>定义与基本概念</h4>
<ul>
<li>
<p><strong>负载因子</strong>是用来衡量哈希表中空间占用率的一个核心指标。它定义为哈希表中已存储的元素数量与哈希表的总位置（槽数）数量的比例。数学上表示为：</p>
<p><code> $$ \text{负载因子} = \frac{\text{已存储的元素数量}}{\text{哈希表的总位置数量}} $$</code></p>
</li>
<li>
<p><strong>衡量指标</strong>：负载因子是衡量哈希表效率和空间利用的关键指标。它反映了表中有多少位置被占用，以及还有多少位置可用。</p>
</li>
</ul>
<h4>重要性</h4>
<ul>
<li>
<p><strong>性能影响</strong>：负载因子的值直接影响哈希表的性能。一个较高的负载因子意味着哈希表的位置被大量占用，可能增加哈希冲突，降低查找效率。而一个较低的负载因子意味着哈希表有许多空闲位置，这可能导致内存的浪费。</p>
</li>
<li>
<p><strong>扩容触发点</strong>：在哈希表的实现中，通常会设置一个负载因子的阈值。当表中的负载因子超过这个阈值时，哈希表将进行扩容，这通常包括分配更大的存储空间并重新分配（rehash）现有元素。</p>
</li>
</ul>
<h3>Go 语言中的负载因子</h3>
<p>​   在 Go 语言的 <code>map</code> 实现中，负载因子与桶（bucket）的平均填充元素数相关联。这里的负载因子并不是传统意义上的哈希表负载因子，而是与每个桶的平均键值对数量相关的一个特定实现细节。</p>
<p><code class="katex-inline">\text{负载因子} = \frac{\text{已存储的元素数量}}{\text{哈希表中桶的数量}}</code></p>
<ul>
<li>
<p><strong>桶的平均元素数量</strong>：在 Go 语言中，当每个桶的平均填充元素数量达到特定值（如 6.5）时，<code>map</code> 将进行扩容。这意味着每个桶平均存储约 6.5 个键值对。</p>
</li>
<li>
<p><strong>扩容策略</strong>：这种特定的扩容策略有助于保持 <code>map</code> 性能的平衡。它旨在优化内存使用和查找效率之间的关系，减少哈希冲突的同时避免过度占用内存空间。</p>
</li>
</ul>
<h2>哈希表（Hash Table）</h2>
<p>​   哈希表是一种用于存储键值对的数据结构，它通过一个哈希函数将键映射到表中的一个位置（称为“槽”或“桶”）来存储数据。哈希表的主要特点是它可以提供快速的数据查找，通常是常数时间复杂度 <em>O</em>(1)。哈希表的效率高度依赖于哈希函数的质量和处理冲突的方式。</p>
<h3>哈希函数</h3>
<p>​   哈希函数是哈希表的核心，它将输入（键）转换为一个整数，这个整数通常用作数组的索引。理想的哈希函数应该满足以下条件：</p>
<ul>
<li>快速计算</li>
<li>哈希值均匀分布</li>
<li>相同的输入总是产生相同的哈希值</li>
<li>不同的输入尽量产生不同的哈希值（尽管完全避免冲突是不可能的）</li>
</ul>
<h3>处理冲突</h3>
<p>​   当两个不同的键产生相同的哈希值时，就发生了冲突。处理哈希冲突的常见方法有两种：开放寻址法和拉链法。</p>
<p>​   开放寻址法（Open Addressing）是处理哈希表中冲突的另一种常用技术。与拉链法不同，开放寻址法不使用链表来存储冲突的元素，而是在哈希表数组本身中寻找空闲位置来存储这些元素。这种方法直接在哈希表的数组中存储所有元素，不需要额外的数据结构。</p>
<h3>开放寻址法</h3>
<h4>工作原理</h4>
<ol>
<li>
<p><strong>插入操作</strong>：当一个新元素需要插入哈希表时，先使用哈希函数计算其索引。如果该索引对应的位置已被占用（发生冲突），则根据某种探测序列（probe sequence）在表中寻找下一个空闲位置。</p>
</li>
<li>
<p><strong>探测序列</strong>：探测序列定义了在发生冲突时如何在哈希表中搜索空闲位置。常见的探测方法包括线性探测、二次探测和双重哈希探测。</p>
<ul>
<li><strong>线性探测（Linear Probing）</strong>：连续检查下一个槽位，直到找到空位。</li>
<li><strong>二次探测（Quadratic Probing）</strong>：探测间隔以二次方增加（例如，1, 4, 9, 16, &#8230;）。</li>
<li><strong>双重哈希（Double Hashing）</strong>：使用两个哈希函数，第二个哈希函数定义探测间隔。</li>
</ul>
</li>
<li>
<p><strong>查找操作</strong>：查找元素时，也使用哈希函数计算索引，然后遵循相同的探测序列进行搜索，直到找到目标元素或遇到空槽位（表示元素不存在）。</p>
</li>
<li>
<p><strong>删除操作</strong>：删除元素时，不能简单地将槽位设置为空，因为这会中断探测序列。通常的做法是将删除的槽位标记为已删除（而非空闲），以便探测序列可以继续通过这个槽位。</p>
</li>
</ol>
<h4>优点和缺点</h4>
<ul>
<li>
<p><strong>优点</strong>：</p>
<ul>
<li>内存利用率高，因为所有元素都存储在哈希表数组中，不需要额外的数据结构。</li>
<li>可以避免链表可能带来的缓存不友好的问题。</li>
</ul>
</li>
<li>
<p><strong>缺点</strong>：</p>
<ul>
<li>当哈希表填满时，性能可能急剧下降，因此需要及时扩容。</li>
<li>删除操作比较复杂，可能需要特殊的标记处理。</li>
<li>集群（clustering）问题：特别是在使用线性探测时，连续的占用槽位可能导致新插入的元素需要经过更长的探测序列。</li>
</ul>
</li>
</ul>
<h4>应用场景</h4>
<p>​   开放寻址法适用于元素数量较少或负载因子较低的情况。在这些情况下，开放寻址法的简单性和内存连续性可以带来性能上的优势。然而，随着元素数量的增加，特别是当负载因子接近或超过 0.7 时，拉链法通常会提供更好的性能。</p>
<p>开放寻址法是理解和实现哈希表的重要方面之一，对于需要高效空间利用和快速访问的场景特别有用。</p>
<h3>拉链法（Chaining）</h3>
<p>​   拉链法是处理哈希表中冲突的一种常用技术。它的基本思想是在哈希表的每个槽（桶）中存储一个链表。当多个键映射到同一个槽时，这些键值对会被添加到对应槽的链表中。</p>
<h4>拉链法的特点</h4>
<ul>
<li><strong>添加操作</strong>：当插入一个新的键值对时，首先使用哈希函数确定其槽位置，然后将键值对添加到该槽的链表中。</li>
<li><strong>查找操作</strong>：查找时，同样先计算哈希值以确定槽位置，然后在对应的链表中搜索具有相应键的元素。</li>
<li><strong>删除操作</strong>：删除操作也是先定位到具体的槽，然后在链表中找到并删除相应的元素。</li>
</ul>
<h4>优点和缺点</h4>
<ul>
<li><strong>优点</strong>：拉链法简单且在冲突较多的情况下仍然能保持较好的性能。它允许哈希表的负载因子超过 1，即存储的元素数量可以超过哈希表的槽数量。</li>
<li><strong>缺点</strong>：在最坏的情况下（所有键都映射到同一个槽），查找性能会退化到 O<em>(</em>n<em>)，其中 </em>n* 是键值对的数量。</li>
</ul>
<h4>拉链法在golang中map的应用</h4>
<p>​   在 Go 语言中，<code>map</code> 的底层实现采用了一种结合桶（bucket）和链表的方法，这与传统拉链法有些相似，但也存在差异。</p>
<p>​   在 Go 的 <code>map</code> 中，每个桶（由 <code>bmap</code> 结构体表示）可以存储固定数量的键值对，通常是 8 对。这意味着，不是每一个键值对都单独分配一个结构，而是多个键值对共享一个桶。这种设计减少了对象的数量，从而减轻了垃圾回收（GC）的负担。</p>
<p>​   当一个桶填满时，Go <code>map</code> 会申请一个新的桶，并将其连接到原有的桶上，形成一种链表结构。这个新桶优先使用预分配的桶。当需要在 <code>map</code> 中插入新的键值对时，会根据哈希值定位到一个特定的桶。如果该桶已满，系统会沿着链表寻找或创建新的桶来存放这个键值对。</p>
<p>​   这种方式在某种程度上体现了拉链法的思想，即通过链表来解决哈希冲突。但与传统的拉链法（每个槽位对应一个链表）不同，Go 的实现是将多个键值对存储在同一个桶内，只有在这个桶填满时，才会通过链表的方式连接到新的桶。这样既保留了拉链法处理冲突的优势，又通过限制单个桶内键值对的数量，提高了内存的使用效率和查找效率。</p>
<p>​   总的来说，Go <code>map</code> 的这种设计是对传统拉链法的一种改进和优化，它在维持良好的性能的同时，也考虑到了内存的有效使用。</p>
<h4>hash值的使用</h4>
<p>通过哈希函数，key可以得到一个唯一值，这个值根据操作系统位数，可能为32位或64位。map将这个唯一值，分成高位和低位，分别有不同的用途：</p>
<ul>
<li><strong>低位</strong>：用于寻找当前key属于哪个bucket</li>
<li><strong>高位</strong>：用于寻找当前key在bucket中的位置，bucket有个tohash字段，便是存储的高位的值，用来声明当前bucket中有哪些key，这样搜索查找时就不用遍历bucket中的每个key，只要先看看tohash数组值即可，提高搜索查找效率</li>
</ul>
<blockquote>
<p>map其使用的hash算法会根据硬件选择，比如如果cpu是否支持aes，那么采用aes哈希（从硬件层面提高效率），并且将hash值映射到bucket时，会采用位运算来规避mod的开销</p>
</blockquote>
<h2>插入过程</h2>
<h4>插入新值时的过程</h4>
<ol>
<li>调用哈希函数为给定键生成一个哈希码。</li>
<li>基于哈希码的一部分确定存储键值对的桶。</li>
<li>选定桶后，将新条目存储在该桶中。</li>
<li>将传入键的完整哈希码与初始哈希码数组中的所有哈希码（如h1, h2&#8230;）进行比较。</li>
<li>如果没有匹配的哈希码，意味着这是一个新条目。</li>
<li>如果桶中有空位，则在键值对列表末尾存储新条目。</li>
<li>否则，创建一个新桶，并将新条目存储在新桶中，旧桶的溢出指针指向这个新桶。</li>
</ol>
<h2>动态扩容</h2>
<h3>扩容过程</h3>
<ol>
<li><strong>触发扩容条件</strong>：
<ul>
<li>当 <code>map</code> 的装载因子超过 6.5 时，即元素数量相对于桶数量太多，需要进行扩容。</li>
<li>当使用的溢出桶（<code>overflow buckets</code>）数量过多时，即使桶的数量没有超过装载因子，也需要进行扩容。这种情况下，扩容的目的主要是内存整理，而不是增加桶的数量。</li>
</ul>
</li>
<li><strong>扩容操作</strong>：
<ul>
<li>扩容会新分配一个更大的数组（桶）。如果是因为装载因子过高，扩容会增加桶的数量（通常是翻倍）；如果是因为溢出桶过多，扩容后的桶数量和原来一样。</li>
<li>在扩容过程中，<code>map</code> 结构体会做一些重新赋值操作，比如切换老的桶（<code>oldbuckets</code>）。</li>
</ul>
</li>
</ol>
<h3>为什么负载因子是 6.5</h3>
<p>​   这个值是根据性能测试选择的。在这些测试中，考虑了如下因素：</p>
<ul>
<li><strong>溢出率</strong>：桶溢出的比例。溢出率过高意味着许多键被映射到了相同的桶，增加了冲突。</li>
<li><strong>内存开销</strong>：每个键值对的平均内存占用。</li>
<li><strong>查找性能</strong>：查找存在或不存在的键所需的平均步骤数。</li>
</ul>
<p>​   通过测试不同的负载因子，Go 团队发现，当负载因子为 6.5 时，这些因素之间达到了最佳平衡，即内存利用率和查找效率都处于理想状态。</p>
<p>​   注意是平均6.5，前面说每个桶最多装8个元素。</p>
<h3>渐进式搬迁</h3>
<p>​   为了避免一次性迁移大量数据带来的性能问题，Go <code>map</code> 采用了渐进式搬迁的方法。这意味着数据的迁移不是一次性完成的，而是在后续的 <code>map</code> 操作中逐渐进行。每次对 <code>map</code> 进行插入、删除或查找操作时，都会触发部分数据的搬迁。</p>
<p>​   在扩容过程中，<code>map</code> 维护了一个指向旧桶数组的指针（<code>oldbuckets</code>），以及一个迁移进度指示器（<code>nevacuate</code>）。在每次 <code>map</code> 操作期间，会根据这个进度指示器将一部分数据从旧桶迁移到新桶中。这个过程会持续进行，直到所有数据都迁移到了新桶中。</p>
<p>​   这种渐进式搬迁的方法可以在扩容期间分摊性能开销，避免了一次性重分配大量内存和数据迁移带来的性能下降。一旦所有数据都从旧桶迁移到新桶中后，<code>oldbuckets</code> 将被设置为 <code>nil</code>，这表示扩容和数据迁移已经完全完成。</p>
<p>​   渐进式搬迁策略使得 Go <code>map</code> 在处理大量数据时仍能保持较高的性能，特别是在动态扩容的情况下。这种策略的一个关键优点是，它允许 <code>map</code> 在扩容过程中继续进行高效的数据操作。</p>
<h3>缩容机制</h3>
<p>​   在 Go 语言中，<code>map</code> 的缩容机制并不像其扩容机制那样明确和直接。Go 语言的 <code>map</code> 实现主要支持扩容操作，以应对元素数量的增长，但它不支持显式的缩容过程，即在元素被删除后释放内存。这意味着，一旦 <code>map</code> 扩容后，即使删除了一些元素，它所占用的内存空间不会自动减少。</p>
<p>关于 <code>map</code> 的扩缩容机制，有以下几点需要注意：</p>
<ol>
<li>
<p><strong>伪缩容</strong>：目前 Go 语言中的 <code>map</code> 实现被认为是进行了“伪缩容”。这主要是指，虽然可以减少 <code>map</code> 中的元素数量，但 <code>map</code> 占用的内存空间实际上并不会减少。即使是在 <code>map</code> 中的元素被大量删除后，已经分配的内存也不会被释放。</p>
</li>
<li>
<p><strong>内存回收</strong>：如果需要减少 <code>map</code> 的内存占用，唯一的方法是创建一个新的 <code>map</code> 并将旧 <code>map</code> 中的元素复制到新的 <code>map</code> 中，然后丢弃旧的 <code>map</code>。这样，旧 <code>map</code> 占用的内存就可以被垃圾回收器回收。</p>
</li>
<li>
<p><strong>内存管理</strong>：因此，在使用 Go 语言的 <code>map</code> 时，应当注意内存管理。如果 <code>map</code> 用于存储大量元素并且会经常删除元素，那么可能需要定期创建新的 <code>map</code> 并复制元素，以避免内存使用无限增长。</p>
</li>
</ol>
<p>​   综上所述，Go 语言的 <code>map</code> 缩容机制并不像其他一些语言中那样自动或显式。这是一个需要开发者自己管理的方面，尤其是在涉及大型数据结构和内存管理时。</p>
<h2>线程安全问题</h2>
<p>​   关于 Go 语言中 <code>map</code> 的线程安全问题，这是一个非常重要的话题。基本来说，Go 的 <code>map</code> 在原生状态下是<strong>不保证线程安全</strong>的。这意味着在没有适当同步机制的情况下，从多个协程（goroutine）并发访问同一个 <code>map</code> 可能会导致竞态条件（race condition）和其他并发错误。</p>
<h3>实验</h3>
<pre><code class="language-golang">package main

import &quot;testing&quot;

func TestMap(t *testing.T) {
    var m = make(map[int]int)
    go func() {
        i := 0
        for {
            m[i]++
            i++
        }
    }()

    i := 0
    for {
        _ = m[i]
        i++
    }
}
</code></pre>
<p>上面代码会报错</p>
<pre><code>fatal error: concurrent map read and map write</code></pre>
<h3>原因</h3>
<ol>
<li>
<p><strong>内部状态修改</strong>：当一个协程对 <code>map</code> 进行写入（添加、修改、删除元素）操作时，它可能会更改 <code>map</code> 的内部状态。例如，在插入新元素时可能会触发扩容操作。如果此时另一个协程尝试访问 <code>map</code>，可能会遇到 <code>map</code> 结构正在变化的情况，导致不可预测的行为。</p>
</li>
<li>
<p><strong>并发写入冲突</strong>：如果多个协程同时尝试写入 <code>map</code>，它们可能会尝试修改 <code>map</code> 的同一部分，这可以导致数据损坏或程序崩溃。</p>
</li>
<li>
<p><strong>读写冲突</strong>：即使是读操作，也可能与写操作冲突。例如，一个协程正在读取 <code>map</code> 的一个元素，而另一个协程同时删除该元素或修改该元素所在的桶的结构，这可能导致读取操作发生错误。</p>
</li>
</ol>
<h3>解决方案</h3>
<h3>sync.RWMutex</h3>
<p>​   如果 <code>map</code> 的读操作远多于写操作，可以使用读写互斥锁 <code>sync.RWMutex</code>。这允许多个协程同时读取 <code>map</code>，但写操作仍然需要独占访问。</p>
<pre><code class="language-go">var rwmu sync.RWMutex
rwmu.RLock()
// 对 map 进行读操作
rwmu.RUnlock()

rwmu.Lock()
// 对 map 进行写操作
rwmu.Unlock()</code></pre>
<h3>sync.Map</h3>
<p>​   Go 标准库中的 <code>sync.Map</code> 是一个专门为并发场景设计的 map 类型。它优化了在特定使用情况下的性能，特别是在写入操作较少而读取操作频繁的场景中。<code>sync.Map</code> 提供了 <code>Load</code>、<code>Store</code>、<code>Delete</code> 等基本操作方法，这些方法都是线程安全的。</p>
<h4>适用场景</h4>
<p>根据官方文档，<code>sync.Map</code> 在以下两种场景下表现出优于普通 map 加读写互斥锁 (<code>map+RWMutex</code>) 的性能：</p>
<ol>
<li><strong>缓存系统</strong>：在一个只会增长的缓存系统中，一个键值对被写入后将被频繁读取或更新，但写入次数相对较少。</li>
<li><strong>分离的键集</strong>：多个 goroutine 分别读写不相交的键集，进行键值对的读、写和重写操作。</li>
</ol>
<pre><code class="language-go">var m sync.Map
m.Store(key, value) // 写入
value, ok := m.Load(key) // 读取
m.Delete(key) // 删除</code></pre>
<h4>实现原理</h4>
<p><code>sync.Map</code> 的高性能线程安全实现依赖于以下几个关键设计：</p>
<ol>
<li>
<p><strong>两个内部存储结构</strong>：<code>sync.Map</code> 维护一个主存储和一个只读存储。主存储用于最新的写入操作，而只读存储包含了先前版本的数据，形成一种数据快照。这使得在大多数情况下，读操作可以在无锁的情况下进行，因为它们主要针对稳定且不变的只读存储。</p>
</li>
<li>
<p><strong>延迟删除和原子操作</strong>：当从 <code>sync.Map</code> 中删除一个键值对时，它并不会立即从内存中移除，而是被标记为删除。这降低了写操作的频率。所有写入和删除操作都是通过原子操作完成的，保证了线程安全。</p>
</li>
<li>
<p><strong>读写分离</strong>：<code>sync.Map</code> 的设计将读写操作分离。由于读操作主要针对不经常变化的只读存储，因此可以在无锁的情况下安全进行，极大地提高了并发读的性能。</p>
</li>
<li>
<p><strong>动态调整</strong>：<code>sync.Map</code> 能够根据使用情况动态调整内部结构，例如，将数据从主存储迁移到只读存储，以优化性能。</p>
</li>
</ol>
<h4>性能优化</h4>
<p>​   在写入少，读取多的场景中，如缓存系统，或者当多个 goroutine 同时操作不同的键时，<code>sync.Map</code> 的这种设计可以显著提高性能。在这些场景下，由于读取操作的高效率和写入操作的原子性，<code>sync.Map</code> 能够提供比传统的带锁 map 更好的性能。</p>
<p>​   总的来说，<code>sync.Map</code> 是一个针对特定并发场景优化的数据结构，它通过内部的复杂机制，实现了在高并发环境下的高效线程安全操作。然而，它并非适用于所有情况，特别是在写入操作频繁的场景中，传统的 map 加锁可能会更加高效。因此，选择 <code>sync.Map</code> 还是普通 map 应该基于具体的应用场景和性能需求。</p>
<h3>其他并发安全map的实现库</h3>
<h3>1. orcaman/concurrent-map</h3>
<h4>GitHub: <a href="https://github.com/orcaman/concurrent-map" target="_blank" rel="noopener">orcaman/concurrent-map</a></h4>
<ul>
<li>特点：
<ul>
<li><code>orcaman/concurrent-map</code> 是一个简单易用的并发安全的哈希表实现。</li>
<li>它通过将 map 分割成多个小的 map 段（shards）来减少锁的粒度，从而提高并发性能。每个段有自己的读写锁，这减少了锁争用，提高了并发读写的效率。</li>
<li>提供基本的 map 操作如 <code>Set</code>, <code>Get</code>, <code>Remove</code>, <code>Items</code> 等。</li>
</ul>
</li>
</ul>
<h3>2. cornelk/hashmap</h3>
<h4>GitHub: <a href="https://github.com/cornelk/hashmap" target="_blank" rel="noopener">cornelk/hashmap</a></h4>
<ul>
<li>特点：
<ul>
<li><code>cornelk/hashmap</code> 是另一个高性能的并发安全的哈希表实现。</li>
<li>它特别为高并发场景优化，比如在多核处理器上运行时，能够提供非常好的性能。</li>
<li>支持扩展和收缩，可以根据存储的元素数量自动调整大小。</li>
<li>提供了丰富的 API，包括迭代器、大小估计、自定义等值比较等功能。</li>
</ul>
</li>
</ul>
<h3>3. alphadose/haxmap</h3>
<h4>GitHub: <a href="https://github.com/alphadose/haxmap" target="_blank" rel="noopener">alphadose/haxmap</a></h4>
<ul>
<li>特点：
<ul>
<li><code>alphadose/haxmap</code> 是一个较新的高性能并发安全的哈希表实现。</li>
<li>这个库的特色在于其使用了一种叫做 &quot;hopscotch hashing&quot; 的技术，这种技术旨在提高缓存利用率和降低冲突概率。</li>
<li>旨在提供高速的并发读写操作，同时保持较低的内存占用。</li>
<li>API 上提供了类似的基本操作，如添加、删除、查找等。</li>
</ul>
</li>
</ul>
<h3>总结</h3>
<p>​   这些库各有其特点和优势，选择合适的库取决于具体的应用场景和性能需求。例如，在需要处理大量并发读写操作的情况下，可能会选择 <code>cornelk/hashmap</code>；而如果希望降低内存占用并提高缓存效率，则 <code>alphadose/haxmap</code> 可能是一个好的选择。重要的是要根据实际需求和性能测试来选择最适合项目的并发安全 map 实现。</p>
<h3>结论</h3>
<p>​   在多协程环境下，对 <code>map</code> 的安全访问需要采用合适的同步机制。选择哪种机制取决于具体的使用场景和性能要求。简单地使用互斥锁可以提供安全保证，但可能会影响性能。在读多写少的场景中，<code>sync.RWMutex</code> 或 <code>sync.Map</code> 可能是更好的选择。顺便提一下，slice也不是线程安全的。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.cztcode.com/2023/5075/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5075</post-id>	</item>
		<item>
		<title>godis中tcp模块详解</title>
		<link>https://www.cztcode.com/2022/4272/</link>
					<comments>https://www.cztcode.com/2022/4272/#respond</comments>
		
		<dc:creator><![CDATA[Jellow]]></dc:creator>
		<pubDate>Mon, 26 Dec 2022 02:32:16 +0000</pubDate>
				<category><![CDATA[Golang]]></category>
		<guid isPermaLink="false">https://www.cztcode.com/?p=4272</guid>

					<description><![CDATA[golang搭建一个tcp服务器还是很简便的。作者在这里考虑到了tcp服务器关闭后，继续处理现有任务并不接受新任务的场景和服务端超时自动关闭连接的场景。遗憾的是有些功能还未实现，有机会可以提pr完善。
学到了很多！]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div>
<p>最近在看godis源码<a href="https://github.com/hdt3213/godis/blob/master/README_CN.md" target="_blank" rel="noopener">https://github.com/hdt3213/godis/blob/master/README_CN.md</a></p>



<p>如果你想自己试一试的话，可以去源码里复制tcp模块运行测试。</p>



<p>下面讲解一下godis中tcp模块的实现</p>



<h2 class="wp-block-heading">tcp模块组成</h2>



<p>tcp模块有下面三个文件，实际的tcp服务器只有server.go一个文件</p>



<ol class="wp-block-list">
<li>echo.go //负责实现一个返回输入内容的handler</li>



<li>echo_test.go //负责测试server.go是否正常</li>



<li>server.go //实现tcp服务器</li>
</ol>



<h2 class="wp-block-heading">server.go</h2>



<p>server.go有两个函数</p>



<ol class="wp-block-list">
<li>ListenAndServeWithSignal //在tcp服务器核心上提供了配置和退出服务器功能</li>



<li>ListenAndServe //tcp服务器核心</li>
</ol>



<p>我们首先看ListenAndServe函数，echo.go和echo_test.go都是为了这个函数准备的</p>



<h3 class="wp-block-heading">参数</h3>



<ol class="wp-block-list">
<li>listener net.Listener //接收一个监听器（仅仅是测试用，因为net.Listen是本地监听器）</li>



<li>handler tcp.Handler //tcp接收请求后，由handler函数处理</li>



<li>closeChan &lt;-chan struct{} //仅接受单向通道closeChan，用于关闭tcp服务器</li>
</ol>



<pre class="wp-block-code"><code>&nbsp;​<br>&nbsp;// ListenAndServe binds port and handle requests, blocking until close<br>&nbsp;func ListenAndServe(listener net.Listener, handler tcp.Handler, closeChan &lt;-chan struct{}) {<br>&nbsp;  // listen signal<br>&nbsp; &nbsp;//创建一个协程，当监测到关闭信号后关闭服务器。<br>&nbsp;  go func() {<br>&nbsp;    &lt;-closeChan<br>&nbsp;    logger.Info("shutting down...")<br>&nbsp;    _ = listener.Close() // listener.Accept() will return err immediately<br>&nbsp;    _ = handler.Close() &nbsp;// close connections<br>&nbsp;  }()<br>&nbsp;  <br>&nbsp;  // listen port<br>&nbsp; &nbsp;//手动终止函数运行时会调用defer，关闭连接，但是在测试里就用不上了（除非你手动终止了服务器）<br>&nbsp;  defer func() {<br>&nbsp;    // close during unexpected error<br>&nbsp;    _ = listener.Close()<br>&nbsp;    _ = handler.Close()<br>&nbsp;  }()<br>&nbsp;​<br>&nbsp;  ctx := context.Background() //下文有详细介绍<br>&nbsp;  var waitDone sync.WaitGroup //下文有详细介绍<br>&nbsp;  for {<br>&nbsp;    conn, err := listener.Accept() //一直等待直到有新连接<br>&nbsp;    if err != nil {<br>&nbsp;      break<br>&nbsp;    }<br>&nbsp;    // handle<br>&nbsp;    logger.Info("accept link")<br>&nbsp;    waitDone.Add(1)<br>&nbsp;    go func() {<br>&nbsp;      defer func() {<br>&nbsp;        waitDone.Done() //协程关闭，计数-1<br>&nbsp;      }()<br>&nbsp;      handler.Handle(ctx, conn) &nbsp;//交给handle处理<br>&nbsp;    }()<br>&nbsp;  }<br>&nbsp;  waitDone.Wait() //等待所有协程退出<br>&nbsp;}<br>&nbsp;​</code></pre>



<h3 class="wp-block-heading">context.Background()</h3>



<p>context 主要用来在 goroutine 之间传递上下文信息，包括：取消信号、超时时间、截止时间、k-v 等。</p>



<p>随着 context 包的引入，标准库中很多接口因此加上了 context 参数，例如 database/sql 包。context 几乎成为了并发控制和超时控制的标准做法。</p>



<p>参考：<a href="https://zhuanlan.zhihu.com/p/68792989" target="_blank" rel="noopener">https://zhuanlan.zhihu.com/p/68792989</a></p>



<p><strong>虽然这里传入了ctx，handler的所有实现方法中均没有调用ctx。应该是handler方法中没有子协程，无需统一并发控制</strong></p>



<h3 class="wp-block-heading">sync.WaitGroup</h3>



<p>在线程需要等待多个协程完成时，可以使用管道channel来等待所有协程的完成信号。但是管道在这里显得有些大材小用，因为它被设计出来不仅仅只是在这里用作简单的同步处理，在这里使用管道实际上是不合适的。而且假设我们有一万、十万甚至更多的for循环，也要申请同样数量大小的管道出来，对内存也是不小的开销。</p>



<p>WaitGroup 对象内部有一个计数器，最初从0开始，它有三个方法：Add()，Done()，Wait() 用来控制计数器的数量。Add(n) 把计数器设置为n ，Done() 每次把计数器减1 ，wait() 会阻塞代码的运行，直到计数器地值减为0。</p>



<p>这里使用WaitGroup记录tcp服务器的连接，当服务器关闭时会执行waitDone.Wait()，等所有连接完成后才会关闭服务器。</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>在生产环境下需要保证TCP服务器关闭前完成必要的清理工作，包括将完成正在进行的数据传输，关闭TCP连接等。这种关闭模式称为优雅关闭，可以避免资源泄露以及客户端未收到完整数据导致故障。</p>
</blockquote>



<h3 class="wp-block-heading">ListenAndServeWithSignal</h3>



<p>这个函数是在tcp服务器基础上提供了配置和退出功能，但是最大连接数和超时时间作者实际并没有进行处理（不知道为啥）。</p>



<p>首先看看几个系统信号表示的含义</p>



<ul class="wp-block-list">
<li>Ctrl-C 发送 INT signal (SIGINT)，通常导致进程结束</li>



<li>SIGHUP，终端控制进程结束(终端连接断开)</li>



<li>SIGQUIT，用户发送QUIT字符(Ctrl+/)触发</li>



<li>SIGTERM，结束程序(可以被捕获、阻塞或忽略)</li>
</ul>



<p>那这个函数就很好理解了，核心就是调用ListenAndServe</p>



<pre class="wp-block-code"><code>&nbsp;// Config stores tcp server properties<br>&nbsp;type Config struct {<br>&nbsp;  Address &nbsp; &nbsp;string &nbsp; &nbsp; &nbsp; &nbsp;`yaml:"address"` //监听的地址，一般配置0.0.0.0全部侦听<br>&nbsp;  MaxConnect uint32 &nbsp; &nbsp; &nbsp; &nbsp;`yaml:"max-connect"` //最大连接数<br>&nbsp;  Timeout &nbsp; &nbsp;time.Duration `yaml:"timeout"` //超时时间<br>&nbsp;}<br>&nbsp;​<br>&nbsp;// ListenAndServeWithSignal binds port and handle requests, blocking until receive stop signal<br>&nbsp;func ListenAndServeWithSignal(cfg *Config, handler tcp.Handler) error {<br>&nbsp;  closeChan := make(chan struct{})<br>&nbsp;  sigCh := make(chan os.Signal)<br>&nbsp;  signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT)<br>&nbsp;  go func() {<br>&nbsp;    sig := &lt;-sigCh<br>&nbsp;    switch sig {<br>&nbsp;    case syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:<br>&nbsp;      closeChan &lt;- struct{}{}<br>&nbsp;    }<br>&nbsp;  }()<br>&nbsp;  listener, err := net.Listen("tcp", cfg.Address)<br>&nbsp;  if err != nil {<br>&nbsp;    return err<br>&nbsp;  }<br>&nbsp;  //cfg.Address = listener.Addr().String()<br>&nbsp;  logger.Info(fmt.Sprintf("bind: %s, start listening...", cfg.Address))<br>&nbsp;  ListenAndServe(listener, handler, closeChan)<br>&nbsp;  return nil<br>&nbsp;}</code></pre>



<h2 class="wp-block-heading">echo.go</h2>



<p>echo.go是用来测试server.go的，这里实现了一个handler返回客户端输入的信息。</p>



<pre class="wp-block-code"><code>&nbsp;​<br>&nbsp;// EchoHandler echos received line to client, using for test<br>&nbsp;type EchoHandler struct {<br>&nbsp;  activeConn sync.Map //线程安全<br>&nbsp;  closing &nbsp; &nbsp;atomic.Boolean //多线程bool原子性<br>&nbsp;}<br>&nbsp;​<br>&nbsp;// MakeEchoHandler creates EchoHandler <br>&nbsp;// 使用函数进行取地址初始化，可以当作构造函数<br>&nbsp;func MakeEchoHandler() *EchoHandler {<br>&nbsp;  return &amp;EchoHandler{}<br>&nbsp;}<br>&nbsp;​<br>&nbsp;// EchoClient is client for EchoHandler, using for test<br>&nbsp;type EchoClient struct {<br>&nbsp;  Conn &nbsp; &nbsp;net.Conn<br>&nbsp;  Waiting wait.Wait &nbsp;//这里作者封装了一下sync.WaitGroup，其实就是sync.WaitGroup<br>&nbsp;} <br>&nbsp;​<br>&nbsp;// Close close connection<br>&nbsp;func (c *EchoClient) Close() error {<br>&nbsp;  c.Waiting.WaitWithTimeout(10 * time.Second) //下文有详解<br>&nbsp;  c.Conn.Close()<br>&nbsp;  return nil<br>&nbsp;}<br>&nbsp;​<br>&nbsp;// Handle echos received line to client<br>&nbsp;func (h *EchoHandler) Handle(ctx context.Context, conn net.Conn) {<br>&nbsp; &nbsp;//服务端已经关闭，等待客户端剩余连接完成时就不会再接入新的请求了<br>&nbsp;  if h.closing.Get() {<br>&nbsp;    // closing handler refuse new connection<br>&nbsp;    _ = conn.Close()<br>&nbsp;    return<br>&nbsp;  }<br>&nbsp;  //创建一个客户端，这里的客户端是相对于server来说的<br>&nbsp;  client := &amp;EchoClient{<br>&nbsp;    Conn: conn,<br>&nbsp;  }<br>&nbsp; &nbsp;//记录当前客户端<br>&nbsp;  h.activeConn.Store(client, struct{}{})<br>&nbsp;​<br>&nbsp;  reader := bufio.NewReader(conn)<br>&nbsp;  for {<br>&nbsp; &nbsp; &nbsp;//下面就是循环读输入内容，然后原封不动返回<br>&nbsp;    // may occurs: client EOF, client timeout, server early close<br>&nbsp;    msg, err := reader.ReadString('\n')<br>&nbsp;    if err != nil {<br>&nbsp;      if err == io.EOF {<br>&nbsp;        logger.Info("connection close")<br>&nbsp;        h.activeConn.Delete(client)<br>&nbsp;      } else {<br>&nbsp;        logger.Warn(err)<br>&nbsp;      }<br>&nbsp;      return<br>&nbsp;    }<br>&nbsp; &nbsp; &nbsp;//这里waiting的意思是，客户端提交了一个任务，等待服务端返回<br>&nbsp;    client.Waiting.Add(1)//任务列表+1<br>&nbsp;    //logger.Info("sleeping")<br>&nbsp;    //time.Sleep(10 * time.Second)<br>&nbsp; &nbsp; &nbsp;//这里只是模拟了一个简单的服务端任务<br>&nbsp;    b := &#91;]byte(msg)<br>&nbsp;    _, _ = conn.Write(b)<br>&nbsp; &nbsp; &nbsp;//服务端做好了处理<br>&nbsp;    client.Waiting.Done()//任务列表-1<br>&nbsp;  }<br>&nbsp;}<br>&nbsp;​<br>&nbsp;// Close stops echo handler<br>&nbsp;func (h *EchoHandler) Close() error {<br>&nbsp;  logger.Info("handler shutting down...")<br>&nbsp;  h.closing.Set(true) //不再接入新的连接<br>&nbsp; &nbsp;//逐个关闭还在排序的连接<br>&nbsp;  h.activeConn.Range(func(key interface{}, val interface{}) bool {<br>&nbsp;    client := key.(*EchoClient)<br>&nbsp;    _ = client.Close()<br>&nbsp;    return true<br>&nbsp;  })<br>&nbsp;  return nil<br>&nbsp;}<br>&nbsp;​</code></pre>



<h3 class="wp-block-heading">数据结构定义</h3>



<pre class="wp-block-preformatted">&nbsp;type EchoHandler struct {<br>&nbsp; &nbsp; activeConn sync.Map //线程安全<br>&nbsp; &nbsp; closing &nbsp; &nbsp;atomic.Boolean //线程安全，多线程bool原子性<br>&nbsp;}</pre>



<h4 class="wp-block-heading">sync.Map</h4>



<p>sync.map是一个线程安全map，用于在多线程（协程）时使用。</p>



<p>需要并发读写时，一般的做法是加锁，但这样性能并不高，Go语言在 1.9 版本中提供了一种效率较高的并发安全的 sync.Map，sync.Map 和 map 不同，不是以语言原生形态提供，而是在 sync 包下的特殊结构。</p>



<p>sync.Map 有以下特性：</p>



<ol class="wp-block-list">
<li>无须初始化，直接声明即可。</li>



<li>sync.Map 不能使用 map 的方式进行取值和设置等操作，而是使用 sync.Map 的方法进行调用，Store 表示存储，Load 表示获取，Delete 表示删除。</li>



<li>使用 Range 配合一个回调函数进行遍历操作，通过回调函数返回内部遍历出来的值，Range 参数中回调函数的返回值在需要继续迭代遍历时，返回 true，终止迭代遍历时，返回 false。</li>
</ol>



<h4 class="wp-block-heading">atomic.Boolean</h4>



<p>也是保证线程安全，具体介绍可以看这篇文章</p>



<p><a href="https://gfw.go101.org/article/concurrent-atomic-operation.html" target="_blank" rel="noopener">https://gfw.go101.org/article/concurrent-atomic-operation.html</a></p>



<h3 class="wp-block-heading">c.Waiting.WaitWithTimeout(10 * time.Second)</h3>



<p>作者自己写了一个wait包，包装了sync.WaitGroup。</p>



<p>除此之外多了一个等待超时的函数WaitWithTimeout用来给客户端在超时关闭连接，一会讲完测试后我会演示一下当客户端还有任务没有完成时超时的情况。</p>



<pre class="wp-block-code"><code>&nbsp;// WaitWithTimeout blocks until the WaitGroup counter is zero or timeout<br>&nbsp;// returns true if timeout<br>&nbsp;func (w *Wait) WaitWithTimeout(timeout time.Duration) bool {<br>&nbsp;  c := make(chan bool, 1)<br>&nbsp; &nbsp;//客户端<br>&nbsp;  go func() { //创建一个协程，等待连接。<br>&nbsp;    defer close(c)<br>&nbsp;    w.wg.Wait() &nbsp;//客户端已关闭<br>&nbsp;    c &lt;- true<br>&nbsp;  }()<br>&nbsp;  select {<br>&nbsp;  case &lt;-c:<br>&nbsp;    return false // completed normally<br>&nbsp;  case &lt;-time.After(timeout):<br>&nbsp;    return true // timed out<br>&nbsp;  }<br>&nbsp;}</code></pre>



<h2 class="wp-block-heading">echo_test.go</h2>



<p>Go 语言推荐测试文件和源代码文件放在一块，测试文件以 <code>_test.go</code> 结尾。比如，当前 package 有 <code>calc.go</code> 一个文件，我们想测试 <code>calc.go</code> 中的 <code>Add</code> 和 <code>Mul</code> 函数，那么应该新建 <code>calc_test.go</code> 作为测试文件。</p>



<pre class="wp-block-preformatted">&nbsp;example/<br>&nbsp; &nbsp; |--calc.go<br>&nbsp; &nbsp; |--calc_test.go</pre>



<p>参考：<a href="https://geektutu.com/post/quick-go-test.html" target="_blank" rel="noopener">https://geektutu.com/post/quick-go-test.html</a></p>



<p>必须符合命名规范goland才会识别为测试文件，并且测试函数要以Test开头+想要测试的函数。当符合这些规范后goland会多出一个快捷运行的箭头。</p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/image-20221226094838140.png" alt="image-20221226094838140"/></figure>



<p>下面是 echo_test.go文件。首先创建了一个服务端，自动运行在一个端口上并接收本地所有ip，然后使用dial连接这个地址。测试了10次输入输出是否正确，然后关闭连接，测试了5次在服务端关闭的情况下的请求。</p>



<p>需要注意的是最后一行time.Sleep(time.Second)是为了服务端和客户端能够正常输出，因为TestListenAndServe执行结束后就看不见控制台输出了。</p>



<pre class="wp-block-code"><code>&nbsp;​<br>&nbsp;func TestListenAndServe(t *testing.T) {<br>&nbsp;  var err error<br>&nbsp;  closeChan := make(chan struct{})<br>&nbsp;  listener, err := net.Listen("tcp", ":0")<br>&nbsp;  if err != nil {<br>&nbsp;    t.Error(err)<br>&nbsp;    return<br>&nbsp;  }<br>&nbsp;  addr := listener.Addr().String()<br>&nbsp;  go ListenAndServe(listener, MakeEchoHandler(), closeChan)<br>&nbsp;​<br>&nbsp;  conn, err := net.Dial("tcp", addr)<br>&nbsp;  if err != nil {<br>&nbsp;    t.Error(err)<br>&nbsp;    return<br>&nbsp;  }<br>&nbsp;  for i := 0; i &lt; 10; i++ {<br>&nbsp;    val := strconv.Itoa(rand.Int())<br>&nbsp;    _, err = conn.Write(&#91;]byte(val + "\n"))<br>&nbsp;    if err != nil {<br>&nbsp;      t.Error(err)<br>&nbsp;      return<br>&nbsp;    }<br>&nbsp;    bufReader := bufio.NewReader(conn)<br>&nbsp;    line, _, err := bufReader.ReadLine()<br>&nbsp;    if err != nil {<br>&nbsp;      t.Error(err)<br>&nbsp;      return<br>&nbsp;    }<br>&nbsp;    if string(line) != val {<br>&nbsp;      t.Error("get wrong response")<br>&nbsp;      return<br>&nbsp;    }<br>&nbsp;  }<br>&nbsp;  _ = conn.Close()<br>&nbsp;  for i := 0; i &lt; 5; i++ {<br>&nbsp;    // create idle connection<br>&nbsp;    _, _ = net.Dial("tcp", addr)<br>&nbsp;  }<br>&nbsp;  closeChan &lt;- struct{}{}<br>&nbsp;  time.Sleep(time.Second)<br>&nbsp;}<br>&nbsp;​</code></pre>



<h2 class="wp-block-heading">测试服务端处理任务超时</h2>



<p>当客户端关闭连接时，如果任务已经完成则立即关闭连接。若还有任务等待服务端相应就需要等待一个超时时间，超时后也会关闭。</p>



<p>在handler服务端处理完任务后，不记录任务已经完成</p>



<pre class="wp-block-code"><code>&nbsp;//echo.go<br>&nbsp;client.Waiting.Add(1)<br>&nbsp;b := &#91;]byte(msg)<br>&nbsp;_, _ = conn.Write(b)<br>&nbsp;//client.Waiting.Done()z</code></pre>



<p>上面说过，测试函数如果关闭过早就看不见server的输出了，所以这里改成20s（比超时10s大就好）</p>



<pre class="wp-block-code"><code>&nbsp;//echo_test.go<br>&nbsp;  //_ = conn.Close()  //客户端不主动关闭连接，模拟还需要等待响应<br>&nbsp;  //for i := 0; i &lt; 5; i++ {<br>&nbsp;  //  // create idle connection<br>&nbsp;  //  _, _ = net.Dial("tcp", addr)<br>&nbsp;  //}<br>&nbsp;  closeChan &lt;- struct{}{}<br>&nbsp;  time.Sleep(time.Second * 20) //更改成20s才能查看超时</code></pre>



<p>为了能看见是因为超时关闭还是任务完成关闭，这里打印WaitWithTimeout的输出。输出为true表示因为超时关闭。</p>



<pre class="wp-block-code"><code>&nbsp;func (c *EchoClient) Close() error {<br>&nbsp;  logger.Warn(c.Waiting.WaitWithTimeout(10 * time.Second))<br>&nbsp;  c.Conn.Close()<br>&nbsp;  return nil<br>&nbsp;}</code></pre>



<h3 class="wp-block-heading">查看结果</h3>



<p>可以看到输出了true，表示因为超时而关闭的连接。</p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/image-20221226100430144.png" alt="image-20221226100430144"/></figure>



<h2 class="wp-block-heading">总结</h2>



<p>golang搭建一个tcp服务器还是很简便的。作者在这里考虑到了tcp服务器关闭后，继续处理现有任务并不接受新任务的场景和服务端超时自动关闭连接的场景。遗憾的是有些功能还未实现，有机会可以提pr完善。</p>



<p><strong>学到了很多！</strong></p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.cztcode.com/2022/4272/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">4272</post-id>	</item>
		<item>
		<title>Golang中的struct {}</title>
		<link>https://www.cztcode.com/2022/4269/</link>
					<comments>https://www.cztcode.com/2022/4269/#respond</comments>
		
		<dc:creator><![CDATA[Jellow]]></dc:creator>
		<pubDate>Sat, 24 Dec 2022 11:29:46 +0000</pubDate>
				<category><![CDATA[Golang]]></category>
		<guid isPermaLink="false">https://www.cztcode.com/?p=4269</guid>

					<description><![CDATA[struct {}是一种普通数据类型，一个无元素的结构体类型，通常在没有信息存储时使用。 优点是大小为0，不需要内存来存储struct {}类型的值。]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div>
<h2 class="wp-block-heading">什么是struct{}和struct{}{}</h2>



<p>struct {}是一种普通<strong>数据类型</strong>，一个无元素的结构体类型，通常在没有信息存储时使用。 优点是大小为0，不需要内存来存储struct {}类型的值。</p>



<p>struct {} {}：实例化一个struct{}类型的数据，大小为0。</p>



<h2 class="wp-block-heading">验证struct{}大小为0</h2>



<pre class="wp-block-code"><code>&nbsp;var s struct{}<br>&nbsp;fmt.Println(unsafe.Sizeof(s)) //输出0</code></pre>



<p>struct{}组成的struct大小也为0</p>



<pre class="wp-block-code"><code>&nbsp;type S struct {<br>&nbsp;    A struct{}<br>&nbsp;    B struct{}<br>&nbsp;  }<br>&nbsp;var s S<br>&nbsp;fmt.Println(unsafe.Sizeof(s)) //输出0</code></pre>



<h3 class="wp-block-heading">unsafe.Sizeof()介绍</h3>



<p>unsafe的软件包可能不可移植，并且不受Go 1兼容性指南的保护。虽然有几个函数在不安全的unsafe包，但是这几个函数调用并不是真的不安全，特别在需要优化内存空间时它们返回的结果对于理解原生的内存布局很有帮助。</p>



<p>unsafe.Sizeof()总是在编译期就进行求值，而不是在运行时，这意味着返回值可以赋值给常量。我们这里可以使用unsafe.Sizeof()获取struct大小。</p>



<h2 class="wp-block-heading">channel中使用struct{}</h2>



<p>一般通过 channel := make(chan struct{})创建一个空channel，使用struct{}的在事件通信时可以节省内存。</p>



<h3 class="wp-block-heading">使用struct{}作为无缓冲channel标志</h3>



<p>这里创建了一个channel，并创建了一个协程。协程中等待10秒才会写入消息，这个消息的类型是struct{}，内容为{}，大小为0。</p>



<pre class="wp-block-code"><code>&nbsp;  type connection struct {<br>&nbsp;    stop chan struct{}<br>&nbsp;  }<br>&nbsp;  var con connection<br>&nbsp;  con.stop = make(chan struct{})<br>&nbsp;  go func() {<br>&nbsp;    fmt.Println("等待10秒")<br>&nbsp;    time.Sleep(time.Second * 10)<br>&nbsp;    con.stop &lt;- struct{}{} //表示传递结束信息<br>&nbsp;  }()<br>&nbsp;  &lt;-con.stop<br>&nbsp;  fmt.Println("此消息的大小为：", unsafe.Sizeof(&lt;-con.stop)) //输出0<br>&nbsp;  fmt.Println("协程结束了")</code></pre>



<h3 class="wp-block-heading">使用struct{}作为有缓冲channel标志</h3>



<p>有缓冲时可以定义任意个标志（操作系统联系死锁可以用这个玩）。</p>



<pre class="wp-block-code"><code>&nbsp;type connection struct {<br>&nbsp;    stop chan struct{}<br>&nbsp;  }<br>&nbsp;  var con connection<br>&nbsp;  con.stop = make(chan struct{}, 2) //缓冲容量为2<br>&nbsp;  go func() {<br>&nbsp;    fmt.Println("第一个协程等待4秒")<br>&nbsp;    time.Sleep(time.Second * 4)<br>&nbsp;    con.stop &lt;- struct{}{} //表示传递结束信息<br>&nbsp;  }()<br>&nbsp;  go func() {<br>&nbsp;    fmt.Println("第二个协程等待3秒")<br>&nbsp;    time.Sleep(time.Second * 3)<br>&nbsp;    con.stop &lt;- struct{}{} //表示传递结束信息<br>&nbsp;  }()<br>&nbsp;  &lt;-con.stop<br>&nbsp;  fmt.Println("第一个协程结束了")<br>&nbsp;  &lt;-con.stop<br>&nbsp;  fmt.Println("第二个协程结束了")<br>&nbsp;  fmt.Println("协程结束了")</code></pre>
]]></content:encoded>
					
					<wfw:commentRss>https://www.cztcode.com/2022/4269/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">4269</post-id>	</item>
		<item>
		<title>《8小时转职Golang工程师》超时强踢功能的BUG修复</title>
		<link>https://www.cztcode.com/2022/4254/</link>
					<comments>https://www.cztcode.com/2022/4254/#comments</comments>
		
		<dc:creator><![CDATA[Jellow]]></dc:creator>
		<pubDate>Wed, 10 Aug 2022 01:40:51 +0000</pubDate>
				<category><![CDATA[Golang]]></category>
		<guid isPermaLink="false">https://www.cztcode.com/?p=4254</guid>

					<description><![CDATA[最近在看刘丹冰的《8小时转职Golang工程师》。刘老师讲的思路很好，但有些细节没有处理好，很多人在跟着写代码后可能出现一些新手看来不太容易发现和解决的Bug。]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div>
<p>最近在看刘丹冰的《<strong><a href="https://www.bilibili.com/video/BV1gf4y1r79E?spm_id_from=333.999.0.0&amp;vd_source=bd2e225c1a23848b80eb4771969e173f" class="rank-math-link" target="_blank" rel="noopener">8小时转职Golang工程师</a></strong>》</p>



<p>刘老师讲的思路很好，但有些细节没有处理好，很多人在跟着写代码后可能出现一些新手看来不太容易发现和解决的Bug。</p>



<p>还给大神发了邮件问：</p>



<h2 class="wp-block-heading">邮件</h2>



<p>刘丹冰Aceld大神</p>



<p>您好</p>



<p>我最近在看您的《8小时转职Golang工程师》，在学到第44集-超时强踢功能时发现了一个可能的潜在BUG。</p>



<p>当server.go中的Handler函数调用conn.Close()后，由于user.go中ListenMessage()函数仍在监听user的channel，最后的“您被踢了”消息可能在conn.Close() ”后发送，这会导致向已关闭的连接中写入消息报错。而您的代码中没有打印conn.Write([]byte(msg + &#8220;\n”))返回的报错信息，所以没有监测到这个问题。</p>



<p>我遇到了这个错误后很久才定位原因，在视频的评论区里也有很多人提出当接收到“您被踢了”消息后CPU占用率飙升。这可能是因为他们使用的也是Goland IDE，而Goland会提示在代码中添加捕获conn.Write([]byte(msg + &#8220;\n”))返回的报错信息，当直接打印时由于外层有for循环，会无限打印报错，导致cpu占用率飙升。</p>



<p>您可以在user.go中更改ListenMessage()方法复现这个问题：</p>



<pre class="wp-block-code"><code>func (this *User) ListenMessage() {
	for {
		msg := &lt;-this.C

​		_, err := this.conn.Write(&#91;]byte(msg + "\n"))
​		if err != nil {
​			fmt.Println(err)
​		}
​	}
}</code></pre>



<p>ListenMessage()这个协程应该如何正确关闭呢，希望得到您的指教！</p>



<h2 class="wp-block-heading">分析问题</h2>



<p>我遇到这个Bug后半天也没找到问题在哪，因为这可太巧了。</p>



<p>首先我打印bug是用println，当时写的时候用的tobnine自动补全这行代码，所以日志信息看起来就像系统报错（写的很正式，因为我是新手，半天没发现这行报错提示是我自己定义的）。其次这个报错是在for循环中，所以控制台会无限打印报错信息，然后导致cpu使用率飙升。最后我打印bug的时候用的是err而不是err.Error()，前者只能看见地址信息（刚开始不知道还有这区别）。</p>



<p>Golang把所有<strong>Excepiton</strong>都用<strong>Error</strong>来处理，我可太不习惯了。第一次都没想到这是一个报错，堆栈信息啥啥没有，我都不知道哪个文件第几行代码出错了。后来能换Panic换Panic…..</p>



<p>好了，先给出解决方法。我假设你已经看过刘丹冰《8小时转职Golang工程师》第44集，并且遇到了和我一样的问题。</p>



<p>这个问题出现的原因邮件里已经说清楚了，就是server.go中超时强踢时已经关闭了conn，但是user.go可能还会监听消息并向用户写入消息。</p>



<h2 class="wp-block-heading">解决方案1</h2>



<p>在打印报错后加一个return退出ListenMessage()协程，这样写虽然不会再出现无限打印，但属于出现错误后再处理。我认为最好的方式还是通过设计避免错误出现。如果出现错误后return可能会造成日志打印很多无效报错，或者如果不打印报错的话会丢失可能的报错提示。</p>



<h2 class="wp-block-heading">解决方案2</h2>



<p>我认为最佳解决方案是让这两个协程进行通信，让ListenMessage()知道conn已经关闭了。</p>



<p>这里用到一个特性，for range channel</p>



<ul class="wp-block-list"><li>场景：当需要不断从channel读取数据时</li><li>原理：使用<code>for-range</code>读取channel，这样既安全又便利，当channel关闭时，for循环会自动退出，无需主动监测channel是否关闭，可以防止读取已经关闭的channel，造成读到数据为通道所存储的数据类型的零值。</li><li>用法：</li></ul>



<pre class="wp-block-code"><code>for x := range ch{<br> &nbsp; &nbsp;fmt.Println(x)<br>}</code></pre>



<p>我们可以这样更改，server.go中只关闭u.C，ListenMessage()检测到u.C关闭后不再写入信息。</p>



<p>user.go</p>



<pre class="wp-block-code"><code>func (u *User) ListenMessage() {<br>  //当u.C通道关闭后，不再进行监听并写入信息<br> &nbsp; for msg := range u.C {<br> &nbsp; &nbsp; &nbsp;_, err := u.conn.Write(&#91;]byte(msg + "\n"))<br> &nbsp; &nbsp; &nbsp;if err != nil {<br> &nbsp; &nbsp; &nbsp; &nbsp;panic(err)<br> &nbsp; &nbsp;  }<br> &nbsp; }<br> &nbsp;//不监听后关闭conn，conn在这里关闭最合适<br> &nbsp; err := u.conn.Close()<br> &nbsp; if err != nil {<br> &nbsp; &nbsp; &nbsp;panic(err)<br> &nbsp; }<br>​<br>}</code></pre>



<p>server.go</p>



<pre class="wp-block-code"><code>func (s *Server) Handler(conn net.Conn) {<br>​<br>  user := NewUser(conn, s)<br>  user.Online()<br>​<br>  isLive := make(chan bool)<br>  go func() {<br>    buf := make(&#91;]byte, 4096)<br>    for {<br>      n, err := conn.Read(buf)<br>      //下线的时候会发长度为0的消息<br>      if n == 0 {<br>        user.Offline()<br>        //这样的写法会造成服务端出现大量CLOSE_WAIT，return也只是退出当前协程，而不是Handle<br>        return<br>      }<br>      //读到末尾的时候err是io.EOF<br>      if err != nil &amp;&amp; err != io.EOF {<br>        fmt.Println("Error:", err.Error())<br>        return<br>      }<br>      //去除最后的'\n'并转为字符串<br>      msg := string(buf&#91;:n-1])<br>      user.DoMessage(msg)<br>      isLive &lt;- true<br>    }<br>​<br>  }()<br>  // 空select会一直阻塞<br>  for {<br>    //超时强踢<br>    select {<br>    case &lt;-isLive:<br>    //当前用户是活跃的,不做任何事情，select中会执行下面这句重置<br>    //time.After(time.Second * 10)重新执行即刻重置定时器，定时到后会发送信息<br>    //这里select 第二个case定时器触发后，处于阻塞状态。当满足第一个 case 的条件后，<br>    //打破了 select 的阻塞状态，每个条件又开始判断，第2个 case 的判断条件一执行，就重置定时器了。<br>    case &lt;-time.After(time.Second * 10):<br>      user.SendMessage("超过10秒未操作")<br> &nbsp; &nbsp; &nbsp;//只关闭uer.C<br>      close(user.C)<br>      return<br>    }<br>  }<br>​<br>}<br>​</code></pre>



<p>这样问题就解决了！！！</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.cztcode.com/2022/4254/feed/</wfw:commentRss>
			<slash:comments>3</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">4254</post-id>	</item>
		<item>
		<title>Go: Hello World</title>
		<link>https://www.cztcode.com/2022/4252/</link>
					<comments>https://www.cztcode.com/2022/4252/#respond</comments>
		
		<dc:creator><![CDATA[Jellow]]></dc:creator>
		<pubDate>Sun, 24 Jul 2022 01:02:42 +0000</pubDate>
				<category><![CDATA[Golang]]></category>
		<guid isPermaLink="false">https://www.cztcode.com/?p=4252</guid>

					<description><![CDATA[最近在学Vue和画插画，但是我也对Go挺好奇的，听说现在新项目都用Go开发，所以抽空继续学学Go语言。]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div>
<p>最近在学Vue和画插画，但是我也对Go挺好奇的，听说现在新项目都用Go开发，所以抽空继续学学Go语言。</p>



<h2 class="wp-block-heading">Go的优点</h2>



<p>参考：<a href="https://www.zhihu.com/question/21409296/answer/18184584" target="_blank" rel="noopener">https://www.zhihu.com/question/21409296/answer/18184584</a></p>



<p>可直接编译成机器码，不依赖其他库，<a href="https://www.zhihu.com/search?q=glibc&amp;search_source=Entity&amp;hybrid_search_source=Entity&amp;hybrid_search_extra={%22sourceType%22%3A%22answer%22%2C%22sourceId%22%3A18184584}" target="_blank" rel="noopener">glibc</a>的版本有一定要求，部署就是扔一个文件上去就完成了。</p>



<p>静态类型语言，但是有动态语言的感觉，静态类型的语言就是可以在编译的时候检查出来隐藏的大多数问题，动态语言的感觉就是有很多的包可以使用，写起来的效率很高。</p>



<p>语言层面支持并发，这个就是Go最大的特色，天生的支持并发，我曾经说过一句话，天生的基因和整容是有区别的，大家一样美丽，但是你喜欢整容的还是天生基因的美丽呢？Go就是基因里面支持的并发，可以充分的利用多核，很容易的使用并发。</p>



<p>内置runtime，支持垃圾回收，这属于动态语言的特性之一吧，虽然目前来说GC不算完美，但是足以应付我们所能遇到的大多数情况，特别是Go1.1之后的GC。</p>



<p>简单易学，Go语言的作者都有C的基因，那么Go自然而然就有了C的基因，那么Go关键字是25个，但是表达能力很强大，几乎支持大多数你在其他语言见过的特性：继承、重载、对象等。</p>



<p>丰富的标准库，Go目前已经内置了大量的库，特别是网络库非常强大，我最爱的也是这部分。</p>



<p>内置强大的工具，Go语言里面内置了很多工具链，最好的应该是gofmt工具，自动化格式化代码，能够让团队review变得如此的简单，代码格式一模一样，想不一样都很困难。</p>



<p>跨平台编译，如果你写的Go代码不包含cgo，那么就可以做到window系统编译linux的应用，如何做到的呢？Go引用了<a href="https://www.zhihu.com/search?q=plan9&amp;search_source=Entity&amp;hybrid_search_source=Entity&amp;hybrid_search_extra={%22sourceType%22%3A%22answer%22%2C%22sourceId%22%3A18184584}" target="_blank" rel="noopener">plan9</a>的代码，这就是不依赖系统的信息。</p>



<p>内嵌C支持，前面说了作者是C的作者，所以Go里面也可以直接包含c代码，利用现有的丰富的C库。</p>



<h3 class="wp-block-heading">适合场景</h3>



<p>服务器编程，以前你如果使用C或者C++做的那些事情，用Go来做很合适，例如处理日志、数据打包、虚拟机处理、文件系统等。</p>



<p>分布式系统，数据库代理器等</p>



<p>网络编程，这一块目前应用最广，包括Web应用、API应用、下载应用、</p>



<p>内存数据库，前一段时间google开发的groupcache，couchbase的部分组建</p>



<p>云平台，目前国外很多云平台在采用Go开发，CloudFoundy的部分组建，前VMare的技术总监自己出来搞的apcera云平台。</p>



<h2 class="wp-block-heading">安装Go环境</h2>



<p>官网下载：<a href="https://go.dev/dl/" target="_blank" rel="noopener">https://go.dev/dl/</a></p>



<p>注意M1版本的Mac下载arm64的发行版，Intel选择amd64。</p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/image-20220724083005172.png" alt="image-20220724083005172"/></figure>



<h2 class="wp-block-heading">Hello World!</h2>



<p>学新语言Hello World 是个值得纪念的时刻。</p>



<p>在Goland中创建一个src文件夹，再创建一个bin文件夹，然后在src文件夹里创建第一个Go文件，main.go。</p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/image-20220724083519595.png" alt="image-20220724083519595"/></figure>



<p>首先来看一下Hello World的程序长什么样，然后我们再逐句解释。</p>



<pre class="wp-block-code"><code>package main<br>​<br>import "fmt"<br>​<br>func main() {<br>  fmt.Println("Hello world")<br>}<br>​</code></pre>



<p>在运行中找到Go构建，将文件设置成main.go，输出目录设置成创建的bin目录。</p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/image-20220724084751182.png" alt="image-20220724084751182"/></figure>



<p>点击运行，Hello World！！！</p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/image-20220724084852965.png" alt="image-20220724084852965"/></figure>



<h2 class="wp-block-heading">逐句解释</h2>



<pre class="wp-block-code"><code>package main</code></pre>



<p>Go的编写像是C和Java的混合体，首先需要声明包名。每一段 Go 程序都必须属于一个包。一个标准的可执行的 Go 程序必须有 package main 的声明。如果一段程序是属于 main 包的，那么当执行 go install 的时候就会将其生成二进制文件，当执行这个文件时，就会调用 main 函数。</p>



<p>如果一个包中没有带有 <code>main</code> 包声明的文件，那么，Go 就会在 <code>pkg</code> 目录中创建一个 <strong>包管理</strong> (<code>.a</code>) 文件。</p>



<p>我们现在只需知道：要想生成可执行的二进制文件，必须把代码写在 <code>main</code> 包里，而且其中必须包含一个 <code>main</code> 函数作为程序的入口函数。包的名字应避免使用下划线，中划线或掺杂大写字母。</p>



<pre class="wp-block-code"><code>import "fmt"</code></pre>



<p>fmt是Go的I/O库，类似于C语言的iostream库，提供打印功能。</p>



<pre class="wp-block-code"><code>func main() {<br>  fmt.Println("Hello world")<br>}</code></pre>



<p>Go中必须有一个main函数作为入口，定义函数使用func关键字。需要注意的是<strong>main函数不能有任何参数和返回值</strong>。</p>



<p>最后调用fmt中的Println方法就可以输出Hello World了，和C语言中的Printf一样。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.cztcode.com/2022/4252/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">4252</post-id>	</item>
	</channel>
</rss>
