<?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/"
	>

<channel>
	<title>Tony Bai &#187; Array</title>
	<atom:link href="http://tonybai.com/tag/array/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Mon, 08 Jun 2026 23:32:23 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.2.1</generator>
		<item>
		<title>读懂Go的设计哲学：为什么说它是“恰到好处”的80/20语言？</title>
		<link>https://tonybai.com/2025/07/05/go-is-8020-language/</link>
		<comments>https://tonybai.com/2025/07/05/go-is-8020-language/#comments</comments>
		<pubDate>Fri, 04 Jul 2025 23:22:24 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[annotations]]></category>
		<category><![CDATA[Array]]></category>
		<category><![CDATA[async]]></category>
		<category><![CDATA[await]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Channel]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Google]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[JUnit]]></category>
		<category><![CDATA[macros]]></category>
		<category><![CDATA[map]]></category>
		<category><![CDATA[ORM]]></category>
		<category><![CDATA[RobPike]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[Slice]]></category>
		<category><![CDATA[struct]]></category>
		<category><![CDATA[Swift]]></category>
		<category><![CDATA[tag]]></category>
		<category><![CDATA[Testing]]></category>
		<category><![CDATA[XML]]></category>
		<category><![CDATA[并发]]></category>
		<category><![CDATA[序列化]]></category>
		<category><![CDATA[标准库]]></category>
		<category><![CDATA[泛型]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4876</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/07/05/go-is-8020-language 大家好，我是Tony Bai。 如果你写了一段时间的 Go，你可能会有一种独特的感觉。一方面，它简洁、高效、可靠；另一方面，你又会时常觉得它“缺少”了点什么——没有其他语言里那些功能强大、眼花缭乱的特性。 有人因此热爱 Go，有人因此“憎恨” Go。但这种“爱”与“恨”的背后，其实都指向了 Go 语言一个最核心、也最常被误解的设计哲学。最近，一篇精彩的博文《Go is 80/20 language》用一个简单而强大的心智模型，完美地诠释了这一切。 这个模型就是——Go 是一门“80/20”语言。 它旨在用 20% 的复杂度，提供 80% 的实用功能。 正如 Go 语言的创造者之一 Rob Pike 所言：“没人否认 87% 的功能比 80% 好，但问题是，那额外的 7% 功能，往往需要付出 36% 的额外工作。” 这“额外的工作”，不仅是语言实现者的负担，更是我们每一个使用者的隐性成本。 Go 的 80/20 设计实例 让我们通过几个具体的例子，来感受 Go 如何将“80/20 法则”贯彻到底。 1. 并发：Goroutines vs. C#/Rust Async Go 的并发模型极其简单：一个 go 关键字，加上用于通信的 channel。相比于 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/go-is-8020-language-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/07/05/go-is-8020-language">本文永久链接</a> &#8211; https://tonybai.com/2025/07/05/go-is-8020-language</p>
<p>大家好，我是Tony Bai。</p>
<p>如果你写了一段时间的 Go，你可能会有一种独特的感觉。一方面，它简洁、高效、可靠；另一方面，你又会时常觉得它“缺少”了点什么——没有其他语言里那些功能强大、眼花缭乱的特性。</p>
<p>有人因此热爱 Go，有人因此“憎恨” Go。但这种“爱”与“恨”的背后，其实都指向了 Go 语言一个最核心、也最常被误解的设计哲学。最近，一篇精彩的博文《<a href="https://blog.kowalczyk.info/article/d-2025-06-26/go-is-8020-language.html">Go is 80/20 language</a>》用一个简单而强大的心智模型，完美地诠释了这一切。</p>
<p>这个模型就是——<strong>Go 是一门“80/20”语言。</strong></p>
<p>它旨在用 <strong>20% 的复杂度，提供 80% 的实用功能</strong>。</p>
<p>正如 Go 语言的创造者之一 Rob Pike 所言：“没人否认 87% 的功能比 80% 好，但问题是，那额外的 7% 功能，往往需要付出 36% 的额外工作。”</p>
<p>这“额外的工作”，不仅是语言实现者的负担，更是我们每一个使用者的隐性成本。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/paid/inside-goroutine-scheduler-qr.png" alt="" /></p>
<h2>Go 的 80/20 设计实例</h2>
<p>让我们通过几个具体的例子，来感受 Go 如何将“80/20 法则”贯彻到底。</p>
<p><strong>1. 并发：Goroutines vs. C#/Rust Async</strong></p>
<p>Go 的并发模型极其简单：一个 go 关键字，加上用于通信的 channel。相比于 C# 或 Rust 中复杂的 async/await 语法、函数“着色”问题、以及需要开发者精细控制的运行时，Go 的并发模型的功能点和“旋钮”要少得多。</p>
<p>这正是 80/20 的体现。Goroutine 和 Channel 提供了 80% 最常用的并发场景解决方案，但其心智负担和实现复杂度，可能只有 async/await 的 20%。它放弃了那“额外 7%”的极致灵活性，换来的是绝大多数开发者都能轻松写对的并发程序。</p>
<p><strong>2. 测试：testing 标准库 vs. Java JUnit</strong></p>
<p>Go 的 testing 标准库只有几百行代码，数年间几乎没有大的变化。它提供了 t.Run, t.Error, b.N 等最核心的测试和基准测试功能。</p>
<p>相比之下，Java 的 JUnit 框架，拥有数万行代码和永无止境的开发迭代，提供了无数便捷的注解和高级功能。但这些功能，真的是我们日常测试所必需的吗？</p>
<p>Go 的 testing 库再次做出了 80/20 的选择：用 20% 的代码量和复杂度，满足了 80% 的测试需求，保持了核心库的稳定与简洁。</p>
<p><strong>3. 元编程：Struct Tags vs. Annotations/Macros</strong></p>
<p>有人抱怨 Go 的 Struct Tags 不如 Java 的注解或 Rust 的宏那么强大。是的，它的功能确实有限，只能附加简单的字符串元数据。</p>
<p>但这恰恰是 80% 的场景所需要的：JSON/XML 的序列化、ORM 映射、配置校验。它用最简单、最直白的方式解决了核心问题，而没有引入宏所带来的编译时复杂性、调试噩梦和陡峭的学习曲线。</p>
<p><strong>4. 泛型：内建泛型先行</strong></p>
<p>当 Go 在 1.0 版本发布时，并没有提供用户自定义泛型。但它为最需要泛型的内建类型——arrays/slices, maps, channels——提供了泛型能力(基于interface{})。</p>
<p>这个决策，是 Go 80/20 哲学最经典的体现。它在当时用最小的实现成本，解决了最痛的 80% 的问题，并让这个设计平稳地服务了 Go 社区超过十年。直到社区和语言本身都准备好了，才谨慎地引入了用户自定义泛型。</p>
<h2>警惕“功能跑步机”与“双重成本”</h2>
<p>许多其他语言，如 C#, Swift, Rust，它们的目标是“100% 的设计，哪怕付出 400% 的成本”。它们似乎陷入了一场永无止境的“功能跑步机”竞赛，不断地增加新特性。</p>
<p>博文作者一针见血地指出了“增加功能”背后，那常常被忽视的<strong>“双重成本”</strong>：</p>
<p><strong>1. 实现者成本</strong></p>
<p>每一个新功能，都会增加语言实现的复杂性。以 Swift 为例，尽管有苹果的无限预算和顶尖人才，其编译器在很长一段时间内都以慢、不稳定而闻名，跨平台能力也迟迟未能完善。这正是因为其设计的复杂性远超出了能够被完美实现的范畴。相比之下，Go 的简洁性保证了它从 1.0 版本开始，就拥有一个快速、稳定、全平台支持的编译器。</p>
<p><strong>2. 用户成本</strong></p>
<p>这是更巨大、更隐性的成本。对于我们开发者来说，学习一个新功能，绝不仅仅是学习它的语法。你需要：</p>
<ul>
<li>学习新的编程范式和设计模式。</li>
<li>学习在何种场景下应该使用它，以及<strong>更重要的，在何种场景下不应该使用它</strong>。</li>
<li>即使你决定不使用这个新功能，你的同事、你依赖的开源库也可能会用，你最终还是被迫要去理解它，整个生态的认知负荷都在上升。</li>
</ul>
<p>功能丰富的语言，最终往往需要制定严格的编码规范来限制其使用。比如 Google 的 C++ Style Guide，其存在目的就是为了将一个“95% 功能”的语言，人为地降级到“90% 功能”的子集来使用，以保证大型团队的协作效率。这恰恰从反面证明了“少即是多”的智慧。</p>
<h2>小结：少即是多，一种克制的智慧</h2>
<p>Go 的 80/20 哲学，并非是懒惰或能力不足，而是一种深思熟虑后的、极其克制的工程决策。它承认了复杂性的巨大代价，并选择把“简单”作为最高优先级。</p>
<p>它为你提供了一套足够强大、但又不至于让你迷失的工具集。它相信，通过组合这些简单的工具，你足以构建出任何复杂的系统。</p>
<p>所以，下一次当你感觉 Go “缺少”某个你习以为常的特性时，不妨换个角度思考：或许，这并非是 Go 的缺陷，而是它最宝贵的财富。</p>
<p>资料地址：https://blog.kowalczyk.info/article/d-2025-06-26/go-is-8020-language.html</p>
<hr />
<p>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/07/05/go-is-8020-language/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>“这代码迟早出事！”——复盘线上问题：六个让你头痛的Go编码坏味道</title>
		<link>https://tonybai.com/2025/05/31/six-smells-in-go/</link>
		<comments>https://tonybai.com/2025/05/31/six-smells-in-go/#comments</comments>
		<pubDate>Sat, 31 May 2025 02:36:48 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Array]]></category>
		<category><![CDATA[atomic]]></category>
		<category><![CDATA[Bug]]></category>
		<category><![CDATA[Build]]></category>
		<category><![CDATA[Channel]]></category>
		<category><![CDATA[closure]]></category>
		<category><![CDATA[Codereview]]></category>
		<category><![CDATA[Compile]]></category>
		<category><![CDATA[Database]]></category>
		<category><![CDATA[defer]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[gobuild]]></category>
		<category><![CDATA[gofmt]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomod]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[gotest]]></category>
		<category><![CDATA[govet]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[Mutex]]></category>
		<category><![CDATA[ORM]]></category>
		<category><![CDATA[recover]]></category>
		<category><![CDATA[Slice]]></category>
		<category><![CDATA[sql]]></category>
		<category><![CDATA[sqlx]]></category>
		<category><![CDATA[standardlibrary]]></category>
		<category><![CDATA[sync]]></category>
		<category><![CDATA[XML]]></category>
		<category><![CDATA[依赖管理]]></category>
		<category><![CDATA[切片]]></category>
		<category><![CDATA[匿名函数]]></category>
		<category><![CDATA[同步]]></category>
		<category><![CDATA[垃圾回收]]></category>
		<category><![CDATA[数组]]></category>
		<category><![CDATA[标准库]]></category>
		<category><![CDATA[简单]]></category>
		<category><![CDATA[编译]]></category>
		<category><![CDATA[闭包]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4769</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/05/31/six-smells-in-go 大家好，我是Tony Bai。 在日常的代码审查 (Code Review) 和线上问题复盘中，我经常会遇到一些看似不起眼，却可能埋下巨大隐患的 Go 代码问题。这些“编码坏味道”轻则导致逻辑混乱、性能下降，重则引发数据不一致、系统崩溃，甚至让团队成员在深夜被告警声惊醒，苦不堪言。 今天，我就结合自己团队中的一些“血淋淋”的经验，和大家聊聊那些曾让我（或许也曾让你）头痛不已的 Go 编码坏味道。希望通过这次复盘，我们都能从中吸取教训，写出更健壮、更优雅、更经得起考验的 Go 代码。 坏味道一：异步时序的“迷魂阵”——“我明明更新了，它怎么还是旧的？” 在高并发场景下，为了提升性能，我们经常会使用 goroutine 进行异步操作。但如果对并发操作的原子性和顺序性缺乏正确理解，就很容易掉进异步时序的陷阱。 典型场景：先异步通知，后更新状态 想象一下，我们有一个订单处理系统，当用户支付成功后，需要先异步发送一个通知给营销系统（比如发优惠券），然后再更新订单数据库的状态为“已支付”。 package main import ( "fmt" "sync" "time" ) type Order struct { ID string Status string // "pending", "paid", "notified" } func updateOrderStatusInDB(order *Order, status string) { fmt.Printf("数据库：订单 %s 状态更新为 %s\n", order.ID, status) [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/six-smells-in-go-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/05/31/six-smells-in-go">本文永久链接</a> &#8211; https://tonybai.com/2025/05/31/six-smells-in-go</p>
<p>大家好，我是Tony Bai。</p>
<p>在日常的代码审查 (Code Review) 和线上问题复盘中，我经常会遇到一些看似不起眼，却可能埋下巨大隐患的 Go 代码问题。这些“编码坏味道”轻则导致逻辑混乱、性能下降，重则引发数据不一致、系统崩溃，甚至让团队成员在深夜被告警声惊醒，苦不堪言。</p>
<p>今天，我就结合自己团队中的一些“血淋淋”的经验，和大家聊聊那些曾让我（或许也曾让你）头痛不已的 Go 编码坏味道。希望通过这次复盘，我们都能从中吸取教训，写出更健壮、更优雅、更经得起考验的 Go 代码。</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<h2>坏味道一：异步时序的“迷魂阵”——“我明明更新了，它怎么还是旧的？”</h2>
<p>在高并发场景下，为了提升性能，我们经常会使用 goroutine 进行异步操作。但如果对并发操作的原子性和顺序性缺乏正确理解，就很容易掉进异步时序的陷阱。</p>
<p><strong>典型场景：先异步通知，后更新状态</strong></p>
<p>想象一下，我们有一个订单处理系统，当用户支付成功后，需要先异步发送一个通知给营销系统（比如发优惠券），然后再更新订单数据库的状态为“已支付”。</p>
<pre><code class="go">package main

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

type Order struct {
    ID     string
    Status string // "pending", "paid", "notified"
}

func updateOrderStatusInDB(order *Order, status string) {
    fmt.Printf("数据库：订单 %s 状态更新为 %s\n", order.ID, status)
    order.Status = status // 模拟数据库更新
}

func asyncSendNotification(order *Order) {
    fmt.Printf("营销系统：收到订单 %s 通知，当前状态：%s。准备发送优惠券...\n", order.ID, order.Status)
    // 模拟耗时操作
    time.Sleep(50 * time.Millisecond)
    fmt.Printf("营销系统：订单 %s 优惠券已发送 (基于状态：%s)\n", order.ID, order.Status)
}

func main() {
    order := &amp;Order{ID: "123", Status: "pending"}
    var wg sync.WaitGroup

    fmt.Printf("主流程：订单 %s 支付成功，准备处理...\n", order.ID)

    // 坏味道：先启动异步通知，再更新数据库状态
    wg.Add(1)
    go func(o *Order) { // 注意这里传递了指针
        defer wg.Done()
        asyncSendNotification(o)
    }(order) // goroutine 捕获的是 order 指针

    // 模拟主流程的其他操作，或者数据库更新前的延时
    time.Sleep(500 * time.Millisecond) 

    updateOrderStatusInDB(order, "paid") // 更新数据库状态

    wg.Wait()
    fmt.Printf("主流程：订单 %s 处理完毕，最终状态：%s\n", order.ID, order.Status)
}
</code></pre>
<p>该示例的可能输出：</p>
<pre><code>主流程：订单 123 支付成功，准备处理...
营销系统：收到订单 123 通知，当前状态：pending。准备发送优惠券...
营销系统：订单 123 优惠券已发送 (基于状态：pending)
数据库：订单 123 状态更新为 paid
主流程：订单 123 处理完毕，最终状态：paid
</code></pre>
<p>我们看到营销系统拿到的优惠券居然是基于“pending”状态。</p>
<p><strong>问题分析：</strong></p>
<p>在上面的代码中，asyncSendNotification goroutine 和 updateOrderStatusInDB 是并发执行的。由于 asyncSendNotification 启动在先，并且捕获的是 order 指针，它很可能在 updateOrderStatusInDB 将订单状态更新为 “paid” <strong>之前</strong> 就读取了 order.Status。这就导致营销系统基于一个过时的状态（”pending”）发送了通知或优惠券，引发业务逻辑错误。</p>
<p><strong>避坑指南：</strong></p>
<ol>
<li><strong>确保关键操作的同步性或顺序性：</strong> 对于有严格先后顺序要求的操作，不要轻易异步化。如果必须异步，确保依赖的操作完成后再执行。</li>
<li><strong>使用同步原语：</strong> 利用 sync.WaitGroup、channel 等确保操作的正确顺序。例如，可以先更新数据库，再启动异步通知。</li>
<li><strong>传递值而非指针（如果适用）：</strong> 如果异步操作仅需快照数据，考虑传递值的副本，而不是指针。但在很多场景下，我们确实需要操作同一个对象。</li>
<li><strong>在异步回调中重新获取最新状态：</strong> 如果异步回调依赖最新状态，应在回调函数内部重新从可靠数据源（如数据库）获取，而不是依赖启动时捕获的状态。</li>
</ol>
<p><strong>修正示例思路：</strong></p>
<pre><code class="go">// ... (Order, updateOrderStatusInDB, asyncSendNotification 定义不变) ...
func main() {
    order := &amp;Order{ID: "123", Status: "pending"}
    var wg sync.WaitGroup

    fmt.Printf("主流程：订单 %s 支付成功，准备处理...\n", order.ID)

    updateOrderStatusInDB(order, "paid") // 先更新数据库状态

    // 再启动异步通知
    wg.Add(1)
    go func(o Order) { // 传递结构体副本，或者在异步函数内部重新获取
        defer wg.Done()
        // 实际场景中，如果 asyncSendNotification 依赖的是更新后的状态，
        // 它应该有能力从某个地方（比如参数，或者内部重新查询）获取到 "paid" 这个状态。
        // 这里简化为直接使用传入时的状态，但强调其应为 "paid"。
        // 或者，更好的方式是 asyncSendNotification 接受一个 status 参数。
        clonedOrderForNotification := o // 假设我们传递的是更新后的状态的副本
        asyncSendNotification(&amp;clonedOrderForNotification)
    }(*order) // 传递 order 的副本，此时 order.Status 已经是 "paid"

    wg.Wait()
    fmt.Printf("主流程：订单 %s 处理完毕，最终状态：%s\n", order.ID, order.Status)
}
</code></pre>
<h2>坏味道二：指针与闭包的“爱恨情仇”——“我以为它没变，结果它却跑了！”</h2>
<p>闭包是 Go 语言中一个强大的特性，它能够捕获其词法作用域内的变量。然而，当闭包捕获的是指针，并且这个指针指向的数据在 goroutine 启动后可能被外部修改，或者指针本身被重新赋值时，就可能导致并发问题和难以预料的行为。虽然 Go 1.22+ 通过实验性的 GOEXPERIMENT=loopvar 改变了 for 循环变量的捕获语义，解决了经典的循环变量闭包陷阱，但指针与闭包结合时对共享可变状态的考量依然重要。</p>
<p><strong>典型场景：闭包捕获指针，外部修改指针或其指向内容</strong></p>
<p>我们来看一个不涉及循环变量，但同样能体现指针与闭包问题的场景：</p>
<pre><code class="go">package main

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

type Config struct {
    Version string
    Timeout time.Duration
}

func watchConfig(cfg *Config, wg *sync.WaitGroup) {
    defer wg.Done()
    // 这个 goroutine 期望在其生命周期内使用 cfg 指向的配置
    // 但如果外部在它执行期间修改了 cfg 指向的内容，或者 cfg 本身被重新赋值，
    // 那么这个 goroutine 看到的内容就可能不是启动时的那个了。
    fmt.Printf("Watcher: 开始监控配置 (Version: %s, Timeout: %v)\n", cfg.Version, cfg.Timeout)
    time.Sleep(100 * time.Millisecond) // 模拟监控工作
    fmt.Printf("Watcher: 监控结束，使用的配置 (Version: %s, Timeout: %v)\n", cfg.Version, cfg.Timeout)
}

func main() {
    currentConfig := &amp;Config{Version: "v1.0", Timeout: 5 * time.Second}
    var wg sync.WaitGroup

    fmt.Printf("主流程：初始配置 (Version: %s, Timeout: %v)\n", currentConfig.Version, currentConfig.Timeout)

    // 启动一个 watcher goroutine，它捕获了 currentConfig 指针
    wg.Add(1)
    go watchConfig(currentConfig, &amp;wg) // currentConfig 指针被传递

    // 主流程在 watcher goroutine 执行期间，修改了 currentConfig 指向的内容
    time.Sleep(10 * time.Millisecond) // 确保 watcher goroutine 已经启动并打印了初始配置
    fmt.Println("主流程：检测到配置更新，准备在线修改...")
    currentConfig.Version = "v2.0" // 直接修改了指针指向的内存内容
    currentConfig.Timeout = 10 * time.Second
    fmt.Printf("主流程：配置已修改为 (Version: %s, Timeout: %v)\n", currentConfig.Version, currentConfig.Timeout)

    // 或者更极端的情况，主流程让 currentConfig 指向了一个全新的 Config 对象
    // time.Sleep(10 * time.Millisecond)
    // fmt.Println("主流程：检测到配置需要完全替换...")
    // currentConfig = &amp;Config{Version: "v3.0", Timeout: 15 * time.Second} // currentConfig 指向了新的内存地址
    // fmt.Printf("主流程：配置已替换为 (Version: %s, Timeout: %v)\n", currentConfig.Version, currentConfig.Timeout)
    // 注意：如果 currentConfig 被重新赋值指向新对象，原 watchConfig goroutine 仍然持有旧对象的指针。
    // 但如果原意是让 watchConfig 感知到“最新的配置”，那么这种方式是错误的。

    wg.Wait()
    fmt.Println("主流程：所有处理完毕。")

    fmt.Println("\n--- 更安全的做法：传递副本或不可变快照 ---")
    // 更安全的做法：如果 goroutine 需要的是启动时刻的配置快照
    stableConfig := &amp;Config{Version: "v1.0-stable", Timeout: 5 * time.Second}
    configSnapshot := *stableConfig // 创建一个副本

    wg.Add(1)
    go func(cfgSnapshot Config, wg *sync.WaitGroup) { // 传递的是 Config 值的副本
        defer wg.Done()
        fmt.Printf("SafeWatcher: 开始监控配置 (Version: %s, Timeout: %v)\n", cfgSnapshot.Version, cfgSnapshot.Timeout)
        time.Sleep(100 * time.Millisecond)
        // 即使外部修改了 stableConfig，cfgSnapshot 依然是启动时的值
        fmt.Printf("SafeWatcher: 监控结束，使用的配置 (Version: %s, Timeout: %v)\n", cfgSnapshot.Version, cfgSnapshot.Timeout)
    }(configSnapshot, &amp;wg)

    time.Sleep(10 * time.Millisecond)
    stableConfig.Version = "v2.0-stable" // 修改原始配置
    stableConfig.Timeout = 10 * time.Second
    fmt.Printf("主流程：stableConfig 已修改为 (Version: %s, Timeout: %v)\n", stableConfig.Version, stableConfig.Timeout)

    wg.Wait()
    fmt.Println("主流程：所有安全处理完毕。")
}
</code></pre>
<p><strong>问题分析：</strong></p>
<p>在第一个示例中，watchConfig goroutine 通过闭包（函数参数也是一种闭包形式）捕获了 currentConfig 指针。这意味着 watchConfig 内部对 cfg 的访问，实际上是访问 main goroutine 中 currentConfig 指针所指向的那块内存。</p>
<ul>
<li><strong>当外部修改指针指向的内容时：</strong> 如代码中 currentConfig.Version = “v2.0&#8243;，watchConfig goroutine 在后续访问 cfg.Version 时，会看到这个被修改后的新值，这可能不是它启动时期望的行为。</li>
<li><strong>当外部修改指针本身时 (注释掉的极端情况)：</strong> 如果 currentConfig = &amp;Config{Version: “v3.0&#8243;, &#8230;}，那么 watchConfig 捕获的 cfg 仍然指向<strong>原始的 Config 对象</strong>（即 “v1.0&#8243; 那个）。如果此时的业务逻辑期望 watchConfig 使用“最新的配置对象”，那么这种捕获指针的方式就会导致错误。</li>
</ul>
<p>这些问题的根源在于对<strong>共享可变状态</strong>的并发访问缺乏控制，以及对指针生命周期和闭包捕获机制的理解不够深入。</p>
<p><strong>避坑指南：</strong></p>
<ol>
<li>
<p><strong>明确 goroutine 需要的数据快照还是共享状态：</strong></p>
<ul>
<li>如果 goroutine 只需要启动时刻的数据快照，并且不希望受外部修改影响，那么应该<strong>传递值的副本</strong>给 goroutine（或者在闭包内部创建副本）。如第二个示例中的 configSnapshot。</li>
<li>如果 goroutine 需要与外部共享并感知状态变化，那么必须使用<strong>同步机制</strong>（如 mutex、channel、atomic 操作）来保护对共享状态的访问，确保数据一致性和避免竞态条件。</li>
</ul>
</li>
<li>
<p><strong>谨慎捕获指针，特别是那些可能在 goroutine 执行期间被修改的指针：</strong></p>
<ul>
<li>如果捕获了指针，要清楚地知道这个指针的生命周期，以及它指向的数据是否会被其他 goroutine 修改。</li>
<li>如果指针指向的数据是可变的，并且多个 goroutine 会并发读写，<strong>必须加锁保护</strong>。</li>
</ul>
</li>
<li>
<p><strong>考虑数据的不可变性：</strong> 如果可能，尽量使用不可变的数据结构。将不可变的数据传递给 goroutine 是最安全的并发方式之一。</p>
</li>
<li>
<p><strong>对于经典的 for 循环启动 goroutine 捕获循环变量的问题：</strong></p>
<ul>
<li><strong>Go 1.22+ (启用 GOEXPERIMENT=loopvar) 或未来版本：</strong> 语言层面已经解决了每次迭代共享同一个循环变量的问题，每次迭代会创建新的变量实例。此时，直接在闭包中捕获循环变量是安全的。</li>
<li><strong>Go 1.21 及更早版本 (或未启用 loopvar 实验特性)：</strong> 仍然需要通过<strong>函数参数传递</strong>的方式来确保每个 goroutine 捕获到正确的循环变量值。例如：</li>
</ul>
</li>
</ol>
<pre><code class="go">for i, v := range values {
    valCopy := v // 如果 v 是复杂类型，可能需要更深的拷贝
    indexCopy := i
    go func() {
        // 使用 valCopy 和 indexCopy
    }()
}
// 或者更推荐的方式：
for i, v := range values {
    go func(idx int, valType ValueType) { // ValueType 是 v 的类型
        // 使用 idx 和 valType
    }(i, v)
}
</code></pre>
<p>虽然 Go 语言在 for 循环变量捕获方面做出了改进，但指针与闭包结合时对共享状态和生命周期的审慎思考，仍然是编写健壮并发程序的关键。</p>
<h2>坏味道三：错误处理的哲学——“是Bug就让它崩！”真的好吗？</h2>
<p>Go 语言通过返回 error 值来处理可预期的错误，而 panic 则用于表示真正意外的、程序无法继续正常运行的严重错误，通常由运行时错误（如数组越界、空指针解引用）或显式调用 panic() 引发。当 panic 发生且未被 recover 时，程序会崩溃并打印堆栈信息。</p>
<p>一种常见的观点是：“如果是 Bug，就应该让它尽快崩溃 (Fail Fast)”，以便问题能被及时发现和修复。这种观点在很多情况下是合理的。然而，在某些 <strong>mission-critical（关键任务）系统</strong>中，例如金融交易系统、空中交通管制系统、重要的基础设施服务等，一次意外的宕机重启可能导致不可估量的损失或严重后果。在这些场景下，即使因为一个未捕获的 Bug 导致了 panic，我们也可能期望系统能有一定的“韧性”，而不是轻易“放弃治疗”。</p>
<p><strong>典型场景：一个关键服务在处理请求时因 Bug 发生 Panic</strong></p>
<pre><code class="go">package main

import (
    "fmt"
    "net/http"
    "runtime/debug"
    "time"
)

// 模拟一个关键数据处理器
type CriticalDataProcessor struct {
    // 假设有一些内部状态
    activeConnections int
    lastProcessedID   string
}

// 处理数据的方法，这里故意引入一个可能导致 panic 的 bug
func (p *CriticalDataProcessor) Process(dataID string, payload map[string]interface{}) error {
    fmt.Printf("Processor: 开始处理数据 %s\n", dataID)
    p.activeConnections++
    defer func() { p.activeConnections-- }() // 确保连接数正确管理

    // 模拟一些复杂逻辑
    time.Sleep(50 * time.Millisecond)

    // ！！！潜在的 Bug ！！！
    // 假设 payload 中 "user" 字段应该是一个结构体指针，但有时可能是 nil
    // 或者，某个深层嵌套的访问可能导致空指针解引用
    // 为了演示，我们简单模拟一个 nil map 访问导致的 panic
    var userDetails map[string]string
    // userDetails = payload["user"].(map[string]string) // 这本身也可能 panic 如果类型断言失败
    // 为了稳定复现 panic，我们直接让 userDetails 为 nil
    if dataID == "buggy-data-001" { // 特定条件下触发 bug
        fmt.Printf("Processor: 触发 Bug，尝试访问 nil map '%s'\n", userDetails["name"]) // 这里会 panic
    }

    p.lastProcessedID = dataID
    fmt.Printf("Processor: 数据 %s 处理成功\n", dataID)
    return nil
}

// HTTP Handler - 版本1: 不做任何 recover
func handleRequestVersion1(processor *CriticalDataProcessor) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        dataID := r.URL.Query().Get("id")
        if dataID == "" {
            http.Error(w, "缺少 id 参数", http.StatusBadRequest)
            return
        }

        // 模拟从请求中获取 payload
        payload := make(map[string]interface{})
        // if dataID == "buggy-data-001" {
        //  // payload["user"] 可能是 nil 或错误类型，导致 Process 方法 panic
        // }

        err := processor.Process(dataID, payload) // 如果 Process 发生 panic，整个 HTTP server goroutine 会崩溃
        if err != nil {
            http.Error(w, fmt.Sprintf("处理失败: %v", err), http.StatusInternalServerError)
            return
        }
        fmt.Fprintf(w, "请求 %s 处理成功\n", dataID)
    }
}

// HTTP Handler - 版本2: 在每个请求处理的 goroutine 顶层 recover
func handleRequestVersion2(processor *CriticalDataProcessor) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                fmt.Fprintf(os.Stderr, "!!!!!!!!!!!!!! PANIC 捕获 !!!!!!!!!!!!!!\n")
                fmt.Fprintf(os.Stderr, "错误: %v\n", err)
                fmt.Fprintf(os.Stderr, "堆栈信息:\n%s\n", debug.Stack())
                fmt.Fprintf(os.Stderr, "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n")

                // 向客户端返回一个通用的服务器错误
                http.Error(w, "服务器内部错误，请稍后重试", http.StatusInternalServerError)

                // 可以在这里记录更详细的错误到日志系统、发送告警等
                // 例如：log.Errorf("Panic recovered: %v, Stack: %s", err, debug.Stack())
                // metrics.Increment("panic_recovered_total")

                // 重要：根据系统的 mission-critical 程度和业务逻辑，
                // 这里可能还需要做一些清理工作，或者尝试让系统保持在一种“安全降级”的状态。
                // 但要注意，recover 后的状态可能是不确定的，需要非常谨慎。
            }
        }()

        dataID := r.URL.Query().Get("id")
        if dataID == "" {
            http.Error(w, "缺少 id 参数", http.StatusBadRequest)
            return
        }
        payload := make(map[string]interface{})

        err := processor.Process(dataID, payload)
        if err != nil {
            // 正常错误处理
            http.Error(w, fmt.Sprintf("处理失败: %v", err), http.StatusInternalServerError)
            return
        }
        fmt.Fprintf(w, "请求 %s 处理成功\n", dataID)
    }
}

func main() {
    processor := &amp;CriticalDataProcessor{}

    // mux1 使用 Version1 handler (不 recover)
    // mux2 使用 Version2 handler (recover)

    // 启动 HTTP 服务器 (这里为了演示，只启动一个，实际中会选择一个)
    // 你可以注释掉一个，运行另一个来观察效果

    // http.HandleFunc("/v1/process", handleRequestVersion1(processor))
    // fmt.Println("V1 Server (不 recover) 启动在 :8080/v1/process")
    // go http.ListenAndServe(":8080", nil)

    http.DefaultServeMux.HandleFunc("/v2/process", handleRequestVersion2(processor))
    fmt.Println("V2 Server (recover) 启动在 :8081/v2/process")
    go http.ListenAndServe(":8081", nil)

    fmt.Println("\n请在浏览器或使用 curl 测试:")
    fmt.Println("  正常请求: curl 'http://localhost:8081/v2/process?id=normal-data-002'")
    fmt.Println("  触发Bug的请求: curl 'http://localhost:8081/v2/process?id=buggy-data-001'")
    fmt.Println("  (如果启动V1服务，触发Bug的请求会导致服务崩溃)")

    select {} // 阻塞 main goroutine，保持服务器运行
}
</code></pre>
<p><strong>问题分析：</strong></p>
<ul>
<li><strong>不 Recover (handleRequestVersion1)：</strong> 当 processor.Process 方法因为 Bug（例如访问 nil map userDetails["name"]）而发生 panic 时，如果这个 panic 没有在当前 goroutine 的调用栈中被 recover，它会一直向上传播。对于由 net/http 包为每个请求创建的 goroutine，如果 panic 未被处理，将导致该 goroutine 崩溃。在某些情况下（取决于 Go 版本和 HTTP server 实现的细节），这可能导致整个 HTTP 服务器进程终止，或者至少是该连接的处理异常中断，影响服务可用性。</li>
<li><strong>Recover (handleRequestVersion2)：</strong> 通过在每个请求处理的 goroutine 顶层使用 defer func() { recover() }()，我们可以捕获这个由 Bug 引发的 panic。捕获后，我们可以：
<ul>
<li>记录详细的错误信息和堆栈跟踪，便于事后分析和修复 Bug。</li>
<li>向当前请求的客户端返回一个通用的错误响应（例如 HTTP 500），而不是让连接直接断开或无响应。</li>
<li><strong>关键在于：</strong> 阻止了单个请求处理中的 Bug 导致的 panic 扩散到导致整个服务不可用的地步。服务本身仍然可以继续处理其他正常的请求。</li>
</ul>
</li>
</ul>
<p><strong>“是Bug就让它崩！”的观点在很多开发和测试环境中是值得提倡的，因为它能让我们更快地发现和定位问题。然而，在线上，特别是对于 mission-critical 系统：</strong></p>
<ul>
<li><strong>可用性是第一要务：</strong> 一次意外的全面宕机，可能比单个请求处理失败带来的损失大得多。</li>
<li><strong>数据一致性风险：</strong> 如果 panic 发生在关键数据操作的中间状态，直接崩溃可能导致数据不一致或损坏。recover 之后虽然也需要谨慎处理状态，但至少给了我们一个尝试回滚或记录问题的机会。</li>
<li><strong>用户体验：</strong> 对用户而言，遇到一个“服务器内部错误”然后重试，通常比整个服务长时间无法访问要好一些。</li>
</ul>
<p><strong>避坑与决策指南：</strong></p>
<ol>
<li><strong>在关键服务的请求处理入口或 goroutine 顶层设置 recover 机制：</strong> 这是构建健壮服务的推荐做法。
<ul>
<li>recover 应该与 defer 配合使用。</li>
<li>在 recover 逻辑中，务必记录详细的错误信息、堆栈跟踪，并考虑集成到告警系统。</li>
</ul>
</li>
<li><strong>recover 之后做什么？——视情况而定，但要极其谨慎：</strong>
<ul>
<li><strong>对于单个请求处理 goroutine：</strong> 通常的做法是记录错误，向当前客户端返回错误响应，然后让该 goroutine 正常结束。避免让这个 panic 影响其他请求。</li>
<li><strong>对于核心的、管理全局状态的 goroutine：</strong> 如果发生 panic，表明系统可能处于一种非常不稳定的状态。recover 后，可能需要执行一些清理操作，尝试将系统恢复到一个已知的安全状态，或者进行优雅关闭并重启。<strong>绝对不应该假装什么都没发生，继续使用可能已损坏的状态。</strong></li>
<li><strong>“苟活”的度：</strong> “苟活”不代表对 Bug 视而不见。recover 的目的是保障服务的整体可用性，同时为我们争取定位和修复 Bug 的时间。捕获到的 panic 必须被视为高优先级事件进行处理。</li>
</ul>
</li>
<li><strong>库代码应极度克制 panic：</strong> 库不应该替应用程序做“是否崩溃”的决策。</li>
<li><strong>测试，测试，再测试：</strong> 通过充分的单元测试、集成测试和压力测试，尽可能在上线前发现和消除潜在的 Bug，减少线上发生 panic 的概率。可以使用 Go 的 race detector 来检测并发代码中的竞态条件。</li>
<li><strong>不要滥用 panic/recover 作为正常的错误处理机制：</strong> panic/recover 主要用于处理不可预料的、灾难性的运行时错误或程序缺陷，而不是替代 error 返回值来处理业务逻辑中的预期错误。</li>
</ol>
<p>“是Bug就让它崩！”在开发阶段有助于快速发现问题，但在生产环境，特别是 mission-critical 系统中，<strong>“有控制地恢复，详细记录，并保障整体服务可用性”</strong> 往往是更明智的选择。这并不意味着容忍 Bug，而是采用一种更成熟、更负责任的方式来应对突发状况，确保系统在面对未知错误时仍能表现出足够的韧性。</p>
<h2>坏味道四：http.Client 的“一次性”误区——“每次都新建，省心又省事？”</h2>
<p>Go 标准库的 net/http 包提供了强大的 HTTP客户端功能。但有些开发者（尤其是初学者）在使用 http.Client 时，会为每一个 HTTP 请求都创建一个新的 http.Client 实例。</p>
<p><strong>典型场景：函数内部频繁创建 http.Client</strong></p>
<pre><code class="go">package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "time"
)

// 坏味道：每次调用都创建一个新的 http.Client
func fetchDataFromAPI(url string) (string, error) {
    client := &amp;http.Client{ // 每次都新建 Client
        Timeout: 10 * time.Second,
    }
    resp, err := client.Get(url)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return "", err
    }
    return string(body), nil
}

// 正确的方式：复用 http.Client
var sharedClient = &amp;http.Client{ // 全局或适当范围复用的 Client
    Timeout: 10 * time.Second,
    // 可以配置 Transport 以控制连接池等
    // Transport: &amp;http.Transport{
    //  MaxIdleConns:        100,
    //  MaxIdleConnsPerHost: 10,
    //  IdleConnTimeout:     90 * time.Second,
    // },
}

func fetchDataFromAPIReusable(url string) (string, error) {
    resp, err := sharedClient.Get(url) // 复用 Client
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return "", err
    }
    return string(body), nil
}

func main() {
    // 模拟多次调用
    // 如果使用 fetchDataFromAPI，每次都会创建新的 TCP 连接
    // _,_ = fetchDataFromAPI("https://www.example.com")
    // _,_ = fetchDataFromAPI("https://www.example.com")

    // 使用 fetchDataFromAPIReusable，会复用连接
    data, err := fetchDataFromAPIReusable("https://httpbin.org/get")
    if err != nil {
        fmt.Printf("请求错误: %v\n", err)
        return
    }
    fmt.Printf("获取到数据 (部分): %s...\n", data[:50])

    data, err = fetchDataFromAPIReusable("https://httpbin.org/get")
    if err != nil {
        fmt.Printf("请求错误: %v\n", err)
        return
    }
    fmt.Printf("再次获取到数据 (部分): %s...\n", data[:50])
}
</code></pre>
<p><strong>问题分析：</strong></p>
<p>http.Client 的零值或通过 &amp;http.Client{} 创建的实例，其内部的 Transport 字段（通常是 *http.Transport）会维护一个 TCP 连接池，并处理 HTTP keep-alive 等机制以复用连接。<strong>如果为每个请求都创建一个新的 http.Client，那么每次请求都会经历完整的 TCP 连接建立过程（三次握手），并在请求结束后关闭连接。</strong></p>
<p><strong>危害：</strong></p>
<ol>
<li><strong>性能下降：</strong> 频繁的 TCP 连接建立和关闭开销巨大。</li>
<li><strong>资源消耗增加：</strong> 短时间内大量创建连接可能导致客户端耗尽可用端口，或者服务器端累积大量 TIME_WAIT 状态的连接，最终影响整个系统的吞吐量和稳定性。</li>
</ol>
<p><strong>避坑指南：</strong></p>
<ol>
<li><strong>复用 http.Client 实例：</strong> 这是官方推荐的最佳实践。可以在全局范围创建一个 http.Client 实例（如 http.DefaultClient，或者一个自定义配置的实例），并在所有需要发起 HTTP 请求的地方复用它。</li>
<li><strong>http.Client 是并发安全的：</strong> 你可以放心地在多个 goroutine 中共享和使用同一个 http.Client 实例。</li>
<li><strong>自定义 Transport：</strong> 如果需要更细致地控制连接池大小、超时时间、TLS 配置等，可以创建一个自定义的 http.Transport 并将其赋给 http.Client 的 Transport 字段。</li>
</ol>
<h2>坏味道五：API 设计的“文档缺失”——“这参数啥意思？猜猜看！”</h2>
<p>良好的 API 设计是软件质量的基石，而清晰、准确的文档则是 API 可用性的关键。然而，在实际项目中，我们常常会遇到一些 API，其参数、返回值、错误码、甚至行为语义都缺乏明确的文档说明，导致用户（调用方）在集成时只能靠“猜”或者阅读源码，极易产生误用。</p>
<p><strong>典型场景：一个“凭感觉”调用的服务发现 API</strong></p>
<p>假设我们有一个类似 Nacos Naming 的服务发现客户端，其 GetInstance API 的文档非常简略，或者干脆没有文档，只暴露了函数签名：</p>
<pre><code class="go">package main

import (
    "errors"
    "fmt"
    "math/rand"
    "time"
)

// 假设这是 Nacos Naming 客户端的一个简化接口
type NamingClient interface {
    // GetInstance 获取服务实例。
    // 关键问题：
    // 1. serviceName 需要包含 namespace/group 信息吗？格式是什么？
    // 2. clusters 是可选的吗？如果提供多个，是随机选一个还是有特定策略？
    // 3. healthyOnly 如果为 true，是否会过滤掉不健康的实例？如果不健康实例是唯一选择呢？
    // 4. 返回的 instance 是什么结构？如果找不到实例，是返回 nil, error 还是空对象？
    // 5. error 可能有哪些类型？调用方需要如何区分处理？
    // 6. 这个调用是阻塞的吗？超时机制是怎样的？
    // 7. 是否有本地缓存机制？缓存刷新策略是？
    GetInstance(serviceName string, clusters []string, healthyOnly bool) (instance interface{}, err error)
}

// 一个非常简化的模拟实现 (坏味道的 API 设计，文档缺失)
type MockNamingClient struct{}

func (c *MockNamingClient) GetInstance(serviceName string, clusters []string, healthyOnly bool) (interface{}, error) {
    fmt.Printf("尝试获取服务: %s, 集群: %v, 只获取健康实例: %t\n", serviceName, clusters, healthyOnly)

    // 模拟一些内部逻辑和不确定性
    if serviceName == "" {
        return nil, errors.New("服务名不能为空 (错误码: Naming-1001)") // 文档里有这个错误码说明吗？
    }

    // 假设我们内部有一些实例数据
    instances := map[string][]string{
        "OrderService":   {"10.0.0.1:8080", "10.0.0.2:8080"},
        "PaymentService": {"10.0.1.1:9090"},
    }

    // 模拟集群选择逻辑 (文档缺失，用户只能猜)
    selectedCluster := ""
    if len(clusters) &gt; 0 {
        selectedCluster = clusters[rand.Intn(len(clusters))] // 随机选一个？
        fmt.Printf("选择了集群: %s\n", selectedCluster)
    }

    // 模拟健康检查和实例返回 (文档缺失)
    if healthyOnly &amp;&amp; rand.Float32() &lt; 0.3 { // 30% 概率找不到健康实例
        return nil, fmt.Errorf("在集群 %s 中未找到 %s 的健康实例 (错误码: Naming-2003)", selectedCluster, serviceName)
    }

    if insts, ok := instances[serviceName]; ok &amp;&amp; len(insts) &gt; 0 {
        return insts[rand.Intn(len(insts))], nil // 返回一个实例地址
    }

    return nil, fmt.Errorf("服务 %s 未找到 (错误码: Naming-4004)", serviceName)
}

func main() {
    client := &amp;MockNamingClient{}

    // 用户A的调用 (基于猜测)
    fmt.Println("用户A 调用:")
    instA, errA := client.GetInstance("OrderService", []string{"clusterA", "clusterB"}, true)
    if errA != nil {
        fmt.Printf("用户A 获取实例失败: %v\n", errA)
    } else {
        fmt.Printf("用户A 获取到实例: %v\n", instA)
    }

    fmt.Println("\n用户B 的调用 (换一种猜测):")
    // 用户B 可能不知道 serviceName 需要什么格式，或者 clusters 参数的意义
    instB, errB := client.GetInstance("com.example.PaymentService", nil, false) // serviceName 格式？clusters 为 nil 会怎样？
    if errB != nil {
        fmt.Printf("用户B 获取实例失败: %v\n", errB)
    } else {
        fmt.Printf("用户B 获取到实例: %v\n", instB)
    }
}
</code></pre>
<p><strong>问题分析：</strong></p>
<p>当 API 的设计者没有提供清晰、详尽的文档来说明每个参数的含义、取值范围、默认行为、边界条件、错误类型以及API的整体行为和副作用时，API 的使用者就只能依赖猜测、尝试，甚至阅读源码（如果开源的话）来理解如何正确调用。</p>
<p><strong>危害：</strong></p>
<ol>
<li><strong>极易误用：</strong> 用户可能以 API 设计者未预期的方式调用接口，导致程序行为不符合预期，甚至引发错误。</li>
<li><strong>集成成本高：</strong> 理解和调试一个文档不清晰的 API 非常耗时。</li>
<li><strong>脆弱的依赖：</strong> 当 API 的内部实现或未明确定义的行为发生变化时，依赖这些隐性行为的调用方代码很可能会中断。</li>
<li><strong>难以排查问题：</strong> 出现问题时，很难判断是调用方使用不当，还是 API 本身的缺陷。</li>
</ol>
<p><strong>避坑指南 (针对 API 设计者)：</strong></p>
<ol>
<li><strong>编写清晰、准确、详尽的文档是 API 设计不可或缺的一部分！</strong> 这不仅仅是注释，可能还包括独立的 API 参考手册、用户指南和最佳实践。</li>
<li><strong>参数和返回值要有明确的语义：</strong> 名称应自解释，复杂类型应有结构和字段说明。
<ul>
<li>例如，serviceName 是否需要包含命名空间或分组信息？格式是什么？</li>
<li>clusters 参数是可选的吗？如果提供多个，选择策略是什么？是轮询、随机还是有特定优先级？</li>
<li>healthyOnly 的确切行为是什么？如果没有健康的实例，是返回错误还是有其他回退逻辑？</li>
</ul>
</li>
<li><strong>明确约定边界条件和错误情况：</strong>
<ul>
<li>哪些参数是必需的，哪些是可选的？可选参数的默认值是什么？</li>
<li>对于无效输入，API 会如何响应？返回哪些具体的错误码或错误信息？（例如，示例中的 Naming-1001, Naming-2003, Naming-4004 是否有统一的文档说明其含义和建议处理方式？）</li>
<li>API 调用可能产生的副作用是什么？</li>
</ul>
</li>
<li><strong>提供清晰的调用示例：</strong> 针对常见的用例，提供可运行的代码示例。</li>
<li><strong>考虑 API 的易用性和健壮性：</strong>
<ul>
<li>是否需要版本化？</li>
<li>是否需要幂等性保证？</li>
<li>认证和授权机制是否清晰？</li>
<li>超时和重试策略是怎样的？</li>
</ul>
</li>
<li><strong>将 API 的使用者视为首要客户：</strong> 站在使用者的角度思考，他们需要哪些信息才能轻松、正确地使用你的 API。</li>
</ol>
<p><strong>对于 API 的使用者：</strong> 当遇到文档不清晰的 API 时，除了“猜测”，更积极的做法是向 API 提供方寻求澄清，或者在有条件的情况下，参与到 API 文档的改进和完善中。</p>
<p>在之前《<a href="https://tonybai.com/2025/05/23/go-api-design-mcp-sdk/">API设计的“Go境界”：Go团队设计MCP SDK过程中的取舍与思考</a>》一文中，我们了见识了Go团队的API设计艺术，大家可以认知阅读和参考。</p>
<h2>坏味道六：匿名函数类型签名的“笨拙感”——“这函数参数看着眼花缭乱！”</h2>
<p>Go 语言的函数是一等公民，可以作为参数传递，也可以作为返回值。这为编写高阶函数和实现某些设计模式提供了极大的灵活性。然而，当匿名函数的类型签名（特别是嵌套或包含多个复杂函数类型参数时）直接写在函数定义中时，代码的可读性会大大降低，显得冗余和笨拙。</p>
<p><strong>典型场景：复杂的函数签名</strong></p>
<pre><code class="go">package main

import (
    "errors"
    "fmt"
    "strings"
)

// 坏味道：函数签名中直接嵌入复杂的匿名函数类型
func processData(
    data []string,
    filterFunc func(string) bool, // 参数1：一个过滤函数
    transformFunc func(string) (string, error), // 参数2：一个转换函数
    aggregatorFunc func([]string) string, // 参数3：一个聚合函数
) (string, error) {
    var filteredData []string
    for _, d := range data {
        if filterFunc(d) {
            transformed, err := transformFunc(d)
            if err != nil {
                // 注意：这里为了简化，直接返回了第一个遇到的错误
                // 实际应用中可能需要更复杂的错误处理逻辑，比如收集所有错误
                return "", fmt.Errorf("转换 '%s' 失败: %w", d, err)
            }
            filteredData = append(filteredData, transformed)
        }
    }
    if len(filteredData) == 0 {
        return "", errors.New("没有数据需要聚合")
    }
    return aggregatorFunc(filteredData), nil
}

// 使用 type 定义函数类型别名，代码更清晰
type StringFilter func(string) bool
type StringTransformer func(string) (string, error)
type StringAggregator func([]string) string

func processDataWithTypeAlias(
    data []string,
    filter StringFilter,
    transform StringTransformer,
    aggregate StringAggregator,
) (string, error) {
    // 函数体与 processData 相同
    var filteredData []string
    for _, d := range data {
        if filter(d) {
            transformed, err := transform(d)
            if err != nil {
                return "", fmt.Errorf("转换 '%s' 失败: %w", d, err)
            }
            filteredData = append(filteredData, transformed)
        }
    }
    if len(filteredData) == 0 {
        return "", errors.New("没有数据需要聚合")
    }
    return aggregate(filteredData), nil
}

func main() {
    sampleData := []string{"  apple  ", "Banana", "  CHERRY  ", "date"}

    // 使用原始的 processData，函数调用时也可能显得冗长
    result, err := processData(
        sampleData,
        func(s string) bool { return len(strings.TrimSpace(s)) &gt; 0 },
        func(s string) (string, error) {
            trimmed := strings.TrimSpace(s)
            if strings.ToLower(trimmed) == "banana" { // 假设banana是不允许的
                return "", errors.New("包含非法水果banana")
            }
            return strings.ToUpper(trimmed), nil
        },
        func(s []string) string { return strings.Join(s, ", ") },
    )

    if err != nil {
        fmt.Printf("处理错误 (原始方式): %v\n", err)
    } else {
        fmt.Printf("处理结果 (原始方式): %s\n", result)
    }

    // 使用 processDataWithTypeAlias，定义和调用都更清晰
    filter := func(s string) bool { return len(strings.TrimSpace(s)) &gt; 0 }
    transformer := func(s string) (string, error) {
        trimmed := strings.TrimSpace(s)
        if strings.ToLower(trimmed) == "banana" {
            return "", errors.New("包含非法水果banana")
        }
        return strings.ToUpper(trimmed), nil
    }
    aggregator := func(s []string) string { return strings.Join(s, ", ") }

    resultTyped, errTyped := processDataWithTypeAlias(sampleData, filter, transformer, aggregator)
    if errTyped != nil {
        fmt.Printf("处理错误 (类型别名方式): %v\n", errTyped)
    } else {
        fmt.Printf("处理结果 (类型别名方式): %s\n", resultTyped)
    }
}
</code></pre>
<p><strong>问题分析：</strong></p>
<p>Go 语言的类型系统是强类型且显式的。函数类型本身也是一种类型。当我们将一个函数类型（特别是具有多个参数和返回值的复杂函数类型）直接作为另一个函数的参数类型或返回值类型时，会导致函数签名变得非常长，难以阅读和理解。这与 Go 追求简洁和可读性的哲学在观感上有所冲突。</p>
<p><strong>避坑指南：</strong></p>
<ol>
<li><strong>使用 type 关键字定义函数类型别名：</strong> 这是解决此类问题的最推荐、最地道也是最常见的方法。通过为复杂的函数签名定义一个有意义的类型名称，可以极大地提高代码的可读性和可维护性。如示例中的 StringFilter, StringTransformer, StringAggregator。</li>
<li><strong>何时可以不使用类型别名：</strong>
<ul>
<li>当函数签名非常简单（例如 func() 或 func(int) int）且该函数类型只在局部、极少数地方使用时，直接写出可能问题不大。</li>
<li>但一旦函数签名变复杂，或者该函数类型需要在多个地方使用（作为不同函数的参数或返回值，或者作为结构体字段类型），就应该毫不犹豫地使用类型别名。</li>
</ul>
</li>
<li><strong>理解背后的设计考量：</strong> Go 语言强调类型的明确性。虽然直接写出函数类型显得“笨拙”，但也保证了类型信息在代码中的完全显露，避免了某些动态语言中因类型不明确可能导致的困惑。类型别名则是在这种明确性的基础上，提供了提升可读性的手段。</li>
</ol>
<p>为了更好地简化匿名函数，Go团队也提出了关于引入轻量级匿名函数语法的提案（Issue #21498），该提案一直是社区讨论的焦点，它旨在提供一种更简洁的方式来定义匿名函数，尤其是当函数类型可以从上下文推断时，从而减少样板代码，提升代码的可读性和编写效率。</p>
<h2>小结：于细微处见真章，持续打磨代码品质</h2>
<p>今天我们复盘的这六个 Go 编码“坏味道”——异步时序混乱、指针闭包陷阱、不当的错误处理、http.Client 误用、文档缺失的 API 以及冗长的函数签名——可能只是我们日常开发中遇到问题的冰山一角。</p>
<p>它们中的每一个，看似都是细节问题，但“千里之堤，溃于蚁穴”。正是这些细节的累积，最终决定了我们软件产品的质量、系统的稳定性和团队的开发效率。</p>
<p>识别并规避这些“坏味道”，需要我们：</p>
<ul>
<li><strong>深入理解 Go 语言的特性和设计哲学。</strong></li>
<li><strong>培养严谨的工程思维和对细节的关注。</strong></li>
<li><strong>重视代码审查，从他人的错误和经验中学习。</strong></li>
<li><strong>持续学习，不断反思和总结自己的编码实践。</strong></li>
</ul>
<p>希望今天的分享能给大家带来一些启发。让我们一起努力，写出更少“坑”、更高质量的 Go 代码！</p>
<hr />
<p><strong>聊一聊，也帮个忙：</strong></p>
<ul>
<li><strong>在你日常的 Go 开发或 Code Review 中，还遇到过哪些让你印象深刻的“编码坏味道”？</strong></li>
<li><strong>对于今天提到的这些问题，你是否有自己独特的解决技巧或更深刻的理解？</strong></li>
<li><strong>你认为在团队中推广良好的编码规范和实践，最有效的方法是什么？</strong></li>
</ul>
<p>欢迎在<strong>评论区</strong>留下你的经验、思考和问题。如果你觉得这篇文章对你有帮助，也请<strong>转发给你身边的 Gopher 朋友们</strong>，让我们一起在 Go 的道路上精进！</p>
<p><strong>想与我进行更深入的 Go 语言、编码实践与 AI 技术交流吗？</strong> 欢迎加入我的<strong>“Go &amp; AI 精进营”知识星球</strong>。</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-and-ai-tribe-zsxq-small-card.jpg" alt="img{512x368}" /></p>
<p>我们星球见！</p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/05/31/six-smells-in-go/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>当Gopher拥有了“Go语言女友”：一张图带你读懂Go的那些“可爱”特性</title>
		<link>https://tonybai.com/2025/05/30/gopher-girlfriend/</link>
		<comments>https://tonybai.com/2025/05/30/gopher-girlfriend/#comments</comments>
		<pubDate>Fri, 30 May 2025 11:33:32 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[.NET]]></category>
		<category><![CDATA[Array]]></category>
		<category><![CDATA[atomic]]></category>
		<category><![CDATA[Bug]]></category>
		<category><![CDATA[Build]]></category>
		<category><![CDATA[Channel]]></category>
		<category><![CDATA[Codereview]]></category>
		<category><![CDATA[Compile]]></category>
		<category><![CDATA[Database]]></category>
		<category><![CDATA[defer]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[gobuild]]></category>
		<category><![CDATA[gofmt]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomod]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[gotest]]></category>
		<category><![CDATA[govet]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[Mutex]]></category>
		<category><![CDATA[ORM]]></category>
		<category><![CDATA[recover]]></category>
		<category><![CDATA[Slice]]></category>
		<category><![CDATA[sql]]></category>
		<category><![CDATA[sqlx]]></category>
		<category><![CDATA[standardlibrary]]></category>
		<category><![CDATA[sync]]></category>
		<category><![CDATA[XML]]></category>
		<category><![CDATA[依赖管理]]></category>
		<category><![CDATA[切片]]></category>
		<category><![CDATA[垃圾回收]]></category>
		<category><![CDATA[数组]]></category>
		<category><![CDATA[标准库]]></category>
		<category><![CDATA[简单]]></category>
		<category><![CDATA[编译]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4765</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/05/30/gopher-girlfriend 大家好，我是Tony Bai。 最近，一张名为 “gopher gf” (Go 语言女友) 的 Meme 图在开发者社区悄然流传，引得无数 Gopher 会心一笑。这张图用拟人化的“女友”特质，巧妙地描绘了 Go 语言的诸多优点和社区文化梗。 那么，这位集万千宠爱于一身的“Go 语言女友”，究竟有哪些令人着迷的“可爱”特性呢？今天，就让我们化身“恋爱观察员”，逐条“解密”这张 Meme 图，看看 Go 语言是如何成为许多开发者心中“理想型”的。 “Gopher 女友”的可爱特质大揭秘！ 让我们一起来看看这位“Gopher 女友”的闪光点，以及它们在 Go 语言世界中的真实写照： 1. “cute” (可爱) Meme 解读： 她有着 Gopher 吉祥物那标志性的、憨态可掬的可爱模样。 Go语言真相： 这首先让人联想到 Go 语言那只呆萌的土拨鼠吉祥物。更深层次来说，Go 语言的语法简洁、核心概念少、没有过多的“语法糖”，使得代码看起来清爽直接，就像一个不施粉黛、自然可爱的女孩，让人一见倾心。 2. “low-maintenance” (低维护) Meme 解读： 她不“作”，好相处，不需要你花太多心思去“伺候”。 Go语言真相： 这简直是 Go 语言的真实写照！ gofmt 强制统一代码风格，彻底终结了关于代码格式的“圣战”，减少了团队协作中的摩擦。 强大的工具链 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/gopher-girlfriend-1.jpg" alt="" /></p>
<p><a href="https://tonybai.com/2025/05/30/gopher-girlfriend">本文永久链接</a> &#8211; https://tonybai.com/2025/05/30/gopher-girlfriend</p>
<p>大家好，我是Tony Bai。</p>
<p>最近，一张名为 “gopher gf” (Go 语言女友) 的 Meme 图在开发者社区悄然流传，引得无数 Gopher 会心一笑。这张图用拟人化的“女友”特质，巧妙地描绘了 Go 语言的诸多优点和社区文化梗。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/gopher-girlfriend-2.jpg" alt="" /></p>
<p>那么，这位集万千宠爱于一身的“Go 语言女友”，究竟有哪些令人着迷的“可爱”特性呢？今天，就让我们化身“恋爱观察员”，逐条“解密”这张 Meme 图，看看 Go 语言是如何成为许多开发者心中“理想型”的。</p>
<h2>“Gopher 女友”的可爱特质大揭秘！</h2>
<p>让我们一起来看看这位“Gopher 女友”的闪光点，以及它们在 Go 语言世界中的真实写照：</p>
<p><strong>1. “cute” (可爱)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 她有着 Gopher 吉祥物那标志性的、憨态可掬的可爱模样。</li>
<li><strong>Go语言真相：</strong> 这首先让人联想到 Go 语言那只呆萌的土拨鼠吉祥物。更深层次来说，Go 语言的<strong>语法简洁、核心概念少、没有过多的“语法糖”</strong>，使得代码看起来清爽直接，就像一个不施粉黛、自然可爱的女孩，让人一见倾心。</li>
</ul>
<p><strong>2. “low-maintenance” (低维护)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 她不“作”，好相处，不需要你花太多心思去“伺候”。</li>
<li><strong>Go语言真相：</strong> 这简直是 Go 语言的真实写照！
<ul>
<li><strong>gofmt 强制统一代码风格</strong>，彻底终结了关于代码格式的“圣战”，减少了团队协作中的摩擦。</li>
<li><strong>强大的工具链</strong> (go build, go test, go mod 等) 让构建、测试、依赖管理变得异常简单。</li>
<li><strong>静态编译生成单个可执行文件</strong>，部署过程干净利落，没有复杂的运行时依赖和“DLL地狱”。</li>
<li><strong>内置垃圾回收 (GC)</strong> 机制，虽然不是“银弹”，但也极大地减轻了开发者的内存管理负担。</li>
</ul>
</li>
</ul>
<p>这些特性使得Go项目的<strong>维护成本相对较低</strong>，开发者可以将更多精力聚焦在业务逻辑上。</p>
<p><strong>3. “leaves you love letters in go.mod” (在 go.mod 里给你留情书)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 多么浪漫的表达！她把对你的“心意”（依赖）都清清楚楚地写在了 go.mod 这封“情书”里。</li>
<li><strong>Go语言真相：</strong> 自从 Go Modules 成为官方推荐的依赖管理方案后，go.mod 文件就成了每个 Go 项目的“标准配置”。它清晰、明确地记录了项目的模块路径、Go 版本以及所有直接和间接依赖及其版本号。这种<strong>依赖关系的透明化和可追溯性</strong>，就像一封真挚的“情书”，让你对项目的“家底”一目了然，极大地方便了依赖管理和构建复现。</li>
</ul>
<p><strong>4. “panics but quickly recovers” (会panic但能快速恢复)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 她偶尔也会有小情绪（panic），但总能很快调整过来（recover），不至于让关系彻底崩溃。</li>
<li><strong>Go语言真相：</strong> Go 语言通过 panic 来表示严重的、通常是程序缺陷导致的运行时错误。但与其他一些语言遇到类似情况直接崩溃不同，Go 提供了 recover 机制。通过在 defer 函数中调用 recover()，我们可以捕获 panic，记录错误信息，执行一些清理操作，甚至尝试让程序从一个可控的状态恢复或优雅降级，而不是让整个服务“一蹶不振”。这种设计赋予了 Go 程序更强的<strong>韧性</strong>。</li>
</ul>
<p><strong>5. “shares her emotions by communicating” (通过沟通分享她的情感)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 她乐于沟通，而不是让你猜她的心思。</li>
<li><strong>Go 语言真相：</strong> 这无疑是在致敬 Go 并发编程的核心原语——<strong>channel</strong>！Go 语言信奉“不要通过共享内存来通信，而要通过通信来共享内存” (Don&#8217;t communicate by sharing memory, share memory by communicating) 的并发哲学。Channel 正是 goroutine 之间进行<strong>数据传递和状态同步</strong>的主要桥梁，它使得并发逻辑的表达更加清晰和安全。</li>
</ul>
<p><strong>6. “thinks mutexes are romantic” (认为互斥锁是浪漫的)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 这个有点“硬核”的浪漫！她认为互斥锁 (mutex) 这种保护共享资源、确保“二人世界”不被打扰的机制，是充满“安全感”的浪漫。</li>
<li><strong>Go语言真相：</strong> sync.Mutex 是 Go 中最常用的并发同步原语之一，用于在并发访问共享资源时避免竞态条件。虽然 Go 推崇通过 channel 进行通信，但在某些场景下，使用互斥锁保护共享数据仍然是必要且高效的。这个梗幽默地反映了 Gopher 对<strong>并发安全</strong>的极致追求和对底层同步机制的熟悉。</li>
</ul>
<p><strong>7. “doesn&#8217;t cry when invalid memory address or nil pointer dereference” (当无效内存地址或空指针解引用时不会哭)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 遇到问题，她不“哭哭啼啼”（难以追踪的错误），而是直接“告诉你”（panic）。</li>
<li><strong>Go 语言真相：</strong> 当 Go 程序遇到空指针解引用、数组越界等严重的运行时错误时，它会立即 panic，并打印出清晰的错误信息和堆栈跟踪。这与某些语言可能产生的段错误 (segmentation fault) 或未定义行为，导致问题难以定位和复现相比，无疑是一种更“直接”和有助于<strong>快速暴露和定位 Bug</strong> 的行为。</li>
</ul>
<p><strong>8. “thinks ORM is astrology for devs” (认为 ORM 对开发者来说是占星术)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 她对那些过度封装、隐藏细节、让人感觉像“玄学”的 ORM 框架持保留态度。</li>
<li><strong>Go语言真相：</strong> 这是 Go 社区一个广为人知的“文化梗”。许多 Gopher 更倾向于使用标准库的 database/sql 包配合轻量级的 SQL 构建库（如 sqlx等），或者直接编写原生 SQL。这背后是对<strong>数据层掌控力、性能透明度以及避免不必要的“魔法”和复杂抽象</strong>的追求。他们认为，SQL 本身就是一种强大的 DSL，过度封装反而可能引入新的问题。</li>
</ul>
<p><strong>9. “cooks you meals from scratch” (从零开始为你做饭)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 她心灵手巧，能用最新鲜的食材（标准库）为你烹制美味佳肴，而不是依赖各种半成品（重型框架或过多第三方库）。</li>
<li><strong>Go 语言真相：</strong> Go 拥有一个异常<strong>强大且设计精良的标准库</strong>。无论是网络编程 (net/http, net)、JSON/XML 处理 (encoding/json, encoding/xml)、文件操作 (os, io)、加密解密 (crypto/*)，还是并发原语 (sync, sync/atomic)，标准库都提供了高质量的实现。这使得 Go 开发者在很多场景下可以“自给自足”，减少对外部依赖，构建出更轻量、更可控的系统。</li>
</ul>
<p><strong>10. “reviews your code every night” (每晚都审查你的代码)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 她非常关心你的代码质量，时刻帮你把关。</li>
<li><strong>Go 语言真相：</strong> 这可以从几个层面理解：
<ul>
<li><strong>静态类型检查：</strong> Go 是一门静态类型语言，编译器在编译阶段就能帮你发现大量的类型错误和低级 Bug，就像一位尽职的“审查员”。</li>
<li><strong>go vet 等工具：</strong> Go 工具链内置了 go vet 等静态分析工具，可以帮助检查代码中潜在的错误或可疑构造。</li>
<li><strong>社区文化：</strong> Go 社区非常重视 Code Review 的实践，鼓励通过同行评审来提升代码质量。</li>
<li><strong>语言设计本身：</strong> Go 语言的简洁性和一些强制性规范（如未使用变量的编译错误），也在某种程度上“迫使”开发者写出更清晰、更规范的代码，更易于审查。</li>
</ul>
</li>
</ul>
<p><strong>11. “compiles fast” (编译快)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 她做事麻利，从不拖沓。</li>
<li><strong>Go 语言真相：</strong> 这绝对是 Go 语言最令人称道的特性之一！Go 的<strong>编译速度极快</strong>，即使是中大型项目，编译过程通常也只需要十几秒钟。这极大地提升了开发者的工作效率和迭代速度，减少了漫长的等待时间，让开发体验如丝般顺滑。快速编译使得“编码-编译-测试”的循环非常高效。</li>
</ul>
<h2>小结：“Go语言女友”，为何如此理想？</h2>
<p>看完了对 “gopher gf” Meme 图的逐条解读，我们不难发现，这位“理想女友”的每一个“可爱特质”，都精准地映射了 Go 语言在现实世界中的核心优势：</p>
<ul>
<li><strong>简洁易学 (cute)</strong></li>
<li><strong>维护成本低 (low-maintenance)</strong></li>
<li><strong>依赖管理清晰 (leaves you love letters in go.mod)</strong></li>
<li><strong>具备韧性的错误处理 (panics but quickly recovers)</strong></li>
<li><strong>推崇通信共享内存的并发模型 (shares her emotions by communicating)</strong></li>
<li><strong>重视并发安全 (thinks mutexes are romantic)</strong></li>
<li><strong>明确的运行时错误反馈 (doesn&#8217;t cry when invalid memory address or nil pointer dereference)</strong></li>
<li><strong>崇尚直接、避免过度抽象 (thinks ORM is astrology for devs)</strong></li>
<li><strong>强大的标准库 (cooks you meals from scratch)</strong></li>
<li><strong>利于代码质量保障的特性与文化 (reviews your code every night)</strong></li>
<li><strong>闪电般的编译速度 (compiles fast)</strong></li>
</ul>
<p>正是这些特性，使得 Go 语言在云原生、微服务、分布式系统、网络编程、命令行工具等众多领域大放异彩，成为越来越多开发者和企业的首选。它就像一位可靠、高效、易于相处且不乏生活情趣的“伴侣”，帮助我们更轻松、更愉快地构建出色的软件系统。</p>
<p>当然，Meme 终归是 Meme，它用一种轻松幽默的方式，概括了 Go 语言的诸多美好。现实中的 Go 语言也并非完美无缺，它依然在不断发展和进化。但不可否认的是，这些“可爱”的特质，正是 Go 语言独特魅力和强大生命力的源泉。</p>
<p>那么，你心中的“Go 语言女友”又是怎样的呢？或者，你最欣赏 Go 语言的哪个“可爱”特质？</p>
<p>欢迎在<strong>评论区</strong>分享你的看法和脑洞！如果你觉得这篇文章有趣且让你对 Go 语言有了更深的（或者说更“萌”的）理解，也请<strong>转发给你身边的 Gopher 朋友们</strong>，一起感受这份来自代码世界的“浪漫”与“可爱”！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<blockquote>
<p>注：本文部分内容经过AI润色和优化，以提升读者阅读体验。</p>
</blockquote>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/05/30/gopher-girlfriend/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go 1.22新特性前瞻</title>
		<link>https://tonybai.com/2023/12/25/go-1-22-foresight/</link>
		<comments>https://tonybai.com/2023/12/25/go-1-22-foresight/#comments</comments>
		<pubDate>Mon, 25 Dec 2023 13:58:12 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[append]]></category>
		<category><![CDATA[Array]]></category>
		<category><![CDATA[bisect]]></category>
		<category><![CDATA[Channel]]></category>
		<category><![CDATA[Compiler]]></category>
		<category><![CDATA[dep]]></category>
		<category><![CDATA[devirtualize]]></category>
		<category><![CDATA[for]]></category>
		<category><![CDATA[for-range]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[go1.21]]></category>
		<category><![CDATA[go1.22]]></category>
		<category><![CDATA[GODEBUG]]></category>
		<category><![CDATA[GOEXPERIMENT]]></category>
		<category><![CDATA[goimports]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[Google]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[gopls]]></category>
		<category><![CDATA[gorilla]]></category>
		<category><![CDATA[govet]]></category>
		<category><![CDATA[Go语言第一课]]></category>
		<category><![CDATA[Go语言精进之路]]></category>
		<category><![CDATA[HandleFunc]]></category>
		<category><![CDATA[host]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[httprouter]]></category>
		<category><![CDATA[Inline]]></category>
		<category><![CDATA[iteration]]></category>
		<category><![CDATA[Iterator]]></category>
		<category><![CDATA[loop]]></category>
		<category><![CDATA[loopvar]]></category>
		<category><![CDATA[map]]></category>
		<category><![CDATA[math]]></category>
		<category><![CDATA[mux]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[PGO]]></category>
		<category><![CDATA[playground]]></category>
		<category><![CDATA[rand]]></category>
		<category><![CDATA[runtime]]></category>
		<category><![CDATA[RussCox]]></category>
		<category><![CDATA[ServeMux]]></category>
		<category><![CDATA[Slice]]></category>
		<category><![CDATA[slog]]></category>
		<category><![CDATA[stdlib]]></category>
		<category><![CDATA[sync]]></category>
		<category><![CDATA[syscall]]></category>
		<category><![CDATA[TCPConn]]></category>
		<category><![CDATA[toolchain]]></category>
		<category><![CDATA[Trace]]></category>
		<category><![CDATA[Tracer]]></category>
		<category><![CDATA[unique]]></category>
		<category><![CDATA[UnixConn]]></category>
		<category><![CDATA[vendor]]></category>
		<category><![CDATA[version]]></category>
		<category><![CDATA[waitgroup]]></category>
		<category><![CDATA[zerocopy]]></category>
		<category><![CDATA[主机]]></category>
		<category><![CDATA[优化]]></category>
		<category><![CDATA[兼容性]]></category>
		<category><![CDATA[内联]]></category>
		<category><![CDATA[切片]]></category>
		<category><![CDATA[包]]></category>
		<category><![CDATA[工具链]]></category>
		<category><![CDATA[循环]]></category>
		<category><![CDATA[数组]]></category>
		<category><![CDATA[标准库]]></category>
		<category><![CDATA[索引]]></category>
		<category><![CDATA[编译器]]></category>
		<category><![CDATA[语法糖]]></category>
		<category><![CDATA[运行时]]></category>
		<category><![CDATA[迭代]]></category>
		<category><![CDATA[迭代器]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4087</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2023/12/25/go-1-22-foresight 美国时间2023年12月20日，Go官方宣布Go 1.22rc1发布，开启了为期2个多月的、常规的公测之旅，Go 1.22预计将于2024.2月份正式发布！ 除了在官网下载Go 1.22rc1版本进行新特性体验之外，我们还可以通过在线的Go Playground选择“Go dev branch”来体验(相比下载安装，在线版本体验会有一些局限)： 注：关于Go的多种安装方法，《Go语言第一课》专栏有系统全面的讲解，欢迎订阅阅读。 本文将和大家一起看看Go 1.22都会带来哪些新特性。不过由于目前为时尚早，下面列出的有些变化最终不一定能进入到Go 1.22的最终版本中，所以切记一切变更点要以最终Go 1.22版本发布时为准。 1. 语言变化 Go 1.22的语言特性变化主要是围绕for loop的。 1.1 loopvar试验特性转正 在Go 1.21版本中，作为试验特性loopvar在Go 1.22中正式转正。如果你还不知道这个特性是啥，我们来看一下下面这个最能说明问题的示例： // go1.22-foresight/lang/for-range/for_range.go package main import ( "fmt" "sync" ) func main() { sl := []int{11, 12, 13, 14, 15} var wg sync.WaitGroup for i, v := range sl { [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/go-1-22-foresight-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2023/12/25/go-1-22-foresight">本文永久链接</a> &#8211; https://tonybai.com/2023/12/25/go-1-22-foresight</p>
<p>美国时间2023年12月20日，<a href="https://groups.google.com/g/golang-announce/c/FIUY9kd7fc0">Go官方宣布Go 1.22rc1发布</a>，开启了为期2个多月的、常规的公测之旅，Go 1.22预计将于2024.2月份正式发布！</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-1-22-foresight-2.png" alt="" /></p>
<p>除了<a href="https://go.dev/dl/#go1.22rc1">在官网下载Go 1.22rc1版本</a>进行新特性体验之外，我们还可以通过<a href="https://go.dev/play/">在线的Go Playground</a>选择“Go dev branch”来体验(相比下载安装，在线版本体验会有一些局限)：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-1-22-foresight-3.png" alt="" /></p>
<blockquote>
<p>注：关于Go的多种安装方法，<a href="http://gk.link/a/10AVZ">《Go语言第一课》</a>专栏有系统全面的讲解，欢迎订阅阅读。</p>
</blockquote>
<p>本文将和大家一起看看Go 1.22都会带来哪些新特性。不过由于目前为时尚早，下面列出的有些变化最终不一定能进入到Go 1.22的最终版本中，所以切记一切变更点要以最终Go 1.22版本发布时为准。</p>
<h2>1. 语言变化</h2>
<p>Go 1.22的语言特性变化主要是<strong>围绕for loop的</strong>。</p>
<h3>1.1 loopvar试验特性转正</h3>
<p>在<a href="https://tonybai.com/2023/08/20/some-changes-in-go-1-21">Go 1.21版本</a>中，作为试验特性loopvar在Go 1.22中正式转正。如果你还不知道这个特性是啥，我们来看一下下面这个最能说明问题的示例：</p>
<pre><code>// go1.22-foresight/lang/for-range/for_range.go

package main

import (
    "fmt"
    "sync"
)

func main() {
    sl := []int{11, 12, 13, 14, 15}
    var wg sync.WaitGroup
    for i, v := range sl {
        wg.Add(1)
        go func() {
            fmt.Printf("%d : %d\n", i, v)
            wg.Done()
        }()
    }
    wg.Wait()
}
</code></pre>
<p>我们分别用Go 1.22rc1和Go 1.21.0来运行上面这段代码：</p>
<pre><code>// 使用go 1.22rc1的运行结果：

$go run for_range.go
4 : 15
1 : 12
0 : 11
3 : 14
2 : 13

// 使用go 1.21.0的运行结果：
$go run for_range.go
4 : 15
4 : 15
4 : 15
4 : 15
4 : 15
</code></pre>
<p>之所以存在差异，是因为Go 1.22版本开始，for range语句中声明的循环变量（比如这里的i和v）不再是整个loop一份(loop var per loop)，而是每次iteration都会有自己的变量(loop var per-iteration)，这样在Go 1.22中，for range中的goroutine启动的<a href="https://tonybai.com/2021/08/09/when-variables-captured-by-closures-are-recycled-in-go/">闭包函数</a>中捕获的变量是loop var per-iteration，这样才会输出5个不同的索引值和对应的切片值。</p>
<blockquote>
<p>注：关于Go 1.22版本之前的for range的坑，<a href="http://gk.link/a/10AVZ">《Go语言第一课》</a>专栏有图文并茂的原理讲解，欢迎订阅阅读。</p>
</blockquote>
<p>那传统的3-clause的for loop呢？其中的循环变量的语义是否也发生变化了呢？我们看下面示例：</p>
<pre><code>// go1.22-foresight/lang/for-range/classic_for_loop.go

package main

import (
    "fmt"
    "sync"
)

func main() {
    sl := []int{11, 12, 13, 14, 15}
    var wg sync.WaitGroup
    for i := 0; i &lt; len(sl); i++ {
        wg.Add(1)
        go func() {
            v := sl[i]
            fmt.Printf("%d : %d\n", i, v)
            wg.Done()
        }()
    }
    wg.Wait()
}
</code></pre>
<p>我们依然分别用Go 1.22rc1和Go 1.21.0版本运行这段代码，得到的结果如下：</p>
<pre><code>// 使用go 1.22rc1的运行结果：

$go run classic_for_loop.go
0 : 11
4 : 15
2 : 13
3 : 14
1 : 12

// 使用go 1.21.0的运行结果：

$go run classic_for_loop.go
panic: runtime error: index out of range [5] with length 5

goroutine 20 [running]:
main.main.func1()
    /Users/tonybai/test/go/go1.22-foresight/lang/for-range/classic_for_loop.go:14 +0xc9
created by main.main in goroutine 1
    /Users/tonybai/test/go/go1.22-foresight/lang/for-range/classic_for_loop.go:13 +0x7f
panic: runtime error: index out of range [5] with length 5

goroutine 19 [running]:
main.main.func1()
    /Users/tonybai/test/go/go1.22-foresight/lang/for-range/classic_for_loop.go:14 +0xc9
created by main.main in goroutine 1
    /Users/tonybai/test/go/go1.22-foresight/lang/for-range/classic_for_loop.go:13 +0x7f
exit status 2
</code></pre>
<p>从输出结果来看，3-clause的for语句中声明的循环变量也变成了loop var per-iteration了。</p>
<p>在Go 1.22之前，go vet工具在遇到像上面代码那样在闭包中引用循环变量的情况时会给出警告，但由于Go 1.22的这个语义修正，go vet对于Go 1.22及以后版本(根据go.mod中的指示)的类似Go代码将不再报错。</p>
<p>不过就像Russ Cox在<a href="https://github.com/golang/go/issues/60078">spec: less error-prone loop variable scoping</a>这一issue中提及那样，该特性落地可能会带来不兼容问题，即对存量代码行为的破坏性改变。为此Go团队提供了一个<a href="https://pkg.go.dev/golang.org/x/tools/cmd/bisect">名为bisect的工具</a>，该工具可以检测出存量代码在for loop语义发生变更后是否会导致问题。不过该工具似乎只能与go test一起使用，也就是说你只能对那些被<a href="https://tonybai.com/2023/07/16/the-guide-of-go-testing-with-testify-package/">Go测试</a>覆盖到的for loop进行检测。</p>
<p>目前<a href="https://github.com/golang/go/issues/60078">spec: less error-prone loop variable scoping</a>这一issue还处于open状态，也没有放入Go 1.22 milestone中，不知道后续是否还会存在变数！</p>
<h3>1.2 range支持整型表达式</h3>
<p>在Go 1.22版本中，for range后面的range表达式除了支持传统的像数组、切片、map、channel等表达式外，<strong>还支持放置整型表达式</strong>，比如下面这个例子：</p>
<pre><code>// lang/range-expr-support-integer/main.go

func main() {
    n := 5
    for i := range n {
        fmt.Println(i)
    }
}
</code></pre>
<p>我们知道：for range会在执行伊始对range表达式做一次求值，这里对n求值结果为5。按照新增的for range后接整型表达式的语义，对于整数值n，for range每次迭代值会从0到n-1按递增顺序进行。上面代码中的for range会从0迭代到4(5-1)，我们执行一下上述代码就可以印证这一点：</p>
<pre><code>$go run main.go
0
1
2
3
4
</code></pre>
<p>如果n &lt;= 0，则循环不运行任何迭代。</p>
<p>这个新语法特性，可以理解为是一种“语法糖”，是下面等价代码的“语法糖”：</p>
<pre><code>for i := 0; i &lt; 5; i++ {
    fmt.Println(i)
}
</code></pre>
<p>不过，迭代总是从0开始，似乎限制了该语法糖的使用范围。</p>
<h3>1.3 试验特性：range-over-function iterators</h3>
<p>在for range支持整型表达式的时候，<a href="https://github.com/golang/go/issues/61405">Go团队也考虑了增加函数迭代器(iterator)</a>，不过前者语义清晰，实现简单。后者展现形式、语义和实现都非常复杂，于是在Go 1.22中，函数迭代器以试验特性提供，通过GOEXPERIMENT=rangefunc可以体验该功能特性。</p>
<p>在没有函数迭代器之前，我们实现一个通用的反向迭代切片的函数可能是像这样：</p>
<pre><code>// lang/range-over-function-iterator/backward_iterate_slice_old.go

func Backward(s []E) func(func(int, E) bool) {
    return func(yield func(int, E) bool) {
        for i := len(s)-1; i &gt;= 0; i-- {
            if !yield(i, s[i]) {
                return
            }
        }
        return
    }
}
</code></pre>
<p>下面是在Go 1.21.0版本中使用上面Backward函数的方式：</p>
<pre><code>// lang/range-over-function-iterator/backward_iterate_slice_old.go

func main() {
    sl := []string{"hello", "world", "golang"}
    Backward(sl)(func(i int, s string) bool {
        fmt.Printf("%d : %s\n", i, s)
        return true
    })
}
</code></pre>
<p>我们用Go 1.21.0运行一下上述示例：</p>
<pre><code>$go run backward_iterate_slice_old.go
2 : golang
1 : world
0 : hello
</code></pre>
<p>在以前版本中，这种对切片、数组或map中进行元素迭代的情况在实际开发中非常常见，也比较模式化，但基于目前语法，使用起来非常不便。于是Go团队提出将它们<a href="https://github.com/golang/go/issues/61405">与for range结合在一起的提案</a>。有了range-over-function iterator机制后，我们就可以像下面这样使用Backward泛型函数了：</p>
<pre><code>// lang/range-over-function-iterator/backward_iterate_slice_new.go

func main() {
    sl := []string{"hello", "world", "golang"}
    for i, s := range Backward(sl) {
        fmt.Printf("%d : %s\n", i, s)
    }
}
</code></pre>
<p>相比于上面的老版本代码，这也的代码更简洁清晰了，使用Go 1.22rc1的运行结果也与老版本别无二致：</p>
<pre><code>$GOEXPERIMENT=rangefunc  go run backward_iterate_slice_new.go
2 : golang
1 : world
0 : hello
</code></pre>
<p>但代价就是要理解什么样原型的函数才能与for range一起使用实现函数迭代，这的确有些复杂，本文就不展开说了，有兴趣的童鞋可以先看看有关<a href="https://go.dev/wiki/RangefuncExperiment">range-over-function iterator的wiki</a>先行了解一下。</p>
<h2>2. 编译器、运行时与工具链</h2>
<h3>2.1 继续增强<a href="https://github.com/golang/go/issues/61577">PGO优化</a></h3>
<p>自<a href="https://tonybai.com/2023/02/08/some-changes-in-go-1-20">Go 1.20版本引入PGO</a>(profile-guided optimization)后，PGO这种优化技术带来的优化效果就得到了持续的提升：Go 1.20实测性能提升仅为1.05%；<a href="https://tonybai.com/2023/08/20/some-changes-in-go-1-21">Go 1.21版本发布</a>时，官方的数据是2%~7%，而Go 1.21编译器自身在PGO优化过后编译速度提升约6%。</p>
<p>在Go 1.22中，官方给出的数字则是2%~14%，这14%的提升想必是来自Google内部的某个实际案例。</p>
<h3>2.2  inline和devirtualize</h3>
<p>在Go 1.22中，Go编译器可以更灵活的运用devirtualize和inline对代码进行优化了。</p>
<p>在面向对象的编程中，虚拟函数是一种在运行时动态确定调用的函数。当调用虚拟函数时，编译器通常会为其生成一段额外的代码，用于在运行时确定要调用的具体函数。这种动态调度的机制使得程序可以根据实际对象类型来执行相应的函数，但也带来了一定的性能开销。通过devirtualize优化技术，编译器会尝试在编译时确定调用的具体函数，而不是在运行时进行动态调度。这样可以避免运行时的开销，并<strong>允许编译器进行更多的优化</strong>。</p>
<p>对应到Go来说，就是在编译阶段<strong>将使用接口进行的方法调用转换为通过接口的实际类型的实例直接调用该方法</strong>。</p>
<blockquote>
<p>注：我的《<a href="https://item.jd.com/13694000.html">Go语言精进之路</a>》一书中有对通过接口调用方法的原理的详尽说明，欢迎阅读。</p>
</blockquote>
<p>关于内联优化，今年Austin Clements发起了<a href="https://github.com/golang/go/issues/61502">inline大修项目</a>，对Go编译器中的内联优化过程进行全面调整，目标是在Go 1.22中拥有更有效的、具有启发能力的内联，为后续内联的进一步增强奠定基础。该大修的成果目前以GOEXPERIMENT=newinliner试验特性的形式在Go 1.22中提供。</p>
<h3>2.3 运行时</h3>
<p>运行时的变化主要还是来自<a href="https://tonybai.com/2023/06/13/understand-go-gc-overhead-behind-the-convenience/">GC</a>。</p>
<p>Go 1.22中，运行时会将基于类型的垃圾回收的元数据放在每个堆对象附近，从而可以将Go程序的CPU性能提高1-3%。同时，通过减少重复的元数据的优化，内存开销也将降低约1%。不确定减少重复元数据(metadata)这一优化是否<a href="https://github.com/golang/go/issues/62483#issuecomment-1800913220">来自对unique包的讨论</a>。</p>
<h3>2.4 工具链</h3>
<p>在Go工具链改善方面，首当其冲的要数go module相关工具了。</p>
<p>在Go 1.22中，go work增加了一个与go mod一致的特性：支持vendor。通过go work vendor，可以将workspace中的依赖放到vendor目录下，同时在构建时，如果module root下有vendor目录，那么默认的构建是go build -mod=vendor，即基于vendor的构建。</p>
<p>go mod init在Go 1.22中将不再考虑GOPATH时代的包依赖工具的配置文件了，比如Gopkg.lock。在Go 1.22版本之前，如果go module之前使用的是类似<a href="https://tonybai.com/2017/06/08/first-glimpse-of-dep/">dep这样的工具来管理包依赖</a>，go mod init会尝试读取dep配置文件来生成go.mod。</p>
<p>go vet工具取消了对loop变量引用的警告，增加了对空append的行为的警告(比如：slice = append(slice))、增加了deferring time.Since的警告以及在log/slog包的方法调用时key-value pair不匹配的警告。</p>
<h2>3. 标准库</h2>
<p>最后，我们来看看标准库的变化。每次Go发布新版本，标准库都是占更新的大头儿，这里无法将所有变更点一一讲解，仅说说几个重要的变更点。</p>
<h3>3.1 增强http.ServerMux表达能力</h3>
<p>Go内置电池，从诞生伊始就内置了强大的http库，不过长期以来http原生的ServeMux表达能力比较单一，不支持通配符等，这也是Go社区长期以来一直使用像<a href="https://github.com/gorilla/mux/">gorilla/mux</a>、<a href="https://github.com/julienschmidt/httprouter">httprouter</a>等第三方路由库的原因。</p>
<p>今年log/slog的作者Jonathan Amsterdam又创建了新的提案：<a href="https://github.com/golang/go/issues/61410">net/http: enhanced ServeMux routing</a>，提高http.ServeMux的表达能力。在新提案中，<a href="https://pkg.go.dev/net/http@go1.22rc1#ServeMux">新的ServeMux将支持如下路由策略</a>(来自http.ServeMux的官方文档)：</p>
<ul>
<li>“/index.html”路由将匹配任何主机和方法的路径”/index.html”；</li>
<li>“GET /static/”将匹配路径以”/static/”开头的GET请求；</li>
<li>“example.com/”可以与任何指向主机为”example.com”的请求匹配；</li>
<li>“example.com/{$}”会匹配主机为”example.com”、路径为”/”的请求，即”example.com/”；</li>
<li>“/b/{bucket}/o/{objectname&#8230;}”匹配第一段为”b”、第三段为”o”的路径。名称”bucket”表示第二段，”objectname”表示路径的其余部分。</li>
</ul>
<p>下面就是基于上面的规则编写的示例代码：</p>
<pre><code>// lib/servemux/main.go

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/index.html", func(w http.ResponseWriter, req *http.Request) {
        fmt.Fprintln(w, `match /index.html`)
    })
    mux.HandleFunc("GET /static/", func(w http.ResponseWriter, req *http.Request) {
        fmt.Fprintln(w, `match "GET /static/"`)
    })
    mux.HandleFunc("example.com/", func(w http.ResponseWriter, req *http.Request) {
        fmt.Fprintln(w, `match "example.com/"`)
    })
    mux.HandleFunc("example.com/{$}", func(w http.ResponseWriter, req *http.Request) {
        fmt.Fprintln(w, `match "example.com/{$}"`)
    })
    mux.HandleFunc("/b/{bucket}/o/{objectname...}", func(w http.ResponseWriter, req *http.Request) {
        bucket := req.PathValue("bucket")
        objectname := req.PathValue("objectname")
        fmt.Fprintln(w, `match /b/{bucket}/o/{objectname...}`+":"+"bucket="+bucket+",objectname="+objectname)
    })

    http.ListenAndServe(":8080", mux)
}
</code></pre>
<p>我们使用curl对上述示例进行一个测试(前提是在/etc/hosts中设置example.com为127.0.0.1)：</p>
<pre><code>$curl localhost:8080/index.html
match /index.html

$curl example.com:8080/static/abc
match "example.com/"

$curl localhost:8080/static/abc
match "GET /static/"

$curl example.com:8080/
match "example.com/{$}"

$curl example.com:8080/b/mybucket/o/myobject/tonybai
match "example.com/"

$curl localhost:8080/b/mybucket/o/myobject/tonybai
match /b/{bucket}/o/{objectname...}:bucket=mybucket,objectname=myobject/tonybai
</code></pre>
<p>从测试情况来看，不同路由设置之间存在交集，这就需要路由匹配优先级规则。新版Go ServeMux规定：如果一个请求有两个或两个以上的模式匹配，则更具体(specific)的模式优先。如果P1符合P2请求的严格子集，也就是说，如果P2符合P1及更多的所有请求，那么P1就比P2更具体。</p>
<p>举个例子：”/images/thumbnails/”比”/images/”更具体，因此两者都可以注册。前者匹配以”/images/thumbnails/”开头的路径，后者则匹配”/images/”子树中的任何其他路径。</p>
<p>如果两者都不更具体，那么模式就会发生冲突。为了向后兼容，这一规则有一个例外：如果两个模式发生冲突，而其中一个模式有主机(host)，另一个没有，那么有主机的模式优先(比如上面测试中的第二次curl执行)。如果通过ServeMux.Handle或ServeMux.HandleFunc设置的模式与另一个已注册的模式发生冲突，这些函数就会panic。</p>
<p>增强后的ServeMux可能会影响向后兼容性，使用GODEBUG=httpmuxgo121=1可以保留原先的ServeMux行为。</p>
<h3>3.2 增加math/rand/v2包</h3>
<p>在日常开发中，我们多会在生成随机数的场景下使用math/rand包，其他时候使用的较少。但Go 1.22中新增了math/rand/v2包，我之所以将这个列为Go 1.22版本标准库的一次重要变化，是因为这是标准库第一次为某个包建立v2版本包，<a href="https://github.com/golang/go/issues/61716">按照Russ Cox的说法</a>，这次v2包的创建，为标准库中的其他可能的v2包树立了榜样。创建math/rand/v2可以使Go团队能够在一个相对不常用且风险较低的包中解决工具问题（如gopls、goimports等对v2包的支持），然后再转向更常用、风险更高的包，如sync/v2或encoding/json/v2等。</p>
<p><a href="https://github.com/golang/go/issues/61716">新增rand/v2包的直接原因</a>是清理math/rand并修复其中许多悬而未决的问题，特别是使用过时的生成器、慢速算法以及与crypto/rand冲突的问题，这里就不针对v2包举具体的示例了，对该包感兴趣的同学可以自行阅读该包的在线文档，并探索如何使用v2包。</p>
<p>同时，该提案也为标准库中的v2包的创建建立了一种模式，即v2包是原始包的子目录，并且以原始包的API为起点，每个偏离点都要有明确的理由。</p>
<p>想当初，go module刚落地到Go中时，Go module支持两种识别major的两种方式，一种是通过branch或tag号来识别，另外一种就是利用vN目录来定义新包。当时还不是很理解为什么要有vN目录这种方式，现在从math/rand/v2包的增加来看，足以体现出当初module设计时的前瞻性考量了。</p>
<h3>3.3 <a href="https://github.com/golang/go/issues/60773">大修Go execution tracer</a></h3>
<p>Go Execution Tracer是解决Go应用性能方面“疑难杂症”的杀手锏级工具，它可以提供Go程序在一段时间内发生的情况的即时视图。这些信息对于了解程序随时间推移的行为非常宝贵，可辅助开发人员对应用进行性能改进。我曾在《<a href="https://tonybai.com/2021/06/28/understand-go-execution-tracer-by-example">通过实例理解Go Execution Tracer</a>》中对其做过系统的说明。</p>
<p>不过当前版本的Go Execution Tracer在原理和使用方面还存在诸多问题，Google的Michael Knyszek在年初发起了<a href="https://github.com/golang/go/issues/60773">Execution tracer overhaul的提案</a>，旨在对Go Execution Tracer进行改进，使Go Execution Tracer可扩展到大型Go部署的Go执行跟踪。具体目标如下：</p>
<ul>
<li>使跟踪解析所需的内存占用量仅为当前的一小部分。</li>
<li>支持可流式传输的跟踪，以便在无需存储的情况下进行分析。</li>
<li>实现部分自描述的跟踪，以减少跟踪消费者的升级负担。</li>
<li>修复长期存在的错误，并提供一条清理实现的路径。</li>
</ul>
<p>在近一年的时间里，Knyszek与Felix Geisendorfer、Nick Ripley、Michael Pratt等一起实现了该提案的目标。</p>
<p>鉴于篇幅，这里就不对新版Tracer的使用做展开说明，有兴趣的童鞋可结合《<a href="https://tonybai.com/2021/06/28/understand-go-execution-tracer-by-example">通过实例理解Go Execution Tracer</a>》中的使用方法自行体验新版Tracer。</p>
<blockquote>
<p>注：<a href="https://go.googlesource.com/proposal/+/ac09a140c3d26f8bb62cbad8969c8b154f93ead6/design/60773-execution-tracer-overhaul.md">新版Tracer的设计文档</a> &#8211; https://go.googlesource.com/proposal/+/ac09a140c3d26f8bb62cbad8969c8b154f93ead6/design/60773-execution-tracer-overhaul.md</p>
</blockquote>
<h3>3.4 其他</h3>
<ul>
<li>“出尔反尔” &#8211; <a href="https://github.com/golang/go/issues/60797">syscall包：取消弃用(undeprecate)</a></li>
</ul>
<p>自<a href="https://tonybai.com/2014/11/04/some-changes-in-go-1-4/">Go 1.4版本</a>以来，syscall包新特性就已经被冻结，并在<a href="https://tonybai.com/2018/11/19/some-changes-in-go-1-11/">Go 1.11版本</a>中被标记为不推荐使用(deprecate)。Go团队推荐gopher使用golang.org/x/sys/unix或golang.org/x/sys/windows。syscall包的大多数功能都能被golang.org/x/sys包替代，除了下面这几个：</p>
<pre><code>syscall.SysProcAttr（类型os/exec.Cmd.SysProcAttr)
syscall.Signal（参考文献os.Signal)
syscall.WaitStatus（参考文献os.(*ProcessState).Sys)
syscall.Stat_t
... ...
</code></pre>
<p>由于syscall包已经弃用，IDE等工具在开发人员使用上述内容时总是得到警告！这引发了众多开发人员的抱怨。为此，在Go 1.22版本中，syscall取消了弃用状态，但其功能特性依旧保持冻结，不再添加新特性。</p>
<ul>
<li>TCPConn to UnixConn：支持zerocopy </li>
</ul>
<p><a href="https://tonybai.com/2021/07/31/io-multiplexing-model-tcp-stream-protocol-parsing-practice-in-go/">gnet</a>作者Andy Pan的提案：<a href="https://github.com/golang/go/issues/58808">TCPConn to UnixConn：支持zerocopy</a>在Go 1.22落地，具体内容可以看一下<a href="https://github.com/golang/go/issues/58808">原始提案issue</a>。</p>
<ul>
<li>新增go/version包</li>
</ul>
<p>在Go 1.21版本发布后，Go团队对Go语言的版本规则做了调整，并明确了<a href="https://tonybai.com/2023/09/10/understand-go-forward-compatibility-and-toolchain-rule/">Go语言的向前兼容性和toolchain规则</a>，Go 1.22中增加go/version包实现了按照上述版本规则的Go version判断，这个包既用于go工具链，也可以用于Gopher自行开发的工具中。</p>
<h2>4. 小结</h2>
<p>Go 1.22版本具有至少两点重要的里程碑意义：</p>
<ul>
<li>通过对loopvar语义的修正，开启了Go已有“语法坑”的fix之路</li>
<li>通过math/rand/v2包树立了Go标准库建立vN版本的模式</li>
</ul>
<p>“语法坑”fix是否能得到社区正向反馈还是一个未知数，其导致的兼容性问题势必会成为Go社区在升级到Go 1.22版本的重要考虑因素，即便决定升级到Go 1.22，严格的代码审查和测试也是必不可少的。</p>
<p>最后，感谢Go团队以及所有Go 1.22贡献者做出的伟大工作！</p>
<p>文本涉及的源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/go1.22-foresight">这里</a>下载。</p>
<h2>5. 参考资料</h2>
<p>-<a href="https://github.com/golang/go/milestone/298">Go 1.22 Milestone</a> &#8211; https://github.com/golang/go/milestone/298</p>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2023, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2023/12/25/go-1-22-foresight/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Go语言开发者的Apache Arrow使用指南：读写Parquet文件</title>
		<link>https://tonybai.com/2023/07/31/a-guide-of-using-apache-arrow-for-gopher-part6/</link>
		<comments>https://tonybai.com/2023/07/31/a-guide-of-using-apache-arrow-for-gopher-part6/#comments</comments>
		<pubDate>Mon, 31 Jul 2023 13:07:28 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Apache]]></category>
		<category><![CDATA[Array]]></category>
		<category><![CDATA[Arrow]]></category>
		<category><![CDATA[ArrowColumnarFormat]]></category>
		<category><![CDATA[bitmap]]></category>
		<category><![CDATA[Brotli]]></category>
		<category><![CDATA[buffer]]></category>
		<category><![CDATA[Builder模式]]></category>
		<category><![CDATA[ChunkedArray]]></category>
		<category><![CDATA[compute]]></category>
		<category><![CDATA[cookbook]]></category>
		<category><![CDATA[Cpp]]></category>
		<category><![CDATA[csv]]></category>
		<category><![CDATA[Datum]]></category>
		<category><![CDATA[dictionary]]></category>
		<category><![CDATA[dremio]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[greptimedb]]></category>
		<category><![CDATA[gzip]]></category>
		<category><![CDATA[InfluxDB]]></category>
		<category><![CDATA[influxdb-iox]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[Kaggle]]></category>
		<category><![CDATA[layout]]></category>
		<category><![CDATA[list]]></category>
		<category><![CDATA[metadata]]></category>
		<category><![CDATA[offset]]></category>
		<category><![CDATA[parquet]]></category>
		<category><![CDATA[primitive]]></category>
		<category><![CDATA[Record]]></category>
		<category><![CDATA[RecordBatch]]></category>
		<category><![CDATA[RPC]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[scalar]]></category>
		<category><![CDATA[Schema]]></category>
		<category><![CDATA[semver]]></category>
		<category><![CDATA[SIMD]]></category>
		<category><![CDATA[slot]]></category>
		<category><![CDATA[snappy]]></category>
		<category><![CDATA[sparse]]></category>
		<category><![CDATA[spec]]></category>
		<category><![CDATA[struct]]></category>
		<category><![CDATA[Table]]></category>
		<category><![CDATA[tsdb]]></category>
		<category><![CDATA[union]]></category>
		<category><![CDATA[ZSTD]]></category>
		<category><![CDATA[列式存储]]></category>
		<category><![CDATA[垃圾回收]]></category>
		<category><![CDATA[引用计数]]></category>
		<category><![CDATA[数据结构]]></category>
		<category><![CDATA[时序数据库]]></category>
		<category><![CDATA[高级数据结构]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3951</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2023/07/31/a-guide-of-using-apache-arrow-for-gopher-part6 Apache Arrow是一种开放的、与语言无关的列式内存格式，在本系列文章的前几篇中，我们都聚焦于内存表示与内存操作。 但对于一个数据库系统或大数据分析平台来说，数据不能也无法一直放在内存中，虽说目前内存很大也足够便宜了，但其易失性也决定了我们在特定时刻还是要将数据序列化后存储到磁盘或一些低成本的存储服务上(比如AWS的S3等)。 那么将Arrow序列化成什么存储格式呢？CSV、JSON？显然这些格式都不是为最大限度提高空间效率以及数据检索能力而设计的。在数据分析领域，Apache Parquet是与Arrow相似的一种开放的、面向列的数据存储格式，它被设计用于高效的数据编码和检索并最大限度提高空间效率。 和Arrow是一种内存格式不同，Parquet是一种数据文件格式。此外，Arrow和Parquet在设计上也做出了各自的一些取舍。Arrow旨在由矢量化计算内核对数据进行操作，提供对任何数组索引的 O(1) 随机访问查找能力；而Parquet为了最大限度提高空间效率，采用了可变长度编码方案和块压缩来大幅减小数据大小，这些技术都是以丧失高性能随机存取查找为代价的。 Parquet也是Apache的顶级项目，大多数实现了Arrow的编程语言也都提供了支持Arrow格式与Parquet文件相互转换的库实现，Go也不例外。在本文中，我们就来粗浅看一下如何使用Go实现Parquet文件的读写，即Arrow和Parquet的相互转换。 注：关于Parquet文件的详细格式(也蛮复杂)，我可能会在后续文章中说明。 1. Parquet简介 如果不先说一说Parquet文件格式，后面的内容理解起来会略有困难的。下面是一个Parquet文件的结构示意图： 图来自https://www.uber.com/blog/cost-efficiency-big-data 我们看到Parquet格式的文件被分为多个row group，每个row group由每一列的列块(column chunk)组成。考虑到磁盘存储的特点，每个列块又分为若干个页。这个列块中的诸多同构类型的列值可以在编码和压缩后存储在各个页中。下面是Parquet官方文档中Parquet文件中数据存储的具体示意图： 我们看到Parquet按row group顺序向后排列，每个row group中column chunk也是依column次序向后排列的。 注：关于上图中repetion level和definition level这样的高级概念，不会成为理解本文内容的障碍，我们将留到后续文章中系统说明。 2. Arrow Table Parquet 有了上面Parquet文件格式的初步知识后，接下来我们就来看看如何使用Go在Arrow和Parquet之间进行转换。 在《高级数据结构》一文中，我们学习了Arrow Table和Record Batch两种高级结构。接下来我们就来看看如何将Table或Record与Parquet进行转换。一旦像Table、Record Batch这样的高级结构的转换搞定了，那Arrow中的那些简单数据类型)也就不在话下了。况且在实际项目中，我们面对更多的也是Arrow的高级数据结构(Table或Record)与Parquet的转换。 我们先来看看Table。 2.1 Table -> Parquet 通过在《高级数据结构》一文，我们知道了Arrow Table的每一列本质上就是Schema+Chunked Array，这和Parquet的文件格式具有较高的适配度。 Arrow Go的parquet实现提供对了Table的良好支持，我们通过一个WriteTable函数就可以将内存中的Arrow Table持久化为Parquet格式的文件，我们来看看下面这个示例： // flat_table_to_parquet.go package main import ( "os" [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part6-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2023/07/31/a-guide-of-using-apache-arrow-for-gopher-part6">本文永久链接</a> &#8211; https://tonybai.com/2023/07/31/a-guide-of-using-apache-arrow-for-gopher-part6</p>
<p>Apache Arrow是一种开放的、与语言无关的列式内存格式，在<a href="https://tonybai.com/tag/arrow">本系列文章</a>的前几篇中，我们都聚焦于<a href="https://tonybai.com/2023/06/25/a-guide-of-using-apache-arrow-for-gopher-part1">内存表示</a>与<a href="https://tonybai.com/2023/07/13/a-guide-of-using-apache-arrow-for-gopher-part4/">内存操作</a>。</p>
<p>但对于一个数据库系统或大数据分析平台来说，数据不能也无法一直放在内存中，虽说目前内存很大也足够便宜了，但其易失性也决定了我们在特定时刻还是要将数据序列化后存储到磁盘或一些低成本的存储服务上(比如AWS的S3等)。</p>
<p>那么将Arrow序列化成什么存储格式呢？CSV、JSON？显然这些格式都不是为最大限度提高空间效率以及数据检索能力而设计的。在数据分析领域，Apache Parquet是与Arrow相似的一种开放的、面向列的数据存储格式，它被设计用于高效的数据编码和检索并最大限度提高空间效率。</p>
<p>和Arrow是一种内存格式不同，<strong>Parquet是一种数据文件格式</strong>。此外，Arrow和Parquet在设计上也做出了各自的一些取舍。Arrow旨在由矢量化计算内核对数据进行操作，提供对任何数组索引的 O(1) 随机访问查找能力；而Parquet为了最大限度提高空间效率，采用了可变长度编码方案和块压缩来大幅减小数据大小，这些技术都是以丧失高性能随机存取查找为代价的。</p>
<p><a href="https://parquet.apache.org">Parquet也是Apache的顶级项目</a>，大多数实现了Arrow的编程语言也都提供了支持Arrow格式与Parquet文件相互转换的库实现，Go也不例外。在本文中，我们就来粗浅看一下如何使用Go实现Parquet文件的读写，即Arrow和Parquet的相互转换。</p>
<blockquote>
<p>注：关于Parquet文件的详细格式(也蛮复杂)，我可能会在后续文章中说明。</p>
</blockquote>
<h2>1. Parquet简介</h2>
<p>如果不先说一说Parquet文件格式，后面的内容理解起来会略有困难的。下面是一个Parquet文件的结构示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part6-2.png" alt="" /><br />
<center>图来自https://www.uber.com/blog/cost-efficiency-big-data </center></p>
<p>我们看到Parquet格式的文件被分为多个row group，每个row group由每一列的列块(column chunk)组成。考虑到磁盘存储的特点，每个列块又分为若干个页。这个列块中的诸多同构类型的列值可以在编码和压缩后存储在各个页中。下面是Parquet官方文档中Parquet文件中数据存储的具体示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part6-3.png" alt="" /></p>
<p>我们看到Parquet按row group顺序向后排列，每个row group中column chunk也是依column次序向后排列的。</p>
<blockquote>
<p>注：关于上图中repetion level和definition level这样的高级概念，不会成为理解本文内容的障碍，我们将留到后续文章中系统说明。</p>
</blockquote>
<h2>2. Arrow Table <-> Parquet</h2>
<p>有了上面Parquet文件格式的初步知识后，接下来我们就来看看如何使用Go在Arrow和Parquet之间进行转换。</p>
<p>在<a href="https://tonybai.com/2023/07/08/a-guide-of-using-apache-arrow-for-gopher-part3/">《高级数据结构》</a>一文中，我们学习了Arrow Table和Record Batch两种高级结构。接下来我们就来看看如何将Table或Record与Parquet进行转换。一旦像Table、Record Batch这样的高级结构的转换搞定了，那Arrow中的那些<a href="https://tonybai.com/2023/06/25/a-guide-of-using-apache-arrow-for-gopher-part1">简单数据类型</a>)也就不在话下了。况且在实际项目中，我们面对更多的也是Arrow的高级数据结构(Table或Record)与Parquet的转换。</p>
<p>我们先来看看Table。</p>
<h3>2.1 Table -> Parquet</h3>
<p>通过在<a href="https://tonybai.com/2023/07/08/a-guide-of-using-apache-arrow-for-gopher-part3/">《高级数据结构》</a>一文，我们知道了Arrow Table的每一列本质上就是Schema+Chunked Array，这和Parquet的文件格式具有较高的适配度。</p>
<p>Arrow Go的parquet实现提供对了Table的良好支持，我们通过一个WriteTable函数就可以将内存中的Arrow Table持久化为Parquet格式的文件，我们来看看下面这个示例：</p>
<pre><code>// flat_table_to_parquet.go

package main

import (
    "os"

    "github.com/apache/arrow/go/v13/arrow"
    "github.com/apache/arrow/go/v13/arrow/array"
    "github.com/apache/arrow/go/v13/arrow/memory"
    "github.com/apache/arrow/go/v13/parquet/pqarrow"
)

func main() {
    schema := arrow.NewSchema(
        []arrow.Field{
            {Name: "col1", Type: arrow.PrimitiveTypes.Int32},
            {Name: "col2", Type: arrow.PrimitiveTypes.Float64},
            {Name: "col3", Type: arrow.BinaryTypes.String},
        },
        nil,
    )

    col1 := func() *arrow.Column {
        chunk := func() *arrow.Chunked {
            ib := array.NewInt32Builder(memory.DefaultAllocator)
            defer ib.Release()

            ib.AppendValues([]int32{1, 2, 3}, nil)
            i1 := ib.NewInt32Array()
            defer i1.Release()

            ib.AppendValues([]int32{4, 5, 6, 7, 8, 9, 10}, nil)
            i2 := ib.NewInt32Array()
            defer i2.Release()

            c := arrow.NewChunked(
                arrow.PrimitiveTypes.Int32,
                []arrow.Array{i1, i2},
            )
            return c
        }()
        defer chunk.Release()

        return arrow.NewColumn(schema.Field(0), chunk)
    }()
    defer col1.Release()

    col2 := func() *arrow.Column {
        chunk := func() *arrow.Chunked {
            fb := array.NewFloat64Builder(memory.DefaultAllocator)
            defer fb.Release()

            fb.AppendValues([]float64{1.1, 2.2, 3.3, 4.4, 5.5}, nil)
            f1 := fb.NewFloat64Array()
            defer f1.Release()

            fb.AppendValues([]float64{6.6, 7.7}, nil)
            f2 := fb.NewFloat64Array()
            defer f2.Release()

            fb.AppendValues([]float64{8.8, 9.9, 10.0}, nil)
            f3 := fb.NewFloat64Array()
            defer f3.Release()

            c := arrow.NewChunked(
                arrow.PrimitiveTypes.Float64,
                []arrow.Array{f1, f2, f3},
            )
            return c
        }()
        defer chunk.Release()

        return arrow.NewColumn(schema.Field(1), chunk)
    }()
    defer col2.Release()

    col3 := func() *arrow.Column {
        chunk := func() *arrow.Chunked {
            sb := array.NewStringBuilder(memory.DefaultAllocator)
            defer sb.Release()

            sb.AppendValues([]string{"s1", "s2"}, nil)
            s1 := sb.NewStringArray()
            defer s1.Release()

            sb.AppendValues([]string{"s3", "s4"}, nil)
            s2 := sb.NewStringArray()
            defer s2.Release()

            sb.AppendValues([]string{"s5", "s6", "s7", "s8", "s9", "s10"}, nil)
            s3 := sb.NewStringArray()
            defer s3.Release()

            c := arrow.NewChunked(
                arrow.BinaryTypes.String,
                []arrow.Array{s1, s2, s3},
            )
            return c
        }()
        defer chunk.Release()

        return arrow.NewColumn(schema.Field(2), chunk)
    }()
    defer col3.Release()

    var tbl arrow.Table
    tbl = array.NewTable(schema, []arrow.Column{*col1, *col2, *col3}, -1)
    defer tbl.Release()

    f, err := os.Create("flat_table.parquet")
    if err != nil {
        panic(err)
    }
    defer f.Close()

    err = pqarrow.WriteTable(tbl, f, 1024, nil, pqarrow.DefaultWriterProps())
    if err != nil {
        panic(err)
    }
}
</code></pre>
<p>我们基于arrow的Builder模式以及NewTable创建了一个拥有三个列的Table(该table的创建例子来自于<a href="https://tonybai.com/2023/07/08/a-guide-of-using-apache-arrow-for-gopher-part3/">《高级数据结构》</a>一文)。有了table后，我们直接调用pqarrow的WriteTable函数即可将table写成parquet格式的文件。</p>
<p>我们来运行一下上述代码：</p>
<pre><code>$go run flat_table_to_parquet.go
</code></pre>
<p>执行完上面命令后，当前目录下会出现一个flat_table.parquet的文件！</p>
<p>我们如何查看该文件内容来验证写入的数据是否与table一致呢？arrow go的parquet实现提供了一个parquet_reader的工具可以帮助我们做到这点，你可以执行如下命令安装这个工具：</p>
<pre><code>$go install github.com/apache/arrow/go/v13/parquet/cmd/parquet_reader@latest
</code></pre>
<p>之后我们就可以执行下面命令查看我们刚刚生成的flat_table.parquet文件的内容了：</p>
<pre><code>$parquet_reader flat_table.parquet
File name: flat_table.parquet
Version: v2.6
Created By: parquet-go version 13.0.0-SNAPSHOT
Num Rows: 10
Number of RowGroups: 1
Number of Real Columns: 3
Number of Columns: 3
Number of Selected Columns: 3
Column 0: col1 (INT32/INT_32)
Column 1: col2 (DOUBLE)
Column 2: col3 (BYTE_ARRAY/UTF8)
--- Row Group: 0  ---
--- Total Bytes: 396  ---
--- Rows: 10  ---
Column 0
 Values: 10, Min: 1, Max: 10, Null Values: 0, Distinct Values: 0
 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE
 Uncompressed Size: 111, Compressed Size: 111
Column 1
 Values: 10, Min: 1.1, Max: 10, Null Values: 0, Distinct Values: 0
 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE
 Uncompressed Size: 169, Compressed Size: 169
Column 2
 Values: 10, Min: [115 49], Max: [115 57], Null Values: 0, Distinct Values: 0
 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE
 Uncompressed Size: 116, Compressed Size: 116
--- Values ---
col1              |col2              |col3              |
1                 |1.100000          |s1                |
2                 |2.200000          |s2                |
3                 |3.300000          |s3                |
4                 |4.400000          |s4                |
5                 |5.500000          |s5                |
6                 |6.600000          |s6                |
7                 |7.700000          |s7                |
8                 |8.800000          |s8                |
9                 |9.900000          |s9                |
10                |10.000000         |s10               |
</code></pre>
<p>parquet_reader列出了parquet文件的meta数据和每个row group中的column列的值，从输出来看，与我们arrow table的数据是一致的。</p>
<p>我们再回头看一下WriteTable函数，它的原型如下：</p>
<pre><code>func WriteTable(tbl arrow.Table, w io.Writer, chunkSize int64,
                props *parquet.WriterProperties, arrprops ArrowWriterProperties) error
</code></pre>
<p>这里说一下WriteTable的前三个参数，第一个是通过NewTable得到的arrow table结构，第二个参数也容易理解，就是一个可写的文件描述符，我们通过os.Create可以轻松拿到，第三个参数为chunkSize，这个chunkSize是什么呢？会对parquet文件的写入结果有影响么？其实这个chunkSize就是每个row group中的行数。同时parquet通过该chunkSize也可以计算出arrow table转parquet文件后有几个row group。</p>
<p>我们示例中的chunkSize值为1024，因此整个parquet文件只有一个row group。下面我们将其值改为5，再来看看输出的parquet文件内容：</p>
<pre><code>$parquet_reader flat_table.parquet
File name: flat_table.parquet
Version: v2.6
Created By: parquet-go version 13.0.0-SNAPSHOT
Num Rows: 10
Number of RowGroups: 2
Number of Real Columns: 3
Number of Columns: 3
Number of Selected Columns: 3
Column 0: col1 (INT32/INT_32)
Column 1: col2 (DOUBLE)
Column 2: col3 (BYTE_ARRAY/UTF8)
--- Row Group: 0  ---
--- Total Bytes: 288  ---
--- Rows: 5  ---
Column 0
 Values: 5, Min: 1, Max: 5, Null Values: 0, Distinct Values: 0
 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE
 Uncompressed Size: 86, Compressed Size: 86
Column 1
 Values: 5, Min: 1.1, Max: 5.5, Null Values: 0, Distinct Values: 0
 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE
 Uncompressed Size: 122, Compressed Size: 122
Column 2
 Values: 5, Min: [115 49], Max: [115 53], Null Values: 0, Distinct Values: 0
 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE
 Uncompressed Size: 80, Compressed Size: 80
--- Values ---
col1              |col2              |col3              |
1                 |1.100000          |s1                |
2                 |2.200000          |s2                |
3                 |3.300000          |s3                |
4                 |4.400000          |s4                |
5                 |5.500000          |s5                |

--- Row Group: 1  ---
--- Total Bytes: 290  ---
--- Rows: 5  ---
Column 0
 Values: 5, Min: 6, Max: 10, Null Values: 0, Distinct Values: 0
 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE
 Uncompressed Size: 86, Compressed Size: 86
Column 1
 Values: 5, Min: 6.6, Max: 10, Null Values: 0, Distinct Values: 0
 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE
 Uncompressed Size: 122, Compressed Size: 122
Column 2
 Values: 5, Min: [115 49 48], Max: [115 57], Null Values: 0, Distinct Values: 0
 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE
 Uncompressed Size: 82, Compressed Size: 82
--- Values ---
col1              |col2              |col3              |
6                 |6.600000          |s6                |
7                 |7.700000          |s7                |
8                 |8.800000          |s8                |
9                 |9.900000          |s9                |
10                |10.000000         |s10               |

</code></pre>
<p>当chunkSize值为5后，parquet文件的row group变成了2，然后parquet_reader工具会按照两个row group的格式分别输出它们的meta信息和列值信息。</p>
<p>接下来，我们再来看一下如何从生成的parquet文件中读取数据并转换为arrow table。</p>
<h3>2.2 Table &lt;- Parquet</h3>
<p>和WriteTable函数对应，arrow提供了ReadTable函数读取parquet文件并转换为内存中的arrow table，下面是代码示例：</p>
<pre><code>// flat_table_from_parquet.go
func main() {
    f, err := os.Open("flat_table.parquet")
    if err != nil {
        panic(err)
    }
    defer f.Close()

    tbl, err := pqarrow.ReadTable(context.Background(), f, parquet.NewReaderProperties(memory.DefaultAllocator),
        pqarrow.ArrowReadProperties{}, memory.DefaultAllocator)
    if err != nil {
        panic(err)
    }

    dumpTable(tbl)
}

func dumpTable(tbl arrow.Table) {
    s := tbl.Schema()
    fmt.Println(s)
    fmt.Println("------")

    fmt.Println("the count of table columns=", tbl.NumCols())
    fmt.Println("the count of table rows=", tbl.NumRows())
    fmt.Println("------")

    for i := 0; i &lt; int(tbl.NumCols()); i++ {
        col := tbl.Column(i)
        fmt.Printf("arrays in column(%s):\n", col.Name())
        chunk := col.Data()
        for _, arr := range chunk.Chunks() {
            fmt.Println(arr)
        }
        fmt.Println("------")
    }
}
</code></pre>
<p>我们看到ReadTable使用起来非常简单，由于parquet文件中包含meta信息，我们调用ReadTable时，一些参数使用默认值或零值即可。</p>
<p>我们运行一下上述代码：</p>
<pre><code>$go run flat_table_from_parquet.go
schema:
  fields: 3
    - col1: type=int32
      metadata: ["PARQUET:field_id": "-1"]
    - col2: type=float64
      metadata: ["PARQUET:field_id": "-1"]
    - col3: type=utf8
      metadata: ["PARQUET:field_id": "-1"]
------
the count of table columns= 3
the count of table rows= 10
------
arrays in column(col1):
[1 2 3 4 5 6 7 8 9 10]
------
arrays in column(col2):
[1.1 2.2 3.3 4.4 5.5 6.6 7.7 8.8 9.9 10]
------
arrays in column(col3):
["s1" "s2" "s3" "s4" "s5" "s6" "s7" "s8" "s9" "s10"]
------
</code></pre>
<h3>2.3 Table -> Parquet(压缩)</h3>
<p>前面提到，Parquet文件格式的设计充分考虑了空间利用效率，再加上其是面向列存储的格式，Parquet支持列数据的压缩存储，并支持为不同列选择不同的压缩算法。</p>
<p>前面示例中调用的WriteTable在默认情况下是不对列进行压缩的，这从parquet_reader读取到的列的元信息中也可以看到(比如下面的Compression: UNCOMPRESSED)：</p>
<pre><code>Column 0
 Values: 10, Min: 1, Max: 10, Null Values: 0, Distinct Values: 0
 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE
 Uncompressed Size: 111, Compressed Size: 111
</code></pre>
<p>我们在WriteTable时也可以通过parquet.WriterProperties参数来为每个列指定压缩算法，比如下面示例：</p>
<pre><code>// flat_table_to_parquet_compressed.go

var tbl arrow.Table
tbl = array.NewTable(schema, []arrow.Column{*col1, *col2, *col3}, -1)
defer tbl.Release()

f, err := os.Create("flat_table_compressed.parquet")
if err != nil {
    panic(err)
}
defer f.Close()

wp := parquet.NewWriterProperties(parquet.WithCompression(compress.Codecs.Snappy),
    parquet.WithCompressionFor("col1", compress.Codecs.Brotli))
err = pqarrow.WriteTable(tbl, f, 1024, wp, pqarrow.DefaultWriterProps())
if err != nil {
    panic(err)
}
</code></pre>
<p>在这段代码中，我们通过parquet.NewWriterProperties构建了新的WriterProperties，这个新的Properties默认所有列使用Snappy压缩，针对col1列使用Brotli算法压缩。我们将压缩后的数据写入flat_table_compressed.parquet文件。使用go run运行flat_table_to_parquet_compressed.go，然后使用parquet_reader查看文件flat_table_compressed.parquet得到如下结果：</p>
<pre><code>$go run flat_table_to_parquet_compressed.go
$parquet_reader flat_table_compressed.parquet
File name: flat_table_compressed.parquet
Version: v2.6
Created By: parquet-go version 13.0.0-SNAPSHOT
Num Rows: 10
Number of RowGroups: 1
Number of Real Columns: 3
Number of Columns: 3
Number of Selected Columns: 3
Column 0: col1 (INT32/INT_32)
Column 1: col2 (DOUBLE)
Column 2: col3 (BYTE_ARRAY/UTF8)
--- Row Group: 0  ---
--- Total Bytes: 352  ---
--- Rows: 10  ---
Column 0
 Values: 10, Min: 1, Max: 10, Null Values: 0, Distinct Values: 0
 Compression: BROTLI, Encodings: RLE_DICTIONARY PLAIN RLE
 Uncompressed Size: 111, Compressed Size: 98
Column 1
 Values: 10, Min: 1.1, Max: 10, Null Values: 0, Distinct Values: 0
 Compression: SNAPPY, Encodings: RLE_DICTIONARY PLAIN RLE
 Uncompressed Size: 168, Compressed Size: 148
Column 2
 Values: 10, Min: [115 49], Max: [115 57], Null Values: 0, Distinct Values: 0
 Compression: SNAPPY, Encodings: RLE_DICTIONARY PLAIN RLE
 Uncompressed Size: 116, Compressed Size: 106
--- Values ---
col1              |col2              |col3              |
1                 |1.100000          |s1                |
2                 |2.200000          |s2                |
3                 |3.300000          |s3                |
4                 |4.400000          |s4                |
5                 |5.500000          |s5                |
6                 |6.600000          |s6                |
7                 |7.700000          |s7                |
8                 |8.800000          |s8                |
9                 |9.900000          |s9                |
10                |10.000000         |s10               |
</code></pre>
<p>从parquet_reader的输出，我们可以看到：各个Column的Compression信息不再是UNCOMPRESSED了，并且三个列在经过压缩后的Size与未压缩对比都有一定的减小：</p>
<pre><code>Column 0:
    Compression: BROTLI, Uncompressed Size: 111, Compressed Size: 98
Column 1:
    Compression: SNAPPY, Uncompressed Size: 168, Compressed Size: 148
Column 2:
    Compression: SNAPPY, Uncompressed Size: 116, Compressed Size: 106
</code></pre>
<p>从文件大小对比也能体现出压缩算法的作用：</p>
<pre><code>-rw-r--r--   1 tonybai  staff   786  7 22 08:06 flat_table.parquet
-rw-r--r--   1 tonybai  staff   742  7 20 13:19 flat_table_compressed.parquet
</code></pre>
<p>Go的parquet实现支持多种压缩算法：</p>
<pre><code>// github.com/apache/arrow/go/parquet/compress/compress.go

var Codecs = struct {
    Uncompressed Compression
    Snappy       Compression
    Gzip         Compression
    // LZO is unsupported in this library since LZO license is incompatible with Apache License
    Lzo    Compression
    Brotli Compression
    // LZ4 unsupported in this library due to problematic issues between the Hadoop LZ4 spec vs regular lz4
    // see: http://mail-archives.apache.org/mod_mbox/arrow-dev/202007.mbox/%3CCAAri41v24xuA8MGHLDvgSnE+7AAgOhiEukemW_oPNHMvfMmrWw@mail.gmail.com%3E
    Lz4  Compression
    Zstd Compression
}{
    Uncompressed: Compression(parquet.CompressionCodec_UNCOMPRESSED),
    Snappy:       Compression(parquet.CompressionCodec_SNAPPY),
    Gzip:         Compression(parquet.CompressionCodec_GZIP),
    Lzo:          Compression(parquet.CompressionCodec_LZO),
    Brotli:       Compression(parquet.CompressionCodec_BROTLI),
    Lz4:          Compression(parquet.CompressionCodec_LZ4),
    Zstd:         Compression(parquet.CompressionCodec_ZSTD),
}
</code></pre>
<p>你只需要根据你的列的类型选择最适合的压缩算法即可。</p>
<h3>2.4 Table &lt;- Parquet(压缩)</h3>
<p>接下来，我们来读取这个数据经过压缩的Parquet。读取压缩的Parquet是否需要在ReadTable时传入特殊的Properties呢？答案是不需要！因为Parquet文件中存储了元信息(metadata)，可以帮助ReadTable使用对应的算法解压缩并提取信息：</p>
<pre><code>// flat_table_from_parquet_compressed.go

func main() {
    f, err := os.Open("flat_table_compressed.parquet")
    if err != nil {
        panic(err)
    }
    defer f.Close()

    tbl, err := pqarrow.ReadTable(context.Background(), f, parquet.NewReaderProperties(memory.DefaultAllocator),
        pqarrow.ArrowReadProperties{}, memory.DefaultAllocator)
    if err != nil {
        panic(err)
    }

    dumpTable(tbl)
}
</code></pre>
<p>运行这段程序，我们就可以读取压缩后的parquet文件了：</p>
<pre><code>$go run flat_table_from_parquet_compressed.go
schema:
  fields: 3
    - col1: type=int32
      metadata: ["PARQUET:field_id": "-1"]
    - col2: type=float64
      metadata: ["PARQUET:field_id": "-1"]
    - col3: type=utf8
      metadata: ["PARQUET:field_id": "-1"]
------
the count of table columns= 3
the count of table rows= 10
------
arrays in column(col1):
[1 2 3 4 5 6 7 8 9 10]
------
arrays in column(col2):
[1.1 2.2 3.3 4.4 5.5 6.6 7.7 8.8 9.9 10]
------
arrays in column(col3):
["s1" "s2" "s3" "s4" "s5" "s6" "s7" "s8" "s9" "s10"]
------
</code></pre>
<p>接下来，我们来看看Arrow中的另外一种高级数据结构Record Batch如何实现与Parquet文件格式的转换。</p>
<h2>3. Arrow Record Batch <-> Parquet</h2>
<blockquote>
<p>注：大家可以先阅读/温习一下<a href="https://tonybai.com/2023/07/08/a-guide-of-using-apache-arrow-for-gopher-part3/">《高级数据结构》</a>一文来了解一下Record Batch的概念。</p>
</blockquote>
<h3>3.1 Record Batch -> Parquet</h3>
<p>Arrow Go实现将一个Record Batch作为一个Row group来对应。下面的程序向Parquet文件中写入了三个record，我们来看一下：</p>
<pre><code>// flat_record_to_parquet.go

func main() {
    var records []arrow.Record
    schema := arrow.NewSchema(
        []arrow.Field{
            {Name: "archer", Type: arrow.BinaryTypes.String},
            {Name: "location", Type: arrow.BinaryTypes.String},
            {Name: "year", Type: arrow.PrimitiveTypes.Int16},
        },
        nil,
    )

    rb := array.NewRecordBuilder(memory.DefaultAllocator, schema)
    defer rb.Release()

    for i := 0; i &lt; 3; i++ {
        postfix := strconv.Itoa(i)
        rb.Field(0).(*array.StringBuilder).AppendValues([]string{"tony" + postfix, "amy" + postfix, "jim" + postfix}, nil)
        rb.Field(1).(*array.StringBuilder).AppendValues([]string{"beijing" + postfix, "shanghai" + postfix, "chengdu" + postfix}, nil)
        rb.Field(2).(*array.Int16Builder).AppendValues([]int16{1992 + int16(i), 1993 + int16(i), 1994 + int16(i)}, nil)
        rec := rb.NewRecord()
        records = append(records, rec)
    }

    // write to parquet
    f, err := os.Create("flat_record.parquet")
    if err != nil {
        panic(err)
    }

    props := parquet.NewWriterProperties()
    writer, err := pqarrow.NewFileWriter(schema, f, props,
        pqarrow.DefaultWriterProps())
    if err != nil {
        panic(err)
    }
    defer writer.Close()

    for _, rec := range records {
        if err := writer.Write(rec); err != nil {
            panic(err)
        }
        rec.Release()
    }
}
</code></pre>
<p>和调用WriteTable完成table到parquet文件的写入不同，这里我们创建了一个FileWriter，通过FileWriter将构建出的Record Batch逐个写入。运行上述代码生成flat_record.parquet文件并使用parquet_reader展示该文件的内容：</p>
<pre><code>$go run flat_record_to_parquet.go
$parquet_reader flat_record.parquet
File name: flat_record.parquet
Version: v2.6
Created By: parquet-go version 13.0.0-SNAPSHOT
Num Rows: 9
Number of RowGroups: 3
Number of Real Columns: 3
Number of Columns: 3
Number of Selected Columns: 3
Column 0: archer (BYTE_ARRAY/UTF8)
Column 1: location (BYTE_ARRAY/UTF8)
Column 2: year (INT32/INT_16)
--- Row Group: 0  ---
--- Total Bytes: 255  ---
--- Rows: 3  ---
Column 0
 Values: 3, Min: [97 109 121 48], Max: [116 111 110 121 48], Null Values: 0, Distinct Values: 0
 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE
 Uncompressed Size: 79, Compressed Size: 79
Column 1
 Values: 3, Min: [98 101 105 106 105 110 103 48], Max: [115 104 97 110 103 104 97 105 48], Null Values: 0, Distinct Values: 0
 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE
 Uncompressed Size: 99, Compressed Size: 99
Column 2
 Values: 3, Min: 1992, Max: 1994, Null Values: 0, Distinct Values: 0
 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE
 Uncompressed Size: 77, Compressed Size: 77
--- Values ---
archer            |location          |year              |
tony0             |beijing0          |1992              |
amy0              |shanghai0         |1993              |
jim0              |chengdu0          |1994              |

--- Row Group: 1  ---
--- Total Bytes: 255  ---
--- Rows: 3  ---
Column 0
 Values: 3, Min: [97 109 121 49], Max: [116 111 110 121 49], Null Values: 0, Distinct Values: 0
 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE
 Uncompressed Size: 79, Compressed Size: 79
Column 1
 Values: 3, Min: [98 101 105 106 105 110 103 49], Max: [115 104 97 110 103 104 97 105 49], Null Values: 0, Distinct Values: 0
 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE
 Uncompressed Size: 99, Compressed Size: 99
Column 2
 Values: 3, Min: 1993, Max: 1995, Null Values: 0, Distinct Values: 0
 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE
 Uncompressed Size: 77, Compressed Size: 77
--- Values ---
archer            |location          |year              |
tony1             |beijing1          |1993              |
amy1              |shanghai1         |1994              |
jim1              |chengdu1          |1995              |

--- Row Group: 2  ---
--- Total Bytes: 255  ---
--- Rows: 3  ---
Column 0
 Values: 3, Min: [97 109 121 50], Max: [116 111 110 121 50], Null Values: 0, Distinct Values: 0
 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE
 Uncompressed Size: 79, Compressed Size: 79
Column 1
 Values: 3, Min: [98 101 105 106 105 110 103 50], Max: [115 104 97 110 103 104 97 105 50], Null Values: 0, Distinct Values: 0
 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE
 Uncompressed Size: 99, Compressed Size: 99
Column 2
 Values: 3, Min: 1994, Max: 1996, Null Values: 0, Distinct Values: 0
 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE
 Uncompressed Size: 77, Compressed Size: 77
--- Values ---
archer            |location          |year              |
tony2             |beijing2          |1994              |
amy2              |shanghai2         |1995              |
jim2              |chengdu2          |1996              |
</code></pre>
<p>我们看到parquet_reader分别输出了三个row group的元数据和列值，每个row group与我们写入的一个record对应。</p>
<p>那读取这样的parquet文件与ReadTable有何不同呢？我们继续往下看。</p>
<h3>3.2 Record Batch &lt;- Parquet</h3>
<p>下面是用于读取</p>
<pre><code>// flat_record_from_parquet.go
func main() {
    f, err := os.Open("flat_record.parquet")
    if err != nil {
        panic(err)
    }
    defer f.Close()

    rdr, err := file.NewParquetReader(f)
    if err != nil {
        panic(err)
    }
    defer rdr.Close()

    arrRdr, err := pqarrow.NewFileReader(rdr,
        pqarrow.ArrowReadProperties{
            BatchSize: 3,
        }, memory.DefaultAllocator)
    if err != nil {
        panic(err)
    }

    s, _ := arrRdr.Schema()
    fmt.Println(*s)

    rr, err := arrRdr.GetRecordReader(context.Background(), nil, nil)
    if err != nil {
        panic(err)
    }

    for {
        rec, err := rr.Read()
        if err != nil &amp;&amp; err != io.EOF {
            panic(err)
        }
        if err == io.EOF {
            break
        }
        fmt.Println(rec)
    }
}
</code></pre>
<p>我们看到相对于将parquet转换为table，将parquet转换为record略为复杂一些，这里的一个关键是在调用NewFileReader时传入的ArrowReadProperties中的BatchSize字段，要想正确读取出record，这个BatchSize需适当填写。这个BatchSize会告诉Reader 每个读取的Record Batch的长度，也就是row数量。这里传入的是3，即3个row为一个Recordd batch。</p>
<p>下面是运行上述程序的结果：</p>
<pre><code>$go run flat_record_from_parquet.go
{[{archer 0x26ccc00 false {[PARQUET:field_id] [-1]}} {location 0x26ccc00 false {[PARQUET:field_id] [-1]}} {year 0x26ccc00 false {[PARQUET:field_id] [-1]}}] map[archer:[0] location:[1] year:[2]] {[] []} 0}
record:
  schema:
  fields: 3
    - archer: type=utf8
        metadata: ["PARQUET:field_id": "-1"]
    - location: type=utf8
          metadata: ["PARQUET:field_id": "-1"]
    - year: type=int16
      metadata: ["PARQUET:field_id": "-1"]
  rows: 3
  col[0][archer]: ["tony0" "amy0" "jim0"]
  col[1][location]: ["beijing0" "shanghai0" "chengdu0"]
  col[2][year]: [1992 1993 1994]

record:
  schema:
  fields: 3
    - archer: type=utf8
        metadata: ["PARQUET:field_id": "-1"]
    - location: type=utf8
          metadata: ["PARQUET:field_id": "-1"]
    - year: type=int16
      metadata: ["PARQUET:field_id": "-1"]
  rows: 3
  col[0][archer]: ["tony1" "amy1" "jim1"]
  col[1][location]: ["beijing1" "shanghai1" "chengdu1"]
  col[2][year]: [1993 1994 1995]

record:
  schema:
  fields: 3
    - archer: type=utf8
        metadata: ["PARQUET:field_id": "-1"]
    - location: type=utf8
          metadata: ["PARQUET:field_id": "-1"]
    - year: type=int16
      metadata: ["PARQUET:field_id": "-1"]
  rows: 3
  col[0][archer]: ["tony2" "amy2" "jim2"]
  col[1][location]: ["beijing2" "shanghai2" "chengdu2"]
  col[2][year]: [1994 1995 1996]

</code></pre>
<p>我们看到：每3行被作为一个record读取出来了。如果将BatchSize改为5，则输出如下：</p>
<pre><code>$go run flat_record_from_parquet.go
{[{archer 0x26ccc00 false {[PARQUET:field_id] [-1]}} {location 0x26ccc00 false {[PARQUET:field_id] [-1]}} {year 0x26ccc00 false {[PARQUET:field_id] [-1]}}] map[archer:[0] location:[1] year:[2]] {[] []} 0}
record:
  schema:
  fields: 3
    - archer: type=utf8
        metadata: ["PARQUET:field_id": "-1"]
    - location: type=utf8
          metadata: ["PARQUET:field_id": "-1"]
    - year: type=int16
      metadata: ["PARQUET:field_id": "-1"]
  rows: 5
  col[0][archer]: ["tony0" "amy0" "jim0" "tony1" "amy1"]
  col[1][location]: ["beijing0" "shanghai0" "chengdu0" "beijing1" "shanghai1"]
  col[2][year]: [1992 1993 1994 1993 1994]

record:
  schema:
  fields: 3
    - archer: type=utf8
        metadata: ["PARQUET:field_id": "-1"]
    - location: type=utf8
          metadata: ["PARQUET:field_id": "-1"]
    - year: type=int16
      metadata: ["PARQUET:field_id": "-1"]
  rows: 4
  col[0][archer]: ["jim1" "tony2" "amy2" "jim2"]
  col[1][location]: ["chengdu1" "beijing2" "shanghai2" "chengdu2"]
  col[2][year]: [1995 1994 1995 1996]
</code></pre>
<p>这次：前5行作为一个record，后4行作为另外一个record。</p>
<p>当然，我们也可以使用flat_table_from_parquet.go中的代码来读取flat_record.parquet(将读取文件名改为flat_record.parquet)，只不过由于将parquet数据转换为了table，其输出内容将变为：</p>
<pre><code>$go run flat_table_from_parquet.go
schema:
  fields: 3
    - archer: type=utf8
        metadata: ["PARQUET:field_id": "-1"]
    - location: type=utf8
          metadata: ["PARQUET:field_id": "-1"]
    - year: type=int16
      metadata: ["PARQUET:field_id": "-1"]
------
the count of table columns= 3
the count of table rows= 9
------
arrays in column(archer):
["tony0" "amy0" "jim0" "tony1" "amy1" "jim1" "tony2" "amy2" "jim2"]
------
arrays in column(location):
["beijing0" "shanghai0" "chengdu0" "beijing1" "shanghai1" "chengdu1" "beijing2" "shanghai2" "chengdu2"]
------
arrays in column(year):
[1992 1993 1994 1993 1994 1995 1994 1995 1996]
------
</code></pre>
<h3>3.3 Record Batch -> Parquet(压缩)</h3>
<p>Recod同样支持压缩写入Parquet，其原理与前面table压缩存储是一致的，都是通过设置WriterProperties来实现的：</p>
<pre><code>// flat_record_to_parquet_compressed.go

func main() {
    ... ...
    f, err := os.Create("flat_record_compressed.parquet")
    if err != nil {
        panic(err)
    }
    defer f.Close()

    props := parquet.NewWriterProperties(parquet.WithCompression(compress.Codecs.Zstd),
        parquet.WithCompressionFor("year", compress.Codecs.Brotli))
    writer, err := pqarrow.NewFileWriter(schema, f, props,
        pqarrow.DefaultWriterProps())
    if err != nil {
        panic(err)
    }
    defer writer.Close()

    for _, rec := range records {
        if err := writer.Write(rec); err != nil {
            panic(err)
        }
        rec.Release()
    }
}
</code></pre>
<p>不过这次针对arrow.string类型和arrow.int16类型的压缩效果非常“差”：</p>
<pre><code>$parquet_reader flat_record_compressed.parquet
File name: flat_record_compressed.parquet
Version: v2.6
Created By: parquet-go version 13.0.0-SNAPSHOT
Num Rows: 9
Number of RowGroups: 3
Number of Real Columns: 3
Number of Columns: 3
Number of Selected Columns: 3
Column 0: archer (BYTE_ARRAY/UTF8)
Column 1: location (BYTE_ARRAY/UTF8)
Column 2: year (INT32/INT_16)
--- Row Group: 0  ---
--- Total Bytes: 315  ---
--- Rows: 3  ---
Column 0
 Values: 3, Min: [97 109 121 48], Max: [116 111 110 121 48], Null Values: 0, Distinct Values: 0
 Compression: ZSTD, Encodings: RLE_DICTIONARY PLAIN RLE
 Uncompressed Size: 79, Compressed Size: 105
Column 1
 Values: 3, Min: [98 101 105 106 105 110 103 48], Max: [115 104 97 110 103 104 97 105 48], Null Values: 0, Distinct Values: 0
 Compression: ZSTD, Encodings: RLE_DICTIONARY PLAIN RLE
 Uncompressed Size: 99, Compressed Size: 125
Column 2
 Values: 3, Min: 1992, Max: 1994, Null Values: 0, Distinct Values: 0
 Compression: BROTLI, Encodings: RLE_DICTIONARY PLAIN RLE
 Uncompressed Size: 77, Compressed Size: 85
--- Values ---
archer            |location          |year              |
tony0             |beijing0          |1992              |
amy0              |shanghai0         |1993              |
jim0              |chengdu0          |1994              |

--- Row Group: 1  ---
--- Total Bytes: 315  ---
--- Rows: 3  ---
Column 0
 Values: 3, Min: [97 109 121 49], Max: [116 111 110 121 49], Null Values: 0, Distinct Values: 0
 Compression: ZSTD, Encodings: RLE_DICTIONARY PLAIN RLE
 Uncompressed Size: 79, Compressed Size: 105
Column 1
 Values: 3, Min: [98 101 105 106 105 110 103 49], Max: [115 104 97 110 103 104 97 105 49], Null Values: 0, Distinct Values: 0
 Compression: ZSTD, Encodings: RLE_DICTIONARY PLAIN RLE
 Uncompressed Size: 99, Compressed Size: 125
Column 2
 Values: 3, Min: 1993, Max: 1995, Null Values: 0, Distinct Values: 0
 Compression: BROTLI, Encodings: RLE_DICTIONARY PLAIN RLE
 Uncompressed Size: 77, Compressed Size: 85
--- Values ---
archer            |location          |year              |
tony1             |beijing1          |1993              |
amy1              |shanghai1         |1994              |
jim1              |chengdu1          |1995              |

--- Row Group: 2  ---
--- Total Bytes: 315  ---
--- Rows: 3  ---
Column 0
 Values: 3, Min: [97 109 121 50], Max: [116 111 110 121 50], Null Values: 0, Distinct Values: 0
 Compression: ZSTD, Encodings: RLE_DICTIONARY PLAIN RLE
 Uncompressed Size: 79, Compressed Size: 105
Column 1
 Values: 3, Min: [98 101 105 106 105 110 103 50], Max: [115 104 97 110 103 104 97 105 50], Null Values: 0, Distinct Values: 0
 Compression: ZSTD, Encodings: RLE_DICTIONARY PLAIN RLE
 Uncompressed Size: 99, Compressed Size: 125
Column 2
 Values: 3, Min: 1994, Max: 1996, Null Values: 0, Distinct Values: 0
 Compression: BROTLI, Encodings: RLE_DICTIONARY PLAIN RLE
 Uncompressed Size: 77, Compressed Size: 85
--- Values ---
archer            |location          |year              |
tony2             |beijing2          |1994              |
amy2              |shanghai2         |1995              |
jim2              |chengdu2          |1996              |
</code></pre>
<p>越压缩，parquet文件的size越大。当然这个问题不是我们这篇文章的重点，只是提醒大家<strong>选择适当的压缩算法十分重要</strong>。</p>
<h3>3.4 Record Batch &lt;- Parquet(压缩)</h3>
<p>和读取table转换后的压缩parquet文件一样，读取record转换后的压缩parquet一样无需特殊设置，使用flat_record_from_parquet.go即可（需要改一下读取的文件名），这里就不赘述了。</p>
<h2>4. 小结</h2>
<p>本文旨在介绍使用Go进行Arrow和Parquet文件相互转换的基本方法，我们以table和record两种高级数据结构为例，分别介绍了读写parquet文件以及压缩parquet文件的方法。</p>
<p>当然本文中的例子都是“平坦(flat)”的简单例子，parquet文件还支持更复杂的嵌套数据，我们会在后续的深入讲解parquet格式的文章中提及。</p>
<p>本文示例代码可以在<a href="https://github.com/bigwhite/experiments/blob/master/arrow/parquet">这里</a>下载。</p>
<h2>5. 参考资料</h2>
<ul>
<li>Parquet File Format &#8211; https://parquet.apache.org/docs/file-format/</li>
<li>《Dremel: Interactive Analysis of Web-Scale Datasets》 &#8211; https://storage.googleapis.com/pub-tools-public-publication-data/pdf/36632.pdf</li>
<li>Announcing Parquet 1.0: Columnar Storage for Hadoop &#8211; https://blog.twitter.com/engineering/en_us/a/2013/announcing-parquet-10-columnar-storage-for-hadoop</li>
<li>Dremel made simple with Parquet &#8211; https://blog.twitter.com/engineering/en_us/a/2013/dremel-made-simple-with-parquet</li>
<li>parquet项目首页 &#8211; http://parquet.apache.org/</li>
<li>Apache Parquet介绍 by influxdata  &#8211; https://www.influxdata.com/glossary/apache-parquet/</li>
<li>Intro to InfluxDB IOx &#8211; https://www.influxdata.com/blog/intro-influxdb-iox/</li>
<li>Apache Arrow介绍 by influxdb &#8211; https://www.influxdata.com/glossary/apache-arrow/</li>
<li>开源时序数据库解析 &#8211; InfluxDB IOx &#8211; https://zhuanlan.zhihu.com/p/534035337</li>
<li>Arrow and Parquet Part 1: Primitive Types and Nullability &#8211; https://arrow.apache.org/blog/2022/10/05/arrow-parquet-encoding-part-1/</li>
<li>Arrow and Parquet Part 2: Nested and Hierarchical Data using Structs and Lists &#8211; https://arrow.apache.org/blog/2022/10/08/arrow-parquet-encoding-part-2/</li>
<li>Arrow and Parquet Part 3: Arbitrary Nesting with Lists of Structs and Structs of Lists &#8211; https://arrow.apache.org/blog/2022/10/17/arrow-parquet-encoding-part-3/</li>
<li>Cost Efficiency @ Scale in Big Data File Format &#8211; https://www.uber.com/blog/cost-efficiency-big-data/</li>
</ul>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2023, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2023/07/31/a-guide-of-using-apache-arrow-for-gopher-part6/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go语言开发者的Apache Arrow使用指南：扩展compute包</title>
		<link>https://tonybai.com/2023/07/22/a-guide-of-using-apache-arrow-for-gopher-part5/</link>
		<comments>https://tonybai.com/2023/07/22/a-guide-of-using-apache-arrow-for-gopher-part5/#comments</comments>
		<pubDate>Sat, 22 Jul 2023 13:58:57 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Apache]]></category>
		<category><![CDATA[Array]]></category>
		<category><![CDATA[Arrow]]></category>
		<category><![CDATA[ArrowColumnarFormat]]></category>
		<category><![CDATA[bitmap]]></category>
		<category><![CDATA[buffer]]></category>
		<category><![CDATA[Builder模式]]></category>
		<category><![CDATA[ChunkedArray]]></category>
		<category><![CDATA[compute]]></category>
		<category><![CDATA[cookbook]]></category>
		<category><![CDATA[Cpp]]></category>
		<category><![CDATA[csv]]></category>
		<category><![CDATA[Datum]]></category>
		<category><![CDATA[dictionary]]></category>
		<category><![CDATA[dremio]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[greptimedb]]></category>
		<category><![CDATA[InfluxDB]]></category>
		<category><![CDATA[influxdb-iox]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[Kaggle]]></category>
		<category><![CDATA[layout]]></category>
		<category><![CDATA[list]]></category>
		<category><![CDATA[metadata]]></category>
		<category><![CDATA[offset]]></category>
		<category><![CDATA[parquet]]></category>
		<category><![CDATA[primitive]]></category>
		<category><![CDATA[Record]]></category>
		<category><![CDATA[RecordBatch]]></category>
		<category><![CDATA[RPC]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[scalar]]></category>
		<category><![CDATA[Schema]]></category>
		<category><![CDATA[semver]]></category>
		<category><![CDATA[SIMD]]></category>
		<category><![CDATA[slot]]></category>
		<category><![CDATA[sparse]]></category>
		<category><![CDATA[spec]]></category>
		<category><![CDATA[struct]]></category>
		<category><![CDATA[Table]]></category>
		<category><![CDATA[tsdb]]></category>
		<category><![CDATA[union]]></category>
		<category><![CDATA[列式存储]]></category>
		<category><![CDATA[垃圾回收]]></category>
		<category><![CDATA[引用计数]]></category>
		<category><![CDATA[数据结构]]></category>
		<category><![CDATA[时序数据库]]></category>
		<category><![CDATA[高级数据结构]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3946</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2023/07/22/a-guide-of-using-apache-arrow-for-gopher-part5 在本系列文章的第4篇《Go语言开发者的Apache Arrow使用指南：数据操作》中我们遇到了大麻烦：Go的Arrow实现居然不支持像max、min、sum这样的简单聚合计算函数:(，分组聚合(grouped aggregation)就更是“遥不可期”。要想对从CSV读取的数据进行聚合操作和分析，我们只能“自己动手，丰衣足食” &#8211; 扩展Arrow Go实现中的compute包了。 不过，Arrow的Go实现还是蛮复杂的，如果对其结构没有一个初步的认知，很难实现这类扩展。在这篇文章中，我们就来了解一下compute包的结构，并尝试为compute包添加几个简单的、仅能处理单一类型的聚合函数，先来完成一些从0到1的工作。 为了深入了解Go Arrow实现，我又翻阅了一下Arrow官方的文档，显然Arrow C++的文档是最丰富的。我快读了一下C++的Arrow文档，对Arrow的结构有了更深刻的认知，基于这些资料，我们先来做一下Arrow结构的回顾。 0. 回顾Arrow的各个layer Arrow的C++文档使用layer来介绍各种Arrow的概念，我们挑几个重要的看一下： 物理层(The physical layer) 物理层针对的是内存的分配管理，包括内存分配的方法(堆分配器、内存文件映射、静态内存区)等。这一层的一个最重要的概念就是我们之前在数据类型一文中提到的Buffer抽象，它代表了内存中的一块连续的数据存储区域。 一维表示层(The one-dimensional layer) 除了物理层，后续的层都是逻辑层。一维表示层是一个逻辑表示层，它定义了Arrow的最基本数据类型：array。数据类型决定了物理层内存数据的解释方法，逻辑数据类型array在物理层投影为一个和多个内存buffer。 我们在“高级数据结构”提到的chunked array也在这一层，chunked array由多个同构类型的array组成，Arrow将其理解为一个同构的(相同类型的)、逻辑上值连续的、更大的array，是array基础类型的一个更泛化的表示。 二维表示层(The two-dimensional layer) “高级数据结构”一文中除chunked array之外的概念，都在这一层，包括schema、table、record batch。 schema是用于描述一维数据(一列数据，即一个逻辑array)的元数据，包括列名、类型与其他元信息。 Table是schema+与schema元信息对应的多个chunked array，它是Arrow中数据集抽象能力最强的逻辑结构。 Record Batch则是schema+与schema元信息对应的多个array。还记得“高级数据结构”一文中的那副直观给出table与record batch差异的图么： 计算层(The compute layer) 计算层一个重要的抽象是Datum，这是一个灵活的抽象，用于统一表示参与计算的各类输入参数和返回值。 计算层真正执行计算的函数被统一放在kernel这个“层次”中，这个层次的函数对Datum类型的输入参数进行计算并返回Datam类型的结果或以Datum类型的输出参数承载计算结果。 IPC层(The Inter-Process Communication (IPC) layer) 这是我们尚未接触过的一层，通过这一层，复合Arrow columnar format的数据可以在进程间(同一主机或不同主机)交互，并且这种交换可以保证尽可能少的内存copy。 文件格式层(The file formats [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part5-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2023/07/22/a-guide-of-using-apache-arrow-for-gopher-part5">本文永久链接</a> &#8211; https://tonybai.com/2023/07/22/a-guide-of-using-apache-arrow-for-gopher-part5</p>
<p>在本系列文章的第4篇<a href="https://tonybai.com/2023/07/13/a-guide-of-using-apache-arrow-for-gopher-part4/">《Go语言开发者的Apache Arrow使用指南：数据操作》</a>中我们遇到了<strong>大麻烦</strong>：Go的Arrow实现居然不支持像max、min、sum这样的简单聚合计算函数:(，分组聚合(grouped aggregation)就更是“遥不可期”。要想对<a href="https://tonybai.com/2023/07/13/a-guide-of-using-apache-arrow-for-gopher-part4/">从CSV读取的数据</a>进行聚合操作和分析，我们只能“自己动手，丰衣足食” &#8211; <strong>扩展Arrow Go实现中的compute包了</strong>。</p>
<p>不过，Arrow的Go实现还是蛮复杂的，如果对其结构没有一个初步的认知，很难实现这类扩展。在这篇文章中，我们就来了解一下compute包的结构，并尝试为compute包添加几个简单的、仅能处理单一类型的聚合函数，先来完成一些<strong>从0到1</strong>的工作。</p>
<p>为了深入了解Go Arrow实现，我又翻阅了一下Arrow官方的文档，显然<a href="https://arrow.apache.org/docs/cpp/overview.html">Arrow C++的文档</a>是最丰富的。我快读了一下C++的Arrow文档，对Arrow的结构有了更深刻的认知，基于这些资料，我们先来做一下Arrow结构的回顾。</p>
<h2>0. 回顾Arrow的各个layer</h2>
<p>Arrow的C++文档使用layer来介绍各种Arrow的概念，我们挑几个重要的看一下：</p>
<ul>
<li>物理层(The physical layer)</li>
</ul>
<p>物理层针对的是内存的分配管理，包括内存分配的方法(堆分配器、内存文件映射、静态内存区)等。这一层的一个最重要的概念就是我们之前在<a href="https://tonybai.com/2023/06/25/a-guide-of-using-apache-arrow-for-gopher-part1">数据类型</a>一文中提到的Buffer抽象，它代表了<strong>内存中的一块连续的数据存储区域</strong>。</p>
<ul>
<li>一维表示层(The one-dimensional layer)</li>
</ul>
<p>除了物理层，后续的层都是<strong>逻辑层</strong>。一维表示层是一个逻辑表示层，它定义了Arrow的最基本数据类型：<strong>array</strong>。<a href="https://tonybai.com/2022/12/18/go-type-system">数据类型决定了物理层内存数据的解释方法</a>，逻辑数据类型array在物理层投影为<strong>一个和多个内存buffer</strong>。</p>
<p>我们在<a href="https://tonybai.com/2023/07/08/a-guide-of-using-apache-arrow-for-gopher-part3/">“高级数据结构”</a>提到的chunked array也在这一层，chunked array由多个同构类型的array组成，Arrow将其理解为一个同构的(相同类型的)、逻辑上值连续的、更大的array，是array基础类型的一个更泛化的表示。</p>
<ul>
<li>二维表示层(The two-dimensional layer)</li>
</ul>
<p><a href="https://tonybai.com/2023/07/08/a-guide-of-using-apache-arrow-for-gopher-part3/">“高级数据结构”</a>一文中除chunked array之外的概念，都在这一层，包括schema、table、record batch。</p>
<p>schema是用于描述一维数据(一列数据，即一个逻辑array)的元数据，包括列名、类型与其他元信息。</p>
<p>Table是schema+与schema元信息对应的多个chunked array，它是Arrow中数据集抽象能力最强的逻辑结构。</p>
<p>Record Batch则是schema+与schema元信息对应的多个array。还记得<a href="https://tonybai.com/2023/07/08/a-guide-of-using-apache-arrow-for-gopher-part3/">“高级数据结构”</a>一文中的那副直观给出table与record batch差异的图么：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part3-5.png" alt="" /></p>
<ul>
<li>计算层(The compute layer)</li>
</ul>
<p>计算层一个重要的抽象是Datum，这是一个灵活的抽象，用于统一表示参与计算的各类输入参数和返回值。</p>
<p>计算层真正执行计算的函数被统一放在kernel这个“层次”中，这个层次的函数对Datum类型的输入参数进行计算并返回Datam类型的结果或以Datum类型的输出参数承载计算结果。</p>
<ul>
<li>IPC层(The Inter-Process Communication (IPC) layer)</li>
</ul>
<p>这是我们尚未接触过的一层，通过这一层，复合Arrow columnar format的数据可以在进程间(同一主机或不同主机)交互，并且这种交换可以保证尽可能少的内存copy。</p>
<ul>
<li>文件格式层(The file formats layer)</li>
</ul>
<p>这一层负责读写文件，在之前的“数据操作”一篇中，我们接触过将CSV文件中的数据读到内存中并组织为Arrow列式存储格式，在后续篇章中，我们还将陆续介绍Arrow与CSV(写入)、Parquet文件的数据交互。</p>
<p>C++有关Arrow的介绍中还有设备层(the devices layer)、文件系统层(the file system layer)等，后续可能不会涉及，这里就不说了。</p>
<p>通过上述回顾，再对照本系列第一篇文章<a href="https://tonybai.com/2023/06/25/a-guide-of-using-apache-arrow-for-gopher-part1">“数据类型”</a>的内容，你对Arrow的理解是不是更深刻一点点了呢:)。</p>
<p>接下来，我们重点看看计算层(the compute layer)。</p>
<h2>1. 计算层(the compute layer)的结构</h2>
<p>Go语言的计算层在compute目录下。Go语言借鉴了C++计算层的设计，将计算层分为compute和kernel，这个从代码布局上也可以明显看出来：</p>
<pre><code>$tree -F -L 2 compute|grep -v go
compute           --- compute层
├── exprs/
├── internal/
│   ├── exec/
│   └── kernels/  --- compute的kernel层
</code></pre>
<p>compute包采用了registry模式，初始化时将底层的kernel function包装成上层的Function并注册到registry中。用户调用某个function时，该function会在registry中查找对应的注册函数并调用。</p>
<p>下面我们通过Uniq这个array-wise函数作为例子来探索一下kernel function的注册与调用过程。下面是<a href="https://tonybai.com/2023/07/13/a-guide-of-using-apache-arrow-for-gopher-part4/">“数据操作”</a>一文中的示例，这里再次借用一下：</p>
<pre><code>// arrow/manipulation/unary_arraywise_function.go

func main() {
    data := []int32{5, 10, 0, 25, 2, 10, 2, 25}
    bldr := array.NewInt32Builder(memory.DefaultAllocator)
    defer bldr.Release()
    bldr.AppendValues(data, nil)
    arr := bldr.NewArray()
    defer arr.Release()

    dat, err := compute.Unique(context.Background(), compute.NewDatum(arr))
    if err != nil {
        fmt.Println(err)
        return
    }

    arr1, ok := dat.(*compute.ArrayDatum)
    if !ok {
        fmt.Println("type assert fail")
        return
    }
    fmt.Println(arr1.MakeArray()) // [5 10 0 25 2]
}
</code></pre>
<p>下面是Unique函数的注册和调用过程示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part5-2.png" alt="" /></p>
<p>很显然，整个过程包括两个明显的阶段：</p>
<ul>
<li>包装并向Registry注册kernel函数(AddFunction)</li>
<li>在Registry中查找函数并调用(GetFunction)</li>
</ul>
<p>当我们在用户层调用compute.Unique函数时，一个统一的CallFunction会被调用，其第二个参数”uniq”表明我们要调用registry中的名为”uniq”的包装函数。在这个过程中GetFunctionRegistry被调用以获取registry实例，在这个过程中，如果registry实例尚没有创建，GetFunctionRegistry会在sync.Once的保护下创建registry并进行初始注册工作(RegisterXXX)。”uniq”对应的包装函数是在RegisterVectorHash中被注册到registry中的。</p>
<p>RegisterVectorHash会通过kernel层提供的GetVectorHashKernels获取kernel层的”uniq”实现，并将其通过NewVectorFunction和AddKernel包装为uniqFn这一用户层的Function，该uniqFn Function最终会被AddFunction加入到registry中。</p>
<p>而CallFunction(ctx, “uniq”)也会从registry中将uniqFn查找出来并执行其Execute方法，该Execute方法实际上执行的是kernel层的”uniq”实现。</p>
<p>我们看到：通过示意图展示的Unique函数的注册与调用过程还是相对清晰的(但如果要阅读对应的代码，还是比较繁琐的)。</p>
<p>到这里我们也大致了解了compute包的结构以及与kernel层的关系，接下来我们就来尝试给compute包添加一些scalar aggregate函数，所谓scalar aggregate函数就是输入是array，输出是一个scalar值的函数，比如：max、min、sum等。</p>
<h2>3. 添加Max、Min、Sum、Avg等Scalar Aggregate函数</h2>
<p>在上一篇<a href="https://tonybai.com/2023/07/13/a-guide-of-using-apache-arrow-for-gopher-part4/">“数据操作”</a>时提过，聚合函数分为Scalar聚合和grouped聚合，显然Scalar聚合函数要简单一些，这里我们就来向compute层添加scalar aggregate函数，以Max为例，我们希望用户层这样使用Max聚合函数：</p>
<pre><code>// max_aggregate_function.go
func main() {
    data := []int64{5, 10, 0, 25, 2, 35, 7, 15}
    bldr := array.NewInt64Builder(memory.DefaultAllocator)
    defer bldr.Release()
    bldr.AppendValues(data, nil)
    arr := bldr.NewArray()
    defer arr.Release()

    dat, err := compute.Max(context.Background(), compute.NewDatum(arr))
    if err != nil {
        fmt.Println(err)
        return
    }

    ad, ok := dat.(*compute.ArrayDatum)
    if !ok {
        fmt.Println("type assert fail")
        return
    }
    arr1 := ad.MakeArray()
    fmt.Println(arr1) // [35]
}
</code></pre>
<blockquote>
<p>注：这里有一个问题，那就是Max返回的Datum是一个ArrayDatum，而不是期望的ScalarDatum。</p>
</blockquote>
<p>通过上面的compute layer的结构，我们知道，如果要添加Max、Min、Sum、Avg等Scalar Aggregate函数，我们需要在kernel层和compute层协作实现。下面是实现的具体步骤。</p>
<h3>3.1 向kernel层添加scalar聚合函数实现</h3>
<p>compute层要支持scalar聚合，需要kernel层线支持scalar聚合，这里我们先向compute/internal/kernels目录添加一个scalar_agg.go，用于在kernel层实现scalar聚合，以Max为例：</p>
<pre><code>// compute/internal/kernels/scalar_agg.go

package kernels

import (
    "fmt"

    "github.com/apache/arrow/go/v13/arrow"
    "github.com/apache/arrow/go/v13/arrow/compute/internal/exec"
    "github.com/apache/arrow/go/v13/arrow/scalar"
)

func ScalarAggKernels(op ScalarAggOperator) (aggs []exec.ScalarKernel) {
    switch op {
    case AggMax:
        maxAggs := maxAggKernels()
        aggs = append(aggs, maxAggs...)
    case AggMin:
        minAggs := minAggKernels()
        aggs = append(aggs, minAggs...)
    case AggAvg:
        avgAggs := avgAggKernels()
        aggs = append(aggs, avgAggs...)
    case AggSum:
        sumAggs := sumAggKernels()
        aggs = append(aggs, sumAggs...)
    }

    return
}

func aggMax(ctx *exec.KernelCtx, batch *exec.ExecSpan, out *exec.ExecResult) error {
    var max int64

    for _, v := range batch.Values {
        if !v.IsArray() {
            return fmt.Errorf("%w: input datum is not array", arrow.ErrInvalid)
        }

        if v.Array.Type != arrow.PrimitiveTypes.Int64 {
            return fmt.Errorf("%w: array type is not int64", arrow.ErrInvalid)
        }

        // for int64 array:
        //   first buffer is meta buffer
        //   second buffer is what we want
        int64s := exec.GetSpanValues[int64](&amp;v.Array, 1)
        for _, v64 := range int64s {
            if v64 &gt; max {
                max = v64
            }
        }
    }

    out.FillFromScalar(scalar.NewInt64Scalar(max))
    return nil
}

func maxAggKernels() (aggs []exec.ScalarKernel) {
    outType := exec.NewOutputType(arrow.PrimitiveTypes.Int64)
    in := exec.NewExactInput(arrow.PrimitiveTypes.Int64)
    aggs = append(aggs, exec.NewScalarKernel([]exec.InputType{in}, outType,
        aggMax, nil))

    return
}
... ...
</code></pre>
<p>上面的ScalarAggKernels函数就像上图中的GetVectorHashKernels一样，为compute层提供kernel层scalar agg函数的获取“渠道”。aggMax函数是实现聚合逻辑的那个函数，它针对输入的array进行操作，计算array中所有元素中的最大值，并将这个值包装成Datum作为out参数输出。</p>
<p>在compute/internal/kernels/types.go中，我们定义了如下枚举常量，用于compute层传入要选择的scalar聚合函数。</p>
<pre><code>// compute/internal/kernels/types.go

//go:generate stringer -type=ScalarAggOperator -linecomment

type ScalarAggOperator int8

const (
    AggMax ScalarAggOperator = iota // max
    AggMin                          // min
    AggAvg                          // avg
    AggSum                          // sum
)
</code></pre>
<h3>3.2 在compute层提供对kernel层聚合函数的包装</h3>
<p>在compute层，我们也提供一个scalar_agg.go文件，用于对kernel层的聚合函数进行包装：</p>
<pre><code>// compute/scalar_agg.go

package compute

import (
    "context"

    "github.com/apache/arrow/go/v13/arrow/compute/internal/kernels"
)

type aggFunction struct {
    ScalarFunction
}

func Max(ctx context.Context, values Datum) (Datum, error) {
    return CallFunction(ctx, "max", nil, values)
}
func Min(ctx context.Context, values Datum) (Datum, error) {
    return CallFunction(ctx, "min", nil, values)
}
func Avg(ctx context.Context, values Datum) (Datum, error) {
    return CallFunction(ctx, "avg", nil, values)
}
func Sum(ctx context.Context, values Datum) (Datum, error) {
    return CallFunction(ctx, "sum", nil, values)
}

func RegisterScalarAggs(reg FunctionRegistry) {
    maxFn := &amp;aggFunction{*NewScalarFunction("max", Unary(), EmptyFuncDoc)}
    for _, k := range kernels.ScalarAggKernels(kernels.AggMax) {
        if err := maxFn.AddKernel(k); err != nil {
            panic(err)
        }
    }
    reg.AddFunction(maxFn, false)

    minFn := &amp;aggFunction{*NewScalarFunction("min", Unary(), EmptyFuncDoc)}
    for _, k := range kernels.ScalarAggKernels(kernels.AggMin) {
        if err := minFn.AddKernel(k); err != nil {
            panic(err)
        }
    }
    reg.AddFunction(minFn, false)

    avgFn := &amp;aggFunction{*NewScalarFunction("avg", Unary(), EmptyFuncDoc)}
    for _, k := range kernels.ScalarAggKernels(kernels.AggAvg) {
        if err := avgFn.AddKernel(k); err != nil {
            panic(err)
        }
    }
    reg.AddFunction(avgFn, false)

    sumFn := &amp;aggFunction{*NewScalarFunction("sum", Unary(), EmptyFuncDoc)}
    for _, k := range kernels.ScalarAggKernels(kernels.AggSum) {
        if err := sumFn.AddKernel(k); err != nil {
            panic(err)
        }
    }
    reg.AddFunction(sumFn, false)
}
</code></pre>
<p>我们看到在这个源文件中，我们提供了供最终用户调用的Max等函数，这些函数是对kernel层scalar聚合函数的包装，通过CallFunction在registry中找到注册的kernel函数并执行它。</p>
<p>RegisterScalarAggs是用于向registry注册scalar聚合函数的函数。</p>
<h3>3.3 在compute层将包装后的聚合函数注册到Registry中</h3>
<p>我们修改一下compute/registry.go，在GetFunctionRegistry函数中增加对RegisterScalarAggs的调用，以实现对scalar聚合函数的注册：</p>
<pre><code>// compute/registry.go

func GetFunctionRegistry() FunctionRegistry {
    once.Do(func() {
        registry = NewRegistry()
        RegisterScalarCast(registry)
        RegisterVectorSelection(registry)
        RegisterScalarBoolean(registry)
        RegisterScalarArithmetic(registry)
        RegisterScalarComparisons(registry)
        RegisterVectorHash(registry)
        RegisterVectorRunEndFuncs(registry)
        RegisterScalarAggs(registry)
    })
    return registry
}
</code></pre>
<h3>3.4 运行示例</h3>
<p>最初运行arrow/compute-extension/max_aggregate_function.go示例的结果并非我们预期，而是一个全0的数组：</p>
<pre><code>$go run max_aggregate_function.go
[0 0 0 0 0 0 0 0]
</code></pre>
<p>经过print调试大法后，我发现compute/executor.go中的executeSpans的实现似乎有一个问题，我在<a href="https://github.com/apache/arrow/issues/36592">arrow项目提了一个issue</a>，并对executor.go做了如下修改：</p>
<pre><code>diff --git a/go/arrow/compute/executor.go b/go/arrow/compute/executor.go
index d3f1a1fd4..e9bda7137 100644
--- a/go/arrow/compute/executor.go
+++ b/go/arrow/compute/executor.go
@@ -604,7 +604,7 @@ func (s *scalarExecutor) executeSpans(data chan&lt;- Datum) (err error) {
                        return
                }

-               return s.emitResult(prealloc, data)
+               return s.emitResult(&amp;output, data)
        }

        // fully preallocating, but not contiguously
(END)
</code></pre>
<p>修改后，再运行arrow/compute-extension/max_aggregate_function.go示例就得到了正确的结果：</p>
<pre><code>$go run max_aggregate_function.go
[35]
</code></pre>
<h3>3.5 To Be Done</h3>
<p>到这里，我们从0到1的为arrow go实现的compute层添加了int64类型的scalar聚合函数的支持(以max为例)，但这仅仅是验证了思路的可行性，上述对compute的修改可能是不合理的。此外，上述的改动不是production ready的，存在一些问题，比如：</p>
<ul>
<li>Max返回的是array datam，而不是我们想要的scalar Datam；</li>
<li>仅支持int64，不支持其他类型的max聚合，比如float64、string等；</li>
<li>性能没有优化；</li>
<li>对chunked array类型的scalar datam尚未给出验证示例。</li>
<li>&#8230; &#8230;</li>
</ul>
<h2>4. 小结</h2>
<p>在本文中我们基于C++的资料，回顾了Arrow的一些基础抽象概念，从而对Arrow有了更为深刻的认知。之后，也是我们的重点，就是给出了compute层的结构以及基于该结构为compute层增加scalar聚合函数的一种思路和示例代码。</p>
<p>不过这种思路只是为了理解arrow的一种试验性方法，存在其不合理的地方，随着arrow演进，这种方法也许将不适用。同时，后续arrow官方可能会为go增加aggregate function的支持，那时请大家以官方实现为准。</p>
<p>C++版本Arrow实现完全支持各种聚合函数，考虑到Go arrow的实现参考了C++版本的思路，如果要为go arrow正式增加聚合函数支持，阅读c++源码并考虑迁移到Go才是正道。</p>
<p>本文示例代码可以在<a href="https://github.com/bigwhite/experiments/blob/master/arrow/compute-extension">这里</a>下载，同时增加了scalar function的arrow的fork版本可以在我的github项目<a href="https://github.com/bigwhite/arrow-extend-compute1">arrow-extend-compute1</a>下找到。</p>
<h2>5. 参考资料</h2>
<ul>
<li>计算层 &#8211; https://arrow.apache.org/docs/cpp/compute.html</li>
<li>计算层教程 &#8211; https://arrow.apache.org/docs/cpp/tutorials/compute_tutorial.html</li>
<li>Arrow C++参考 &#8211; https://arrow.apache.org/docs/cpp/overview.html</li>
<li>Go unique kernel函数PR &#8211; https://github.com/apache/arrow/pull/34172</li>
</ul>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2023, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2023/07/22/a-guide-of-using-apache-arrow-for-gopher-part5/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go语言开发者的Apache Arrow使用指南：数据操作</title>
		<link>https://tonybai.com/2023/07/13/a-guide-of-using-apache-arrow-for-gopher-part4/</link>
		<comments>https://tonybai.com/2023/07/13/a-guide-of-using-apache-arrow-for-gopher-part4/#comments</comments>
		<pubDate>Thu, 13 Jul 2023 14:41:25 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Apache]]></category>
		<category><![CDATA[Array]]></category>
		<category><![CDATA[Arrow]]></category>
		<category><![CDATA[ArrowColumnarFormat]]></category>
		<category><![CDATA[bitmap]]></category>
		<category><![CDATA[buffer]]></category>
		<category><![CDATA[Builder模式]]></category>
		<category><![CDATA[ChunkedArray]]></category>
		<category><![CDATA[cookbook]]></category>
		<category><![CDATA[Cpp]]></category>
		<category><![CDATA[csv]]></category>
		<category><![CDATA[dictionary]]></category>
		<category><![CDATA[dremio]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[greptimedb]]></category>
		<category><![CDATA[InfluxDB]]></category>
		<category><![CDATA[influxdb-iox]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[Kaggle]]></category>
		<category><![CDATA[layout]]></category>
		<category><![CDATA[list]]></category>
		<category><![CDATA[metadata]]></category>
		<category><![CDATA[offset]]></category>
		<category><![CDATA[parquet]]></category>
		<category><![CDATA[primitive]]></category>
		<category><![CDATA[Record]]></category>
		<category><![CDATA[RecordBatch]]></category>
		<category><![CDATA[RPC]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[Schema]]></category>
		<category><![CDATA[semver]]></category>
		<category><![CDATA[SIMD]]></category>
		<category><![CDATA[slot]]></category>
		<category><![CDATA[sparse]]></category>
		<category><![CDATA[spec]]></category>
		<category><![CDATA[struct]]></category>
		<category><![CDATA[Table]]></category>
		<category><![CDATA[tsdb]]></category>
		<category><![CDATA[union]]></category>
		<category><![CDATA[列式存储]]></category>
		<category><![CDATA[垃圾回收]]></category>
		<category><![CDATA[引用计数]]></category>
		<category><![CDATA[数据结构]]></category>
		<category><![CDATA[时序数据库]]></category>
		<category><![CDATA[高级数据结构]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3938</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2023/07/13/a-guide-of-using-apache-arrow-for-gopher-part4 在前面的Arrow系列文章中，我们介绍了Arrow的基础数据类型以及高级数据类型，这让我们具备了在内存中建立起一个immutable数据集的能力。但这并非我们的目标，我们最终是要对建立起来的数据集进行查询和分析等操作(manipulation)的。 在这一篇文章中，我们就来看看如何基于Go arrow的实现对内存中的Arrow数据集进行操作。 注：由于Arrow官方文档尚没有Go语言的cookbook，这里的一些例子参考了其他语言的Cookbook，比如Python。 1. 从CSV文件中读取数据 在操作数据之前，我们首先需要准备数据，并将数据读取到内存中以Arrow的列式存储形式组织起来。Arrow的Go实现支持从多种文件格式中将数据读取出来并在内存中构建出Arrow列式数据集： 从图中我们看到：Arrow Go支持读取的文件格式包括CSV、JSON和Parquet等。CSV、JSON都是日常最常用的文件格式，那么Parquet是什么呢？这是一种面向列的文件存储格式，支持高效的数据存储和数据获取能力。influxdb iox存储引擎采用的就是Apache Arrow + Parquet的经典组合。我们在本系列的后续文章中会单独说一下Arrow + Parquet，在本文中Parquet不是重点。 注：Parquet的读音是：&#91;&#8217;pɑːkeɪ&#93; 。 在这篇文章中，我们以从CSV文件中读取数据为例。我们的CSV文件来自于Kaggle平台上的开放数据集，这是一份记录着Delhi这个地方(应该是印度城市德里)1996年到2017年小时级的天气数据的CSV文件：testset.csv。该文件带有列头，有20列，10w多行记录。 我们先来小试牛刀，即取该csv文件前10几行，存成名为testset.tiny.csv的文件。我们编写一段Go程序来读取CSV中的数据并在内存中建立一个Arrow Record Batch！大家还记得Arrow Record Batch是什么结构了么？我们回顾一下“高级数据结构”中的那张图你就记起来了： 接下来我们就使用Arrow Go实现提供的csv包读取testset.tiny.csv文件并输出经由读出的数据建构的Record Batch： // read_tiny_csv_multi_trunks.go package main import ( "fmt" "io" "os" "github.com/apache/arrow/go/v13/arrow/csv" ) func read(data io.ReadCloser) error { // read 5 lines at a time to create record [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part4-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2023/07/13/a-guide-of-using-apache-arrow-for-gopher-part4">本文永久链接</a> &#8211; https://tonybai.com/2023/07/13/a-guide-of-using-apache-arrow-for-gopher-part4</p>
<p>在前面的Arrow系列文章中，我们介绍了<a href="https://tonybai.com/2023/06/25/a-guide-of-using-apache-arrow-for-gopher-part1">Arrow的基础数据类型</a>以及<a href="https://tonybai.com/2023/07/08/a-guide-of-using-apache-arrow-for-gopher-part3">高级数据类型</a>，这让我们具备了在内存中建立起一个immutable数据集的能力。但这并非我们的目标，我们最终是要对建立起来的数据集进行查询和分析等操作(manipulation)的。</p>
<p>在这一篇文章中，我们就来看看如何基于Go arrow的实现对内存中的Arrow数据集进行操作。</p>
<blockquote>
<p>注：由于Arrow官方文档尚没有Go语言的cookbook，这里的一些例子参考了其他语言的Cookbook，比如<a href="https://arrow.apache.org/cookbook/py">Python</a>。</p>
</blockquote>
<h2>1. 从CSV文件中读取数据</h2>
<p>在操作数据之前，我们首先需要准备数据，并将数据读取到内存中以Arrow的列式存储形式组织起来。Arrow的Go实现支持从多种文件格式中将数据读取出来并在内存中构建出Arrow列式数据集：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part4-2.png" alt="" /></p>
<p>从图中我们看到：Arrow Go支持读取的文件格式包括CSV、JSON和<a href="https://parquet.apache.org/">Parquet</a>等。CSV、JSON都是日常最常用的文件格式，那么Parquet是什么呢？这是一种<strong>面向列的文件存储格式</strong>，支持高效的数据存储和数据获取能力。influxdb iox存储引擎采用的就是Apache Arrow + Parquet的经典组合。我们在本系列的后续文章中会单独说一下Arrow + Parquet，在本文中Parquet不是重点。</p>
<blockquote>
<p>注：Parquet的读音是：&#91;&#8217;pɑːkeɪ&#93; 。</p>
</blockquote>
<p>在这篇文章中，我们以从CSV文件中读取数据为例。我们的CSV文件来自于<a href="https://www.kaggle.com/datasets">Kaggle平台上的开放数据集</a>，这是一份记录着Delhi这个地方(应该是印度城市德里)1996年到2017年小时级的天气数据的<a href="https://www.kaggle.com/datasets/mahirkukreja/delhi-weather-data">CSV文件</a>：testset.csv。该文件带有列头，有20列，10w多行记录。</p>
<p>我们先来小试牛刀，即取该csv文件前10几行，存成名为testset.tiny.csv的文件。我们编写一段Go程序来读取CSV中的数据并在内存中建立一个Arrow Record Batch！大家还记得Arrow Record Batch是什么结构了么？我们回顾一下<a href="https://tonybai.com/2023/07/08/a-guide-of-using-apache-arrow-for-gopher-part3">“高级数据结构”</a>中的那张图你就记起来了：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part3-2.png" alt="" /></p>
<p>接下来我们就使用Arrow Go实现提供的csv包读取testset.tiny.csv文件并输出经由读出的数据建构的Record Batch：</p>
<pre><code>// read_tiny_csv_multi_trunks.go

package main

import (
    "fmt"
    "io"
    "os"

    "github.com/apache/arrow/go/v13/arrow/csv"
)

func read(data io.ReadCloser) error {
    // read 5 lines at a time to create record batches
    rdr := csv.NewInferringReader(data, csv.WithChunk(5),
        // strings can be null, and these are the values
        // to consider as null
        csv.WithNullReader(true, "", "null", "[]"),
        // assume the first line is a header line which names the columns
        csv.WithHeader(true))

    for rdr.Next() {
        rec := rdr.Record()
        fmt.Println(rec)
    }

    return nil
}

func main() {
    data, err := os.Open("./testset.tiny.csv")
    if err != nil {
        panic(err)
    }
    read(data)
}
</code></pre>
<p>这里的csv包可不是标准库中的那个包，而是Arrow Go实现中专门用于将csv文件数据读取并转换为Arrow内存对象的包。csv包提供了两个创建csv.Reader实例的函数，这里使用的是NewInferringReader(即带列类型推导的Reader)。该函数可以自动读取位于第一行的csv文件的header，获取列字段的名称与个数，形成Record的schema，并在读取下一条记录时尝试推导(infer)这一列的类型(data type)。</p>
<p>这里在调用NewInferringReader时还传入了一个功能选项开关WithChunk(5)，即一次读取5条记录来构建一个新的Record Batch。</p>
<p>我们运行一下上面的代码：</p>
<pre><code>$go run read_tiny_csv_multi_trunks.go
record:
  schema:
  fields: 20
    - datetime_utc: type=utf8, nullable
    -  _conds: type=utf8, nullable
    -  _dewptm: type=int64, nullable
    -  _fog: type=int64, nullable
    -  _hail: type=int64, nullable
    -  _heatindexm: type=utf8, nullable
    -  _hum: type=int64, nullable
    -  _precipm: type=utf8, nullable
    -  _pressurem: type=int64, nullable
    -  _rain: type=int64, nullable
    -  _snow: type=int64, nullable
    -  _tempm: type=int64, nullable
    -  _thunder: type=int64, nullable
    -  _tornado: type=int64, nullable
    -  _vism: type=int64, nullable
    -  _wdird: type=int64, nullable
    -  _wdire: type=utf8, nullable
    -  _wgustm: type=utf8, nullable
    -  _windchillm: type=utf8, nullable
    -  _wspdm: type=float64, nullable
  rows: 5
  col[0][datetime_utc]: ["19961101-11:00" "19961101-12:00" "19961101-13:00" "19961101-14:00" "19961101-16:00"]
  col[1][ _conds]: ["Smoke" "Smoke" "Smoke" "Smoke" "Smoke"]
  col[2][ _dewptm]: [9 10 11 10 11]
  col[3][ _fog]: [0 0 0 0 0]
  col[4][ _hail]: [0 0 0 0 0]
  col[5][ _heatindexm]: [(null) (null) (null) (null) (null)]
  col[6][ _hum]: [27 32 44 41 47]
  col[7][ _precipm]: [(null) (null) (null) (null) (null)]
  col[8][ _pressurem]: [1010 -9999 -9999 1010 1011]
  col[9][ _rain]: [0 0 0 0 0]
  col[10][ _snow]: [0 0 0 0 0]
  col[11][ _tempm]: [30 28 24 24 23]
  col[12][ _thunder]: [0 0 0 0 0]
  col[13][ _tornado]: [0 0 0 0 0]
  col[14][ _vism]: [5 (null) (null) 2 (null)]
  col[15][ _wdird]: [280 0 0 0 0]
  col[16][ _wdire]: ["West" "North" "North" "North" "North"]
  col[17][ _wgustm]: [(null) (null) (null) (null) (null)]
  col[18][ _windchillm]: [(null) (null) (null) (null) (null)]
  col[19][ _wspdm]: [7.4 (null) (null) (null) 0]
</code></pre>
<p>我们看到结果输出了将csv文件中数据读取并转换后的Record Batch的信息！</p>
<p>不过这个结果有一个问题，那就是我们的testset.tiny.csv有12行数据，上述结果<strong>为什么仅读出了5行呢</strong>？利用<a href="https://tonybai.com/2021/11/12/go-workspace-mode-in-go-1-18">go.work</a>引用本地下载的arrow代码做一下print调试后发现这样的一个错误：</p>
<pre><code>strconv.ParseInt: parsing "1.2": invalid syntax
</code></pre>
<p>翻看一下testset.tiny.csv文件，在第五行发现了包含1.2的这条数据：</p>
<pre><code>19961101-16:00,Smoke,11,0,0,,47,,1011,0,0,23,0,0,1.2,0,North,,,0
</code></pre>
<p>1.2这数据对应的是” &#95;vism”这一列，我们看一下上面这一列的schema信息：</p>
<pre><code>-  _vism: type=int64, nullable
</code></pre>
<p>我们看到NewInferringReader将这一列识别成int64类型了！NewInferringReader是根据第一行数据中来做类型推导的，而vism这一列的第一条数据恰为5，将其推导为int64也就不足为奇了。那么如何修正上述问题呢？NewInferringReader提供了一个WithColumnTypes的功能选项，通过它我们可以指定vism列的Arrow DataType：</p>
<pre><code>    rdr := csv.NewInferringReader(data, csv.WithChunk(5),
        // strings can be null, and these are the values
        // to consider as null
        csv.WithNullReader(true, "", "null", "[]"),
        // assume the first line is a header line which names the columns
        csv.WithHeader(true),
        csv.WithColumnTypes(map[string]arrow.DataType{
            " _vism": arrow.PrimitiveTypes.Float64,
        }),
    )
</code></pre>
<p>修改后，我们再来运行一下read_tiny_csv_multi_trunks.go这个文件的代码：</p>
<pre><code>$go run read_tiny_csv_multi_trunks.go
record:
  schema:
  fields: 20
    - datetime_utc: type=utf8, nullable
    -  _conds: type=utf8, nullable
    -  _dewptm: type=int64, nullable
    -  _fog: type=int64, nullable
    -  _hail: type=int64, nullable
    -  _heatindexm: type=utf8, nullable
    -  _hum: type=int64, nullable
    -  _precipm: type=utf8, nullable
    -  _pressurem: type=int64, nullable
    -  _rain: type=int64, nullable
    -  _snow: type=int64, nullable
    -  _tempm: type=int64, nullable
    -  _thunder: type=int64, nullable
    -  _tornado: type=int64, nullable
    -  _vism: type=float64, nullable
    -  _wdird: type=int64, nullable
    -  _wdire: type=utf8, nullable
    -  _wgustm: type=utf8, nullable
    -  _windchillm: type=utf8, nullable
    -  _wspdm: type=float64, nullable
  rows: 5
  col[0][datetime_utc]: ["19961101-11:00" "19961101-12:00" "19961101-13:00" "19961101-14:00" "19961101-16:00"]
  col[1][ _conds]: ["Smoke" "Smoke" "Smoke" "Smoke" "Smoke"]
  col[2][ _dewptm]: [9 10 11 10 11]
  col[3][ _fog]: [0 0 0 0 0]
  col[4][ _hail]: [0 0 0 0 0]
  col[5][ _heatindexm]: [(null) (null) (null) (null) (null)]
  col[6][ _hum]: [27 32 44 41 47]
  col[7][ _precipm]: [(null) (null) (null) (null) (null)]
  col[8][ _pressurem]: [1010 -9999 -9999 1010 1011]
  col[9][ _rain]: [0 0 0 0 0]
  col[10][ _snow]: [0 0 0 0 0]
  col[11][ _tempm]: [30 28 24 24 23]
  col[12][ _thunder]: [0 0 0 0 0]
  col[13][ _tornado]: [0 0 0 0 0]
  col[14][ _vism]: [5 (null) (null) 2 1.2]
  col[15][ _wdird]: [280 0 0 0 0]
  col[16][ _wdire]: ["West" "North" "North" "North" "North"]
  col[17][ _wgustm]: [(null) (null) (null) (null) (null)]
  col[18][ _windchillm]: [(null) (null) (null) (null) (null)]
  col[19][ _wspdm]: [7.4 (null) (null) (null) 0]

record:
  schema:
  fields: 20
    - datetime_utc: type=utf8, nullable
    -  _conds: type=utf8, nullable
    -  _dewptm: type=int64, nullable
    -  _fog: type=int64, nullable
    -  _hail: type=int64, nullable
    -  _heatindexm: type=utf8, nullable
    -  _hum: type=int64, nullable
    -  _precipm: type=utf8, nullable
    -  _pressurem: type=int64, nullable
    -  _rain: type=int64, nullable
    -  _snow: type=int64, nullable
    -  _tempm: type=int64, nullable
    -  _thunder: type=int64, nullable
    -  _tornado: type=int64, nullable
    -  _vism: type=float64, nullable
    -  _wdird: type=int64, nullable
    -  _wdire: type=utf8, nullable
    -  _wgustm: type=utf8, nullable
    -  _windchillm: type=utf8, nullable
    -  _wspdm: type=float64, nullable
  rows: 5
  col[0][datetime_utc]: ["19961101-17:00" "19961101-18:00" "19961101-19:00" "19961101-20:00" "19961101-21:00"]
  col[1][ _conds]: ["Smoke" "Smoke" "Smoke" "Smoke" "Smoke"]
  col[2][ _dewptm]: [12 13 13 13 13]
  col[3][ _fog]: [0 0 0 0 0]
  col[4][ _hail]: [0 0 0 0 0]
  col[5][ _heatindexm]: [(null) (null) (null) (null) (null)]
  col[6][ _hum]: [56 60 60 68 68]
  col[7][ _precipm]: [(null) (null) (null) (null) (null)]
  col[8][ _pressurem]: [1011 1010 -9999 -9999 1010]
  col[9][ _rain]: [0 0 0 0 0]
  col[10][ _snow]: [0 0 0 0 0]
  col[11][ _tempm]: [21 21 21 19 19]
  col[12][ _thunder]: [0 0 0 0 0]
  col[13][ _tornado]: [0 0 0 0 0]
  col[14][ _vism]: [(null) 0.8 (null) (null) (null)]
  col[15][ _wdird]: [0 0 0 0 0]
  col[16][ _wdire]: ["North" "North" "North" "North" "North"]
  col[17][ _wgustm]: [(null) (null) (null) (null) (null)]
  col[18][ _windchillm]: [(null) (null) (null) (null) (null)]
  col[19][ _wspdm]: [(null) 0 (null) (null) (null)]

record:
  schema:
  fields: 20
    - datetime_utc: type=utf8, nullable
    -  _conds: type=utf8, nullable
    -  _dewptm: type=int64, nullable
    -  _fog: type=int64, nullable
    -  _hail: type=int64, nullable
    -  _heatindexm: type=utf8, nullable
    -  _hum: type=int64, nullable
    -  _precipm: type=utf8, nullable
    -  _pressurem: type=int64, nullable
    -  _rain: type=int64, nullable
    -  _snow: type=int64, nullable
    -  _tempm: type=int64, nullable
    -  _thunder: type=int64, nullable
    -  _tornado: type=int64, nullable
    -  _vism: type=float64, nullable
    -  _wdird: type=int64, nullable
    -  _wdire: type=utf8, nullable
    -  _wgustm: type=utf8, nullable
    -  _windchillm: type=utf8, nullable
    -  _wspdm: type=float64, nullable
  rows: 2
  col[0][datetime_utc]: ["19961101-22:00" "19961101-23:00"]
  col[1][ _conds]: ["Smoke" "Smoke"]
  col[2][ _dewptm]: [13 12]
  col[3][ _fog]: [0 0]
  col[4][ _hail]: [0 0]
  col[5][ _heatindexm]: [(null) (null)]
  col[6][ _hum]: [68 64]
  col[7][ _precipm]: [(null) (null)]
  col[8][ _pressurem]: [1009 1009]
  col[9][ _rain]: [0 0]
  col[10][ _snow]: [0 0]
  col[11][ _tempm]: [19 19]
  col[12][ _thunder]: [0 0]
  col[13][ _tornado]: [0 0]
  col[14][ _vism]: [(null) (null)]
  col[15][ _wdird]: [0 0]
  col[16][ _wdire]: ["North" "North"]
  col[17][ _wgustm]: [(null) (null)]
  col[18][ _windchillm]: [(null) (null)]
  col[19][ _wspdm]: [(null) (null)]
</code></pre>
<p>这次12行数据都被成功读取出来了！</p>
<p>接下来，我们再来读取一下完整数据集testset.csv，我们通过输出读取的数据集行数来判断一下读取是否完全成功：</p>
<pre><code>// read_csv_rows_count.go

func read(data io.ReadCloser) error {
    var total int64
    // read 10000 lines at a time to create record batches
    rdr := csv.NewInferringReader(data, csv.WithChunk(10000),
        // strings can be null, and these are the values
        // to consider as null
        csv.WithNullReader(true, "", "null", "[]"),
        // assume the first line is a header line which names the columns
        csv.WithHeader(true),
        csv.WithColumnTypes(map[string]arrow.DataType{
            " _vism": arrow.PrimitiveTypes.Float64,
        }),
    )

    for rdr.Next() {
        rec := rdr.Record()
        total += rec.NumRows()
    }

    fmt.Println("total columns =", total)
    return nil
}
</code></pre>
<p>我们开着错误输出的调试语句，看看上面的代码的输出结果：</p>
<pre><code>======nextn: strconv.ParseInt: parsing "N/A": invalid syntax
total columns = 10000
</code></pre>
<p>我们看到上述程序仅读取了1w条记录，并输出了一个错误信息：CSV文件中包含“N/A”字样的数据，导致CSV Reader读取失败。经过数据对比核查，发现hum的数据存在大量“N/A”，另外pressurem的类型也有问题。那么如何解决这个问题呢？NewInferringReader提供了WithIncludeColumns功能选项可以供我们提供我们想要的列，这样我们可以给出一个列白名单，将hum列排除在外。修改后的read代码如下：</p>
<pre><code>// read_csv_rows_count_with_col_filter.go

func read(data io.ReadCloser) error {
    var total int64
    // read 10000 lines at a time to create record batches
    rdr := csv.NewInferringReader(data, csv.WithChunk(10000),
        // strings can be null, and these are the values
        // to consider as null
        csv.WithNullReader(true, "", "null", "[]"),
        // assume the first line is a header line which names the columns
        csv.WithHeader(true),
        csv.WithColumnTypes(map[string]arrow.DataType{
            " _pressurem": arrow.PrimitiveTypes.Float64,
        }),
        csv.WithIncludeColumns([]string{
            "datetime_utc", // 19961101-11:00
            " _conds",      // Smoke、Haze
            " _fog",        // 0
            " _heatindexm",
            " _pressurem", //
            " _rain",      //
            " _snow",      //
            " _tempm",     //
            " _thunder",   //
            " _tornado",   //
        }),
    )   

    for rdr.Next() {
        rec := rdr.Record()
        total += rec.NumRows()
    }   

    fmt.Println("total columns =", total)
    return nil
}
</code></pre>
<p>运行修改后的代码：</p>
<pre><code>$go run read_csv_rows_count_with_col_filter.go
total columns = 100990
</code></pre>
<p>我们顺利将CSV中的数据读到了内存中，并组织成了多个Record Batch。</p>
<h2>2. Arrow compute API介绍</h2>
<p>一旦内存中有了Arrow格式的数据后，我们就可以基于这份数据进行数据操作了，比如过滤、查询、计算、转换等等。那么是否需要开发人员自己根据对Arrow type的结构的理解来实现针对这些数据操作的算法呢？<strong>不用的</strong>！</p>
<p>Arrow社区提供了<a href="https://arrow.apache.org/docs/cpp/api/compute.html">compute API</a>以及各种语言的高性能实现以供基于Arrow格式进行数据操作的开发人员直接复用。</p>
<p>Go Arrow实现也提供了compute包用于操作内存中的Arrow object。不过根据compute包的注释来看，目前Go compute包还属于实验性质，并非stable的API，将来可能有变：</p>
<pre><code>// The overwhemling majority of things in this package require go1.18 as
// it utilizes generics. The files in this package and its sub-packages
// are all excluded from being built by go versions lower than 1.18 so
// that the larger Arrow module itself is still compatible with go1.17.
//
// Everything in this package should be considered Experimental for now.
package compute
</code></pre>
<p>另外我们从上面注释也可以看到，compute包用到了泛型，因此需要Go 1.18及以后版本才能使用。</p>
<p>为了更好地理解compute API，我们需要知道一些有关compute的概念，首先了解一下Datum。</p>
<h3>2.1 Datum</h3>
<p>compute API中的函数需要支持多种类型数据作为输入，可以是arrow的array type，也可以是一个标量值(scalar)，为了统一输出表示，compute API建立了一个名为Datum的抽象。Datum可以理解为一个compute API函数可以接受的各种arrow类型的union类型，union中既可以是一个scalar(标量值），也可以是Array、Chunked Array，甚至是一整个Record Batch或一个Arrow Table。</p>
<p>不出预料，Go中采用接口来建立Datum这个抽象：</p>
<pre><code>// Datum is a variant interface for wrapping the various Arrow data structures
// for now the various Datum types just hold a Value which is the type they
// are wrapping, but it might make sense in the future for those types
// to actually be aliases or embed their types instead. Not sure yet.
type Datum interface {
    fmt.Stringer
    Kind() DatumKind
    Len() int64
    Equals(Datum) bool
    Release()

    data() any
}
</code></pre>
<p>Datum支持的类型通过DatumKind的常量枚举值可以看出：</p>
<pre><code>// DatumKind is an enum used for denoting which kind of type a datum is encapsulating
type DatumKind int

const (
    KindNone    DatumKind = iota // none
    KindScalar                   // scalar
    KindArray                    // array
    KindChunked                  // chunked_array
    KindRecord                   // record_batch
    KindTable                    // table
)
</code></pre>
<h3>2.2 Function Type</h3>
<p>compute包提供的是协助数据操作和分析的函数，这些函数可以被分为几类，我们由简单到复制的顺序逐一看一下：</p>
<h4>2.2.1 标量(scalar)函数或逐元素(element-wise)函数</h4>
<p>这类函数接受一个scalar参数或一个array类型的datum参数，函数会对输入参数中的逐个元素进行操作，比如求反、求绝对值等。如果传入的是scalar，则返回scalar，如果传入的是array类型，则返回array类型。传入和返回的array长度应相同。</p>
<p>下图(来自<a href="https://book.douban.com/subject/35954154/">《In-Memory Analytics with Apache Arrow》</a>一书)直观地解释了这类函数的操作特性：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part4-3.png" alt="" /></p>
<p>我们用go代码实现一下上图中的两个示例，先来看unary element-wise操作的例子：</p>
<pre><code>// unary_elementwise_function.go

func main() {
    data := []int32{5, 10, 0, 25, 2}
    bldr := array.NewInt32Builder(memory.DefaultAllocator)
    defer bldr.Release()
    bldr.AppendValues(data, nil)
    arr := bldr.NewArray()
    defer arr.Release()

    dat, err := compute.Negate(context.Background(), compute.ArithmeticOptions{}, compute.NewDatum(arr))
    if err != nil {
        fmt.Println(err)
        return
    }

    arr1, ok := dat.(*compute.ArrayDatum)
    if !ok {
        fmt.Println("type assert fail")
        return
    }
    fmt.Println(arr1.MakeArray()) // [-5 -10 0 -25 -2]
}
</code></pre>
<p>compute包实现了常见的一元和二元arithmetic函数：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part4-4.png" alt="" /></p>
<p>下面是二元Add操作的示例：</p>
<pre><code>// binary_elementwise_function.go

func main() {
    data1 := []int32{5, 10, 0, 25, 2}
    data2 := []int32{1, 5, 2, 10, 5}
    scalarData1 := int32(6)

    bldr := array.NewInt32Builder(memory.DefaultAllocator)
    defer bldr.Release()
    bldr.AppendValues(data1, nil)
    arr1 := bldr.NewArray()
    defer arr1.Release()

    bldr.AppendValues(data2, nil)
    arr2 := bldr.NewArray()
    defer arr2.Release()

    result1, err := compute.Add(context.Background(), compute.ArithmeticOptions{},
        compute.NewDatum(arr1),
        compute.NewDatum(arr2))
    if err != nil {
        fmt.Println(err)
        return
    }

    result2, err := compute.Add(context.Background(), compute.ArithmeticOptions{},
        compute.NewDatum(arr1),
        compute.NewDatum(scalarData1))
    if err != nil {
        fmt.Println(err)
        return
    }

    resultArr1, ok := result1.(*compute.ArrayDatum)
    if !ok {
        fmt.Println("type assert fail")
        return
    }
    fmt.Println(resultArr1.MakeArray()) // [6 15 2 35 7]

    resultArr2, ok := result2.(*compute.ArrayDatum)
    if !ok {
        fmt.Println("type assert fail")
        return
    }
    fmt.Println(resultArr2.MakeArray()) // [11 16 6 31 8]
}
</code></pre>
<p>在这个示例里，我们实现了array + array和array + scalar两个操作，两个加法操作的结果都是一个新array。</p>
<p>接下来我们来看</p>
<h4>2.2.2 array-wise(逐array)函数</h4>
<p>这一类的函数使用整个array进行操作，经常进行转换或输出与输入array不同长度的结果。下图(来自<a href="https://book.douban.com/subject/35954154/">《In-Memory Analytics with Apache Arrow》</a>一书)直观地解释了这类函数的操作特性：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part4-5.png" alt="" /></p>
<p>Go compute包没有提供sort_unique函数，这里用Unique模拟一个unary array-wise操作，代码如下：</p>
<pre><code>// unary_arraywise_function.go

func main() {
    data := []int32{5, 10, 0, 25, 2, 10, 2, 25}
    bldr := array.NewInt32Builder(memory.DefaultAllocator)
    defer bldr.Release()
    bldr.AppendValues(data, nil)
    arr := bldr.NewArray()
    defer arr.Release()

    dat, err := compute.Unique(context.Background(), compute.NewDatum(arr))
    if err != nil {
        fmt.Println(err)
        return
    }

    arr1, ok := dat.(*compute.ArrayDatum)
    if !ok {
        fmt.Println("type assert fail")
        return
    }
    fmt.Println(arr1.MakeArray()) // [5 10 0 25 2]
}
</code></pre>
<p>而上图中的二元array-wise Filter操作可以由下面代码实现：</p>
<pre><code>// binary_arraywise_function.go

func main() {
    data := []int32{5, 10, 0, 25, 2}
    filterMask := []bool{true, false, true, false, true}

    bldr := array.NewInt32Builder(memory.DefaultAllocator)
    defer bldr.Release()
    bldr.AppendValues(data, nil)
    arr := bldr.NewArray()
    defer arr.Release()

    bldr1 := array.NewBooleanBuilder(memory.DefaultAllocator)
    defer bldr1.Release()
    bldr1.AppendValues(filterMask, nil)
    filterArr := bldr1.NewArray()
    defer filterArr.Release()

    dat, err := compute.Filter(context.Background(), compute.NewDatum(arr),
        compute.NewDatum(filterArr),
        compute.FilterOptions{})
    if err != nil {
        fmt.Println(err)
        return
    }

    arr1, ok := dat.(*compute.ArrayDatum)
    if !ok {
        fmt.Println("type assert fail")
        return
    }
    fmt.Println(arr1.MakeArray()) // [5 0 2]
}
</code></pre>
<p>注意：compute.Filter函数要求传入的value datum和filter datum的底层array长度要相同。</p>
<h4>2.2.3 聚合(Aggregation)函数</h4>
<p>Arrow compute支持两类聚合函数，一类是标量聚合(scalar aggregation)，它的操作对象为一个array或一个标量，计算后输出一个标量值，常见的例子包括：count、min、max、mean、avg、sum等聚合计算；另外一类则是分组聚合(grouped aggregation)，即先按某些“key”列进行分组后，再分别聚合，有些类似SQL中的group by操作。下图(来自<a href="https://book.douban.com/subject/35954154/">《In-Memory Analytics with Apache Arrow》</a>一书)直观地解释了这两类函数的操作特性：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part4-6.png" alt="" /></p>
<p>不过遗憾的是<a href="https://github.com/apache/arrow/issues/32545">Go尚未提供对这类聚合函数的直接支持</a>。</p>
<p>要想实现上述十分有用的聚合数据操作，在官方尚未提供支持之前，我们可以考虑自行扩展compute包。</p>
<blockquote>
<p>注：相对完整的标量聚合和分组聚合的函数列表，可以<a href="https://arrow.apache.org/docs/cpp/compute.html#aggregations">参考C++版本的API ref</a>。</p>
</blockquote>
<h2>3. 小结</h2>
<p>鉴于本篇篇幅以及Go对聚合函数的尚未支持，计划中对Delhi CSV文件的聚合分析只能留到后面系列文章了。</p>
<p>简单回顾一下本文内容。我们介绍了Go Arrow实现从CSV文件读取数据的方法以及一些技巧，然后我们介绍了Arrow除了其format之外的一个重点内容：compute API，这为基于arrow的array数据进行数据操作提供了开箱即用和高性能的API，大家要理解其中Datum的抽象概念，以及各类Function的操作对象和返回结果类型。</p>
<blockquote>
<p>注：本文涉及的源代码在<a href="https://github.com/bigwhite/experiments/blob/master/arrow/manipulation">这里</a>可以下载。</p>
</blockquote>
<h2>4. 参考资料</h2>
<ul>
<li><a href="https://voltrondata.com/resources/make-data-files-easier-to-work-with-golang-arrow">Make Data Files Easier to Work With Using Golang and Apache Arrow</a> &#8211; https://voltrondata.com/resources/make-data-files-easier-to-work-with-golang-arrow</li>
<li>《In-Memory Analytics with Apache Arrow》- https://book.douban.com/subject/35954154/</li>
<li>C++ compute API &#8211; https://arrow.apache.org/docs/cpp/compute.html</li>
<li>C++ Acero高级API &#8211; https://arrow.apache.org/docs/cpp/streaming_execution.html</li>
</ul>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2023, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2023/07/13/a-guide-of-using-apache-arrow-for-gopher-part4/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go语言开发者的Apache Arrow使用指南：高级数据结构</title>
		<link>https://tonybai.com/2023/07/08/a-guide-of-using-apache-arrow-for-gopher-part3/</link>
		<comments>https://tonybai.com/2023/07/08/a-guide-of-using-apache-arrow-for-gopher-part3/#comments</comments>
		<pubDate>Sat, 08 Jul 2023 15:27:54 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Apache]]></category>
		<category><![CDATA[Array]]></category>
		<category><![CDATA[Arrow]]></category>
		<category><![CDATA[ArrowColumnarFormat]]></category>
		<category><![CDATA[bitmap]]></category>
		<category><![CDATA[buffer]]></category>
		<category><![CDATA[Builder模式]]></category>
		<category><![CDATA[ChunkedArray]]></category>
		<category><![CDATA[Cpp]]></category>
		<category><![CDATA[dictionary]]></category>
		<category><![CDATA[dremio]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[greptimedb]]></category>
		<category><![CDATA[InfluxDB]]></category>
		<category><![CDATA[influxdb-iox]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[layout]]></category>
		<category><![CDATA[list]]></category>
		<category><![CDATA[metadata]]></category>
		<category><![CDATA[offset]]></category>
		<category><![CDATA[parquet]]></category>
		<category><![CDATA[primitive]]></category>
		<category><![CDATA[Record]]></category>
		<category><![CDATA[RecordBatch]]></category>
		<category><![CDATA[RPC]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[Schema]]></category>
		<category><![CDATA[semver]]></category>
		<category><![CDATA[SIMD]]></category>
		<category><![CDATA[slot]]></category>
		<category><![CDATA[sparse]]></category>
		<category><![CDATA[spec]]></category>
		<category><![CDATA[struct]]></category>
		<category><![CDATA[Table]]></category>
		<category><![CDATA[tsdb]]></category>
		<category><![CDATA[union]]></category>
		<category><![CDATA[列式存储]]></category>
		<category><![CDATA[垃圾回收]]></category>
		<category><![CDATA[引用计数]]></category>
		<category><![CDATA[数据结构]]></category>
		<category><![CDATA[时序数据库]]></category>
		<category><![CDATA[高级数据结构]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3933</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2023/07/08/a-guide-of-using-apache-arrow-for-gopher-part3 经过对前面两篇文章《Arrow数据类型》和《Arrow Go实现的内存管理》的学习，我们知道了各种Arrow array type以及它们在内存中的layout，我们了解了Go arrow实现在内存管理上的一些机制和使用原则。 Arrow的array type只是一个定长的、同类型的值序列。在实际应用中，array type更多时候只是充当基础类型，我们需要具有组合基础类型能力的更高级的数据结构。在这一篇文章中，我们就来看看Arrow规范以及一些实现中提供的高级数据结构，包括Record Batch、Chunked Array以及Table。 我们先来看看Record Batch。 1. Record Batch Record这个名字让我想起了Pascal编程语言中的Record。在Pascal中，Record的角色大致与Go中的Struct类似，也是一组异构字段的集合。下面是《In-Memory Analytics with Apache Arrow》书中的一个Record例子： // 以Go语言呈现 type Archer struct { archer string location string year int16 } Record Batch则顾名思义，是一批Record，即一个Record的集合：&#91;N&#93;Archer。 如果将Record的各个字段作为列，将集合中的每个Record作为行，我们能得到如下面示意图中的结构： Go Arrow实现中没有直接使用“Record Batch”这个名字，而是使用了“Record”，这个“Record”实际代表的就是Record Batch。下面是Go Arrow实现定义的Record接口： // github.com/apache/arrow/go/arrow/record.go // Record is a collection of equal-length arrays matching [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part3-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2023/07/08/a-guide-of-using-apache-arrow-for-gopher-part3">本文永久链接</a> &#8211; https://tonybai.com/2023/07/08/a-guide-of-using-apache-arrow-for-gopher-part3</p>
<p>经过对前面两篇文章<a href="https://tonybai.com/2023/06/25/a-guide-of-using-apache-arrow-for-gopher-part1">《Arrow数据类型》</a>和<a href="https://tonybai.com/2023/06/30/a-guide-of-using-apache-arrow-for-gopher-part2">《Arrow Go实现的内存管理》</a>的学习，我们知道了各种Arrow array type以及它们在内存中的layout，我们了解了Go arrow实现在内存管理上的一些机制和使用原则。</p>
<p>Arrow的array type只是一个定长的、同类型的值序列。在实际应用中，array type更多时候只是充当基础类型，<strong>我们需要具有组合基础类型能力的更高级的数据结构</strong>。在这一篇文章中，我们就来看看Arrow规范以及一些实现中提供的高级数据结构，包括Record Batch、Chunked Array以及Table。</p>
<p>我们先来看看<a href="https://arrow.apache.org/docs/format/Glossary.html#term-record-batch">Record Batch</a>。</p>
<h2>1. Record Batch</h2>
<p>Record这个名字让我想起了<a href="https://en.wikipedia.org/wiki/Pascal_(programming_language)">Pascal编程语言</a>中的Record。在Pascal中，Record的角色大致与Go中的Struct类似，也是一组异构字段的集合。下面是<a href="https://book.douban.com/subject/35954154/">《In-Memory Analytics with Apache Arrow》</a>书中的一个Record例子：</p>
<pre><code>// 以Go语言呈现
type Archer struct {
    archer string
    location string
    year int16
}
</code></pre>
<p>Record Batch则顾名思义，是<strong>一批Record</strong>，即一个Record的集合：&#91;N&#93;Archer。</p>
<p>如果将Record的各个字段作为列，将集合中的每个Record作为行，我们能得到如下面示意图中的结构：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part3-2.png" alt="" /></p>
<p>Go Arrow实现中没有直接使用“Record Batch”这个名字，而是使用了“Record”，这个“Record”实际代表的就是Record Batch。下面是Go Arrow实现定义的Record接口：</p>
<pre><code>// github.com/apache/arrow/go/arrow/record.go

// Record is a collection of equal-length arrays matching a particular Schema.
// Also known as a RecordBatch in the spec and in some implementations.
//
// It is also possible to construct a Table from a collection of Records that
// all have the same schema.
type Record interface {
    json.Marshaler

    Release()
    Retain()

    Schema() *Schema

    NumRows() int64
    NumCols() int64

    Columns() []Array
    Column(i int) Array
    ColumnName(i int) string
    SetColumn(i int, col Array) (Record, error)

    // NewSlice constructs a zero-copy slice of the record with the indicated
    // indices i and j, corresponding to array[i:j].
    // The returned record must be Release()'d after use.
    //
    // NewSlice panics if the slice is outside the valid range of the record array.
    // NewSlice panics if j &lt; i.
    NewSlice(i, j int64) Record
}
</code></pre>
<p>我们依然可以使用<a href="">Builder模式</a>来创建一个arrow.Record，下面我们就来用Go代码创建&#91;N&#93;Archer这个Record Batch：</p>
<pre><code>// record_batch.go
func main() {
    schema := arrow.NewSchema(
        []arrow.Field{
            {Name: "archer", Type: arrow.BinaryTypes.String},
            {Name: "location", Type: arrow.BinaryTypes.String},
            {Name: "year", Type: arrow.PrimitiveTypes.Int16},
        },
        nil,
    )

    rb := array.NewRecordBuilder(memory.DefaultAllocator, schema)
    defer rb.Release()

    rb.Field(0).(*array.StringBuilder).AppendValues([]string{"tony", "amy", "jim"}, nil)
    rb.Field(1).(*array.StringBuilder).AppendValues([]string{"beijing", "shanghai", "chengdu"}, nil)
    rb.Field(2).(*array.Int16Builder).AppendValues([]int16{1992, 1993, 1994}, nil)

    rec := rb.NewRecord()
    defer rec.Release()

    fmt.Println(rec)
}
</code></pre>
<p>运行上述示例，输出如下：</p>
<pre><code>$go run record_batch.go
record:
  schema:
  fields: 3
    - archer: type=utf8
    - location: type=utf8
    - year: type=int16
  rows: 3
  col[0][archer]: ["tony" "amy" "jim"]
  col[1][location]: ["beijing" "shanghai" "chengdu"]
  col[2][year]: [1992 1993 1994]
</code></pre>
<p>在这个示例里，我们看到了一个名为Schema的概念，并且NewRecordBuilder创建时需要传入一个arrow.Schema的实例。和数据库表Schema类似，Arrow中的Schema也是一个元数据概念，它包含一系列作为“列”的字段的名称和类型信息。Schema不仅在Record Batch中使用，在后面的Table中，Schema也是必要元素。</p>
<p>arrow.Record可以通过NewSlice可以ZeroCopy方式共享Record Batch的内存数据，NewSlice会创建一个新的Record Batch，这个Record Batch中的Record与原Record是共享的：</p>
<pre><code>// record_batch_slice.go

sl := rec.NewSlice(0, 2)
fmt.Println(sl)
cols := sl.Columns()
a1 := cols[0]
fmt.Println(a1)
</code></pre>
<p>新的sl取了rec的前两个record，输出sl得到如下结果：</p>
<pre><code>record:
  schema:
  fields: 3
    - archer: type=utf8
    - location: type=utf8
    - year: type=int16
  rows: 2
  col[0][archer]: ["tony" "amy"]
  col[1][location]: ["beijing" "shanghai"]
  col[2][year]: [1992 1993]

["tony" "amy"]
</code></pre>
<p>相同schema的record batch可以合并，我们只需要分配一个更大的Record Batch，然后将两个待合并的Record batch copy到新Record Batch中就可以了，但显然这样做的开销很大。</p>
<p>Arrow的一些实现中提供了Chunked Array的概念，可以更低开销的来完成某个列的array的追加。</p>
<blockquote>
<p>注：Chunked array并不是Arrow Columnar Format的一部分。</p>
</blockquote>
<h2>2. Chunked Array</h2>
<p>如果说Record Batch本质上是不同Array type的横向聚合，那么Chunked Array就是<strong>相同Array type的纵向聚合</strong>了，用Go语法表示就是：&#91;N&#93;Array或&#91;&#93;Array，即array of array。下面是一个Chunked Array的结构示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part3-3.png" alt="" /></p>
<p>我们看到：Go的Chunked array的实现使用的是一个Array切片：</p>
<pre><code>// github.com/apache/arrow/go/arrow/table.go

// Chunked manages a collection of primitives arrays as one logical large array.
type Chunked struct {
    refCount int64 // refCount must be first in the struct for 64 bit alignment and sync/atomic (https://github.com/golang/go/issues/37262)

    chunks []Array

    length int
    nulls  int
    dtype  DataType
}
</code></pre>
<p>按照Go切片的本质，Chunked Array中的各个元素Array间的实际内存buffer并不连续。并且正如示意图所示：每个Array的长度也并非是一样的。</p>
<blockquote>
<p>注：在<a href="http://gk.link/a/10AVZ">《Go语言第一课》</a>中的第15讲中有关于切片本质的深入系统的讲解。</p>
</blockquote>
<p>我们可以使用arrow包提供的NewChunked函数创建一个Chunked Array，具体见下面源码：</p>
<pre><code>// chunked_array.go

func main() {
    ib := array.NewInt64Builder(memory.DefaultAllocator)
    defer ib.Release()

    ib.AppendValues([]int64{1, 2, 3, 4, 5}, nil)
    i1 := ib.NewInt64Array()
    defer i1.Release()

    ib.AppendValues([]int64{6, 7}, nil)
    i2 := ib.NewInt64Array()
    defer i2.Release()

    ib.AppendValues([]int64{8, 9, 10}, nil)
    i3 := ib.NewInt64Array()
    defer i3.Release()

    c := arrow.NewChunked(
        arrow.PrimitiveTypes.Int64,
        []arrow.Array{i1, i2, i3},
    )
    defer c.Release()

    for _, arr := range c.Chunks() {
        fmt.Println(arr)
    }

    fmt.Println("chunked length =", c.Len())
    fmt.Println("chunked null count=", c.NullN())
}
</code></pre>
<p>我们看到在Chunked Array聚合了多个arrow.Array实例，并且这些arrow.Array实例的长短可不一致，arrow.Chunked的Len()返回的则是Chunked中Array的长度之和。下面是示例程序的输出结果：</p>
<pre><code>$go run chunked_array.go
[1 2 3 4 5]
[6 7]
[8 9 10]
chunked length = 10
chunked null count= 0
</code></pre>
<p>这样来看，Chunked Array可以看成一个逻辑上的大Array。</p>
<p>好了，问题来了！Record Batch是用来聚合等长array type的，那么<strong>是否有某种数据结构可以用来聚合等长的Chunked Array呢</strong>？答案是有的！下面我们就来看看这种结构：Table。</p>
<h2>3. Table</h2>
<p>Table和Chunked Array一样并不属于Arrow Columnar Format的一部分，最初只是Arrow的C++实现中的一个数据结构，Go Arrow的实现也提供了对Table的支持。</p>
<p>Table的结构示意图如下(图摘自<a href="https://book.douban.com/subject/35954154/">《In-Memory Analytics with Apache Arrow》</a>一书)：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part3-4.png" alt="" /></p>
<p>我们看到：和Record Batch的每列是一个array不同，Table的每一列为一个chunked array，所有列的chunked array的Length是相同的，但各个列的chunked array中的array的长度可以不同。</p>
<p>Table和Record Batch相似的地方是都有自己的Schema。</p>
<p>下面的示意图(来自<a href="https://arrow.apache.org/docs/format/Glossary.html#term-table">这里</a>)对Table和Chunked Array做了十分直观的对比：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part3-5.png" alt="" /></p>
<p>Record Batch是Arrow Columnar format中的一部分，所有语言的实现都支持Record Batch；但Table并非format spec的一部分，并非所有语言的实现对其都提供支持。</p>
<p>另外从图中看到，由于Table采用了Chunked Array作为列，chunked array下的各个array内部分布并不连续，这让Table在运行时丧失了一些局部性。</p>
<p>下面我们就使用Go arrow实现来创建一个table，这是一个3列、10行的table：</p>
<pre><code>// table.go

func main() {
    schema := arrow.NewSchema(
        []arrow.Field{
            {Name: "col1", Type: arrow.PrimitiveTypes.Int32},
            {Name: "col2", Type: arrow.PrimitiveTypes.Float64},
            {Name: "col3", Type: arrow.BinaryTypes.String},
        },
        nil,
    )

    col1 := func() *arrow.Column {
        chunk := func() *arrow.Chunked {
            ib := array.NewInt32Builder(memory.DefaultAllocator)
            defer ib.Release()

            ib.AppendValues([]int32{1, 2, 3}, nil)
            i1 := ib.NewInt32Array()
            defer i1.Release()

            ib.AppendValues([]int32{4, 5, 6, 7, 8, 9, 10}, nil)
            i2 := ib.NewInt32Array()
            defer i2.Release()

            c := arrow.NewChunked(
                arrow.PrimitiveTypes.Int32,
                []arrow.Array{i1, i2},
            )
            return c
        }()
        defer chunk.Release()

        return arrow.NewColumn(schema.Field(0), chunk)
    }()
    defer col1.Release()

    col2 := func() *arrow.Column {
        chunk := func() *arrow.Chunked {
            fb := array.NewFloat64Builder(memory.DefaultAllocator)
            defer fb.Release()

            fb.AppendValues([]float64{1.1, 2.2, 3.3, 4.4, 5.5}, nil)
            f1 := fb.NewFloat64Array()
            defer f1.Release()

            fb.AppendValues([]float64{6.6, 7.7}, nil)
            f2 := fb.NewFloat64Array()
            defer f2.Release()

            fb.AppendValues([]float64{8.8, 9.9, 10.0}, nil)
            f3 := fb.NewFloat64Array()
            defer f3.Release()

            c := arrow.NewChunked(
                arrow.PrimitiveTypes.Float64,
                []arrow.Array{f1, f2, f3},
            )
            return c
        }()
        defer chunk.Release()

        return arrow.NewColumn(schema.Field(1), chunk)
    }()
    defer col2.Release()

    col3 := func() *arrow.Column {
        chunk := func() *arrow.Chunked {
            sb := array.NewStringBuilder(memory.DefaultAllocator)
            defer sb.Release()

            sb.AppendValues([]string{"s1", "s2"}, nil)
            s1 := sb.NewStringArray()
            defer s1.Release()

            sb.AppendValues([]string{"s3", "s4"}, nil)
            s2 := sb.NewStringArray()
            defer s2.Release()

            sb.AppendValues([]string{"s5", "s6", "s7", "s8", "s9", "s10"}, nil)
            s3 := sb.NewStringArray()
            defer s3.Release()

            c := arrow.NewChunked(
                arrow.BinaryTypes.String,
                []arrow.Array{s1, s2, s3},
            )
            return c
        }()
        defer chunk.Release()

        return arrow.NewColumn(schema.Field(2), chunk)
    }()
    defer col3.Release()

    var tbl arrow.Table
    tbl = array.NewTable(schema, []arrow.Column{*col1, *col2, *col3}, -1)
    defer tbl.Release()

    dumpTable(tbl)
}

func dumpTable(tbl arrow.Table) {
    s := tbl.Schema()
    fmt.Println(s)
    fmt.Println("------")

    fmt.Println("the count of table columns=", tbl.NumCols())
    fmt.Println("the count of table rows=", tbl.NumRows())
    fmt.Println("------")

    for i := 0; i &lt; int(tbl.NumCols()); i++ {
        col := tbl.Column(i)
        fmt.Printf("arrays in column(%s):\n", col.Name())
        chunk := col.Data()
        for _, arr := range chunk.Chunks() {
            fmt.Println(arr)
        }
        fmt.Println("------")
    }
}
</code></pre>
<p>我们看到：table创建之前，我们需要准备一个schema，以及各个column。每个column则是一个chunked array。</p>
<p>运行上述代码，我们得到如下结果：</p>
<pre><code>$go run table.go
schema:
  fields: 3
    - col1: type=int32
    - col2: type=float64
    - col3: type=utf8
------
the count of table columns= 3
the count of table rows= 10
------
arrays in column(col1):
[1 2 3]
[4 5 6 7 8 9 10]
------
arrays in column(col2):
[1.1 2.2 3.3 4.4 5.5]
[6.6 7.7]
[8.8 9.9 10]
------
arrays in column(col3):
["s1" "s2"]
["s3" "s4"]
["s5" "s6" "s7" "s8" "s9" "s10"]
------
</code></pre>
<p>table还支持schema变更，我们可以基于上述代码为table增加一列：</p>
<pre><code>// table_schema_change.go

func main() {
    schema := arrow.NewSchema(
        []arrow.Field{
            {Name: "col1", Type: arrow.PrimitiveTypes.Int32},
            {Name: "col2", Type: arrow.PrimitiveTypes.Float64},
            {Name: "col3", Type: arrow.BinaryTypes.String},
        },
        nil,
    )

    col1 := func() *arrow.Column {
        chunk := func() *arrow.Chunked {
            ib := array.NewInt32Builder(memory.DefaultAllocator)
            defer ib.Release()

            ib.AppendValues([]int32{1, 2, 3}, nil)
            i1 := ib.NewInt32Array()
            defer i1.Release()

            ib.AppendValues([]int32{4, 5, 6, 7, 8, 9, 10}, nil)
            i2 := ib.NewInt32Array()
            defer i2.Release()

            c := arrow.NewChunked(
                arrow.PrimitiveTypes.Int32,
                []arrow.Array{i1, i2},
            )
            return c
        }()
        defer chunk.Release()

        return arrow.NewColumn(schema.Field(0), chunk)
    }()
    defer col1.Release()

    col2 := func() *arrow.Column {
        chunk := func() *arrow.Chunked {
            fb := array.NewFloat64Builder(memory.DefaultAllocator)
            defer fb.Release()

            fb.AppendValues([]float64{1.1, 2.2, 3.3, 4.4, 5.5}, nil)
            f1 := fb.NewFloat64Array()
            defer f1.Release()

            fb.AppendValues([]float64{6.6, 7.7}, nil)
            f2 := fb.NewFloat64Array()
            defer f2.Release()

            fb.AppendValues([]float64{8.8, 9.9, 10.0}, nil)
            f3 := fb.NewFloat64Array()
            defer f3.Release()

            c := arrow.NewChunked(
                arrow.PrimitiveTypes.Float64,
                []arrow.Array{f1, f2, f3},
            )
            return c
        }()
        defer chunk.Release()

        return arrow.NewColumn(schema.Field(1), chunk)
    }()
    defer col2.Release()

    col3 := func() *arrow.Column {
        chunk := func() *arrow.Chunked {
            sb := array.NewStringBuilder(memory.DefaultAllocator)
            defer sb.Release()

            sb.AppendValues([]string{"s1", "s2"}, nil)
            s1 := sb.NewStringArray()
            defer s1.Release()

            sb.AppendValues([]string{"s3", "s4"}, nil)
            s2 := sb.NewStringArray()
            defer s2.Release()

            sb.AppendValues([]string{"s5", "s6", "s7", "s8", "s9", "s10"}, nil)
            s3 := sb.NewStringArray()
            defer s3.Release()

            c := arrow.NewChunked(
                arrow.BinaryTypes.String,
                []arrow.Array{s1, s2, s3},
            )
            return c
        }()
        defer chunk.Release()

        return arrow.NewColumn(schema.Field(2), chunk)
    }()
    defer col3.Release()

    var tbl arrow.Table
    tbl = array.NewTable(schema, []arrow.Column{*col1, *col2, *col3}, -1)
    defer tbl.Release()

    dumpTable(tbl)

    col4 := func() *arrow.Column {
        chunk := func() *arrow.Chunked {
            sb := array.NewStringBuilder(memory.DefaultAllocator)
            defer sb.Release()

            sb.AppendValues([]string{"ss1", "ss2"}, nil)
            s1 := sb.NewStringArray()
            defer s1.Release()

            sb.AppendValues([]string{"ss3", "ss4", "ss5"}, nil)
            s2 := sb.NewStringArray()
            defer s2.Release()

            sb.AppendValues([]string{"ss6", "ss7", "ss8", "ss9", "ss10"}, nil)
            s3 := sb.NewStringArray()
            defer s3.Release()

            c := arrow.NewChunked(
                arrow.BinaryTypes.String,
                []arrow.Array{s1, s2, s3},
            )
            return c
        }()
        defer chunk.Release()

        return arrow.NewColumn(arrow.Field{Name: "col4", Type: arrow.BinaryTypes.String}, chunk)
    }()
    defer col4.Release()

    tbl, err := tbl.AddColumn(
        3,
        arrow.Field{Name: "col4", Type: arrow.BinaryTypes.String},
        *col4,
    )
    if err != nil {
        panic(err)
    }

    dumpTable(tbl)
}
</code></pre>
<p>运行上述示例，输出如下：</p>
<pre><code>$go run table_schema_change.go
schema:
  fields: 3
    - col1: type=int32
    - col2: type=float64
    - col3: type=utf8
------
the count of table columns= 3
the count of table rows= 10
------
arrays in column(col1):
[1 2 3]
[4 5 6 7 8 9 10]
------
arrays in column(col2):
[1.1 2.2 3.3 4.4 5.5]
[6.6 7.7]
[8.8 9.9 10]
------
arrays in column(col3):
["s1" "s2"]
["s3" "s4"]
["s5" "s6" "s7" "s8" "s9" "s10"]
------
schema:
  fields: 4
    - col1: type=int32
    - col2: type=float64
    - col3: type=utf8
    - col4: type=utf8
------
the count of table columns= 4
the count of table rows= 10
------
arrays in column(col1):
[1 2 3]
[4 5 6 7 8 9 10]
------
arrays in column(col2):
[1.1 2.2 3.3 4.4 5.5]
[6.6 7.7]
[8.8 9.9 10]
------
arrays in column(col3):
["s1" "s2"]
["s3" "s4"]
["s5" "s6" "s7" "s8" "s9" "s10"]
------
arrays in column(col4):
["ss1" "ss2"]
["ss3" "ss4" "ss5"]
["ss6" "ss7" "ss8" "ss9" "ss10"]
------

</code></pre>
<p>这种对schema变更操作的支持在实际开发中也是非常有用的。</p>
<h2>4. 小结</h2>
<p>本文讲解了基于array type的三个高级数据结构：Record Batch、Chunked Array和Table。其中Record Batch是Arrow Columnar Format中的结构，可以被所有实现arrow的编程语言所支持；Chunked Array和Table则是在一些编程语言的实现中创建的。</p>
<p>三个概念容易混淆，这里给出简单记法：</p>
<ul>
<li>Record Batch: schema + 长度相同的多个array</li>
<li>Chunked Array: []array</li>
<li>Table: schema + 总长度相同的多个Chunked Array</li>
</ul>
<blockquote>
<p>注：本文涉及的源代码在<a href="https://github.com/bigwhite/experiments/blob/master/arrow/advanced-datastructure">这里</a>可以下载。</p>
</blockquote>
<h2>5. 参考资料</h2>
<ul>
<li><a href="https://arrow.apache.org/docs/format/Glossary.html">Apache Arrow Glossary</a> &#8211; https://arrow.apache.org/docs/format/Glossary.html</li>
</ul>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2023, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2023/07/08/a-guide-of-using-apache-arrow-for-gopher-part3/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go语言开发者的Apache Arrow使用指南：内存管理</title>
		<link>https://tonybai.com/2023/06/30/a-guide-of-using-apache-arrow-for-gopher-part2/</link>
		<comments>https://tonybai.com/2023/06/30/a-guide-of-using-apache-arrow-for-gopher-part2/#comments</comments>
		<pubDate>Fri, 30 Jun 2023 14:00:59 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Apache]]></category>
		<category><![CDATA[Array]]></category>
		<category><![CDATA[Arrow]]></category>
		<category><![CDATA[ArrowColumnarFormat]]></category>
		<category><![CDATA[bitmap]]></category>
		<category><![CDATA[buffer]]></category>
		<category><![CDATA[Builder模式]]></category>
		<category><![CDATA[Cpp]]></category>
		<category><![CDATA[dictionary]]></category>
		<category><![CDATA[dremio]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[greptimedb]]></category>
		<category><![CDATA[InfluxDB]]></category>
		<category><![CDATA[influxdb-iox]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[layout]]></category>
		<category><![CDATA[list]]></category>
		<category><![CDATA[metadata]]></category>
		<category><![CDATA[offset]]></category>
		<category><![CDATA[parquet]]></category>
		<category><![CDATA[primitive]]></category>
		<category><![CDATA[RPC]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[semver]]></category>
		<category><![CDATA[SIMD]]></category>
		<category><![CDATA[slot]]></category>
		<category><![CDATA[sparse]]></category>
		<category><![CDATA[spec]]></category>
		<category><![CDATA[struct]]></category>
		<category><![CDATA[tsdb]]></category>
		<category><![CDATA[union]]></category>
		<category><![CDATA[列式存储]]></category>
		<category><![CDATA[垃圾回收]]></category>
		<category><![CDATA[引用计数]]></category>
		<category><![CDATA[时序数据库]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3920</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2023/06/30/a-guide-of-using-apache-arrow-for-gopher-part2 如果你看了上一篇《Go语言开发者的Apache Arrow使用指南：数据类型》中的诸多Go操作arrow的代码示例，你很可能会被代码中大量使用的Retain和Release方法搞晕。不光大家有这样的感觉，我也有同样的feeling：Go是GC语言，为什么还要借助另外一套Retain和Release来进行内存管理呢？ 在这一篇文章中，我们就来探索一下这个问题的答案，并看看如何使用Retain和Release，顺便再了解一下Apache Arrow的Go实现原理。 注：本文的内容基于Apache Arrow Go v13版本(go.mod中go version为v13)的代码。 1. Go Arrow实现中的builder模式 看过第一篇文章中的代码的童鞋可能发现了，无论是Primitive array type还是嵌套类型的诸如List array type，其array的创建套路都是这样的： 首先创建对应类型的Builder，比如array.Int32Builder； 然后，向Builder实例中append值； 最后，通过Builder的NewArray方法获得目标Array的实例，比如array.Int32。 据说这个builder模式是参考了Arrow的C++实现。这里将Go的builder模式中各个类型之间的关系以下面这幅示意图的形式呈现一下： 当然这幅图也大概可以作为Go Arrow实现的原理图。 从图中，我们可以看到： Arrow go提供了Builder、Array、ArrayData接口作为抽象，在这些接口中都包含了用作内存引用计数管理的Retain和Release方法； array包提供了Builder接口的一个默认实现builder类型，所有的XXXBuilder都组(内)合(嵌)了这个类型，这个类型实现了Retain方法，Release方法需要XXXBuilder自行实现。 array包提供了Array接口的一个默认实现array类型，所有的array type(比如array.Int32)都组(内)合(嵌)了这个array类型。该类型实现了Retain和Release方法。 // github.com/apache/arrow/go/arrow/array/array.go type array struct { refCount int64 data *Data nullBitmapBytes []byte } // Retain increases the reference count by 1. // Retain [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part2-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2023/06/30/a-guide-of-using-apache-arrow-for-gopher-part2">本文永久链接</a> &#8211; https://tonybai.com/2023/06/30/a-guide-of-using-apache-arrow-for-gopher-part2</p>
<p>如果你看了上一篇<a href="https://tonybai.com/2023/06/25/a-guide-of-using-apache-arrow-for-gopher-part1">《Go语言开发者的Apache Arrow使用指南：数据类型》</a>中的诸多Go操作arrow的代码示例，你很可能会被代码中大量使用的Retain和Release方法搞晕。不光大家有这样的感觉，我也有同样的feeling：<strong><a href="https://tonybai.com/2023/06/13/understand-go-gc-overhead-behind-the-convenience">Go是GC语言</a>，为什么还要借助另外一套Retain和Release来进行内存管理呢</strong>？</p>
<p>在这一篇文章中，我们就来探索一下这个问题的答案，并看看如何使用Retain和Release，顺便再了解一下Apache Arrow的Go实现原理。</p>
<blockquote>
<p>注：本文的内容基于Apache Arrow Go v13版本(go.mod中go version为v13)的代码。</p>
</blockquote>
<h2>1. Go Arrow实现中的builder模式</h2>
<p>看过第一篇文章中的代码的童鞋可能发现了，无论是Primitive array type还是嵌套类型的诸如List array type，其array的创建套路都是这样的：</p>
<ul>
<li>首先创建对应类型的Builder，比如array.Int32Builder；</li>
<li>然后，向Builder实例中append值；</li>
<li>最后，通过Builder的NewArray方法获得目标Array的实例，比如array.Int32。</li>
</ul>
<p>据说这个builder模式是参考了Arrow的C++实现。这里将Go的builder模式中各个类型之间的关系以下面这幅示意图的形式呈现一下：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part2-2.png" alt="" /></p>
<p>当然这幅图也大概可以作为Go Arrow实现的原理图。</p>
<p>从图中，我们可以看到：</p>
<ul>
<li>Arrow go提供了Builder、Array、ArrayData接口作为抽象，在这些接口中都包含了用作内存引用计数管理的Retain和Release方法；</li>
<li>array包提供了Builder接口的一个默认实现builder类型，所有的XXXBuilder都组(内)合(嵌)了这个类型，这个类型实现了Retain方法，Release方法需要XXXBuilder自行实现。</li>
<li>array包提供了Array接口的一个默认实现array类型，所有的array type(比如array.Int32)都组(内)合(嵌)了这个array类型。该类型实现了Retain和Release方法。</li>
</ul>
<pre><code>// github.com/apache/arrow/go/arrow/array/array.go
type array struct {
    refCount        int64
    data            *Data
    nullBitmapBytes []byte
}

// Retain increases the reference count by 1.
// Retain may be called simultaneously from multiple goroutines.
func (a *array) Retain() {
    atomic.AddInt64(&amp;a.refCount, 1)
}

// Release decreases the reference count by 1.
// Release may be called simultaneously from multiple goroutines.
// When the reference count goes to zero, the memory is freed.
func (a *array) Release() {
    debug.Assert(atomic.LoadInt64(&amp;a.refCount) &gt; 0, "too many releases")

    if atomic.AddInt64(&amp;a.refCount, -1) == 0 {
        a.data.Release()
        a.data, a.nullBitmapBytes = nil, nil
    }
}
</code></pre>
<p>下面以Int64 array type为例：</p>
<pre><code>// github.com/apache/arrow/go/arrow/array/numeric.gen.go 

// A type which represents an immutable sequence of int64 values.
type Int64 struct {
    array // “继承”了array的Retain和Release方法。
    values []int64
}
</code></pre>
<ul>
<li>通过XXXBuilder类型的NewArray方法可以获得该Builder对应的Array type实例，比如：调用Int32Builder的NewArray可获得一个Int32 array type的实例。一个array type实例对应的数据是逻辑上immutable的，一旦创建便不能改变。</li>
<li>通过Array接口的Data方法可以得到该array type的底层数据layout实现(arrow.ArrayData接口的实现)，包括child data。</li>
<li>arrow包定义了所有的数据类型对应的ID值和string串，这个与arrow.DataType接口放在了一个源文件中。</li>
<li>另外要注意，XXXBuilder的实例是“一次性”的，一旦调用NewArray方法返回一个array type实例，该XXXBuilder就会被reset。如果再次调用其NewArray方法，只能得到一个空的array type实例。你可以重用该Builder，只需向该Builder实例重新append值即可(见下面示例)：</li>
</ul>
<pre><code>// reuse_string_builder.go

func main() {
    bldr := array.NewStringBuilder(memory.DefaultAllocator)
    defer bldr.Release()
    bldr.AppendValues([]string{"hello", "apache arrow"}, nil)
    arr := bldr.NewArray()
    defer arr.Release()
    bitmaps := arr.NullBitmapBytes()
    fmt.Println(hex.Dump(bitmaps))
    bufs := arr.Data().Buffers()
    for _, buf := range bufs {
        fmt.Println(hex.Dump(buf.Buf()))
    }
    fmt.Println(arr)

    // reuse the builder
    bldr.AppendValues([]string{"happy birthday", "leo messi"}, nil)
    arr1 := bldr.NewArray()
    defer arr1.Release()
    bitmaps1 := arr1.NullBitmapBytes()
    fmt.Println(hex.Dump(bitmaps1))
    bufs1 := arr1.Data().Buffers()
    for _, buf := range bufs1 {
        if buf != nil {
            fmt.Println(hex.Dump(buf.Buf()))
        }
    }
    fmt.Println(arr1)
}
</code></pre>
<p>输出上面示例运行结果：</p>
<pre><code>$go run reuse_string_builder.go
00000000  03                                                |.|

00000000  03 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000000  00 00 00 00 05 00 00 00  11 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000000  68 65 6c 6c 6f 61 70 61  63 68 65 20 61 72 72 6f  |helloapache arro|
00000010  77 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |w...............|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

["hello" "apache arrow"]
00000000  03                                                |.|

00000000  03 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000000  00 00 00 00 0e 00 00 00  17 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000000  68 61 70 70 79 20 62 69  72 74 68 64 61 79 6c 65  |happy birthdayle|
00000010  6f 20 6d 65 73 73 69 00  00 00 00 00 00 00 00 00  |o messi.........|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

["happy birthday" "leo messi"]
</code></pre>
<p>想必到这里，大家对Arrow的Go实现原理有了一个大概的认知了。接下来，我们再来看Go arrow实现的内存引用计数管理。</p>
<h2>2. Go Arrow实现的内存引用计数管理</h2>
<p>在上面图中，我们看到Go Arrow实现的几个主要接口Builder、Array、ArrayData都包含了Release和Retain方法，也就是说实现了这些接口的类型都支持采用引用计数方法(Reference Counting)进行内存的跟踪和管理。Retain方法的语义是引用计数加1，而Release方法则是引用计数减1。由于采用了原子操作对引用计数进行加减，因此这两个方法是并发安全的。当引用计数减到0时，该引用计数对应的内存块就可以被释放掉了。</p>
<p><a href="https://github.com/apache/arrow/tree/main/go">Go Arrow实现的主页</a>上对引用计数的使用场景和规则做了如下说明：</p>
<ul>
<li>如果你被传递了一个对象并希望获得它的所有权(ownership)，你必须调用Retain方法。当你不再需要该对象时，你必须调用对应的Release方法。”获得所有权”意味着你希望在当前函数调用的范围之外访问该对象。</li>
<li>你通过名称以New或Copy开头的函数创建的任何对象，或者在通过channel接收对象时，你都将拥有所有权。因此，一旦你不再需要这个对象，你必须调用Release。</li>
<li>如果你通过一个channel发送一个对象，你必须在发送之前调用Retain，因为接收者将拥有该对象。接收者有义务在以后不再需要该对象时调用Release。</li>
</ul>
<p>有了这个说明后，我们对于Retain和Release的使用场景基本做到心里有谱了。但还有一个问题亟待解决，那就是：<strong>Go是GC语言，为何还要在GC之上加上一套引用计数呢</strong>？</p>
<p>这个问题我在<a href="https://github.com/apache/arrow/issues/35232">这个issue</a>中找到了答案。一个Go arrow实现的commiter在回答issue时提到：“理论上，如果你知道你使用的是默认的Go分配器，你实际上不必在你的消费者(指的是Arrow Go包 API的使用者)代码中调用Retain/Release，可以直接让Go垃圾回收器管理一切。我们只需要确保我们在库内调用Retain/Release，这样如果消费者使用非Go GC分配器，我们就可以确保他们不会出现内存泄漏”。</p>
<p>下面是默认的Go分配器的实现代码：</p>
<pre><code>package memory

// DefaultAllocator is a default implementation of Allocator and can be used anywhere
// an Allocator is required.
//
// DefaultAllocator is safe to use from multiple goroutines.
var DefaultAllocator Allocator = NewGoAllocator()

type GoAllocator struct{}

func NewGoAllocator() *GoAllocator { return &amp;GoAllocator{} }

func (a *GoAllocator) Allocate(size int) []byte {
    buf := make([]byte, size+alignment) // padding for 64-byte alignment
    addr := int(addressOf(buf))
    next := roundUpToMultipleOf64(addr)
    if addr != next {
        shift := next - addr
        return buf[shift : size+shift : size+shift]
    }
    return buf[:size:size]
}

func (a *GoAllocator) Reallocate(size int, b []byte) []byte {
    if size == len(b) {
        return b
    }

    newBuf := a.Allocate(size)
    copy(newBuf, b)
    return newBuf
}

func (a *GoAllocator) Free(b []byte) {}
</code></pre>
<p>我们看到默认的Allocator只是分配一个原生切片，并且切片的底层内存块要保证64-byte对齐。</p>
<p>但为什么Retain和Release依然存在且需要调用呢？这位commiter给出了他理解的几点原因：</p>
<ul>
<li>允许用户控制buffer和内部数据何时被设置为nil，以便在可能的情况下提前标记为可被垃圾收集；</li>
<li>如果用户愿意，允许正确使用不依赖Go垃圾收集器的分配器（比如mallocator实现，它使用malloc/free来管理C内存而不是使用Go垃圾收集来管理）；</li>
<li>虽然用户可以通过SetFinalizer来使用Finalizer进行内存释放，但一般来说，我们建议最好有一个显式的释放动作，而不是依赖finalizer，因为没有实际保证finalizer会运行。此外，finalizer只在GC期间运行，这意味着如果你的分配器正在分配C内存或其他东西，而Go内存一直很低，那么你有可能在任何finalizer运行以实际调用Free之前，就被分配了大量的C内存，从而耗尽了你的内存。</li>
</ul>
<p>基于这些原因，Go Arrow实现保留了Retain和Release，虽然有上门的一些场景使用方法，但这两个方法的加入一定程度上增加了Go Arrow API使用的门槛。并且在重度使用Go Arrow实现的程序中，大家务必对程序做稳定性长测试验证，以确保memory没有leak。</p>
<h2>3. 如何实现ZeroCopy的内存数据共享</h2>
<p><a href="https://book.douban.com/subject/35954154/">《In-Memory Analytics with Apache Arrow》</a>一书在第二章中提到了采用Arrow实现zerocopy的内存数据共享的原理，这里将其称为“切片(slice)原理”，用书中的例子简单描述就是这样的：假设你想对一个有数十亿行的非常大的数据集进行一些分析操作。提高这种操作性能的一个常见方法是对行的子集进行并行操作，即仅通过对数组和数据缓冲区进行切分，而不需要复制底层数据。这样你操作的每个批次都不是一个副本&#8211;<strong>它只是数据的一个视图</strong>。书中还给出了如下示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part2-3.png" alt="" /></p>
<p>右侧切片列中的每个切片的虚线表示它们只是各自列中的数据子集的视图，每个切片都可以安全地进行并行操作。</p>
<p>array type是逻辑上immutable的，底层data buffer一旦建立后，便可以通过切片的方式来以zerocopy方式做内存数据共享，极大提高了数据操作的性能。</p>
<h2>4. 小结</h2>
<p>本文介绍了Go arrow实现的主要结构以及实现模式：builder模式，并结合Go arrow官方资料说明了采用引用计数进行内存管理的原因与使用方法，最后介绍了Arrow实现ZeroCopy的内存数据共享的原理。这些将为后续继续深入学习Arrow高级数据类型/结构奠定良好的基础。</p>
<blockquote>
<p>注：本文涉及的源代码在<a href="https://github.com/bigwhite/experiments/blob/master/arrow/memory-management">这里</a>可以下载。</p>
</blockquote>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2023, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2023/06/30/a-guide-of-using-apache-arrow-for-gopher-part2/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go语言开发者的Apache Arrow使用指南：数据类型</title>
		<link>https://tonybai.com/2023/06/25/a-guide-of-using-apache-arrow-for-gopher-part1/</link>
		<comments>https://tonybai.com/2023/06/25/a-guide-of-using-apache-arrow-for-gopher-part1/#comments</comments>
		<pubDate>Sat, 24 Jun 2023 20:43:38 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Apache]]></category>
		<category><![CDATA[Array]]></category>
		<category><![CDATA[Arrow]]></category>
		<category><![CDATA[ArrowColumnarFormat]]></category>
		<category><![CDATA[bitmap]]></category>
		<category><![CDATA[buffer]]></category>
		<category><![CDATA[Cpp]]></category>
		<category><![CDATA[dictionary]]></category>
		<category><![CDATA[dremio]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[greptimedb]]></category>
		<category><![CDATA[InfluxDB]]></category>
		<category><![CDATA[influxdb-iox]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[layout]]></category>
		<category><![CDATA[list]]></category>
		<category><![CDATA[metadata]]></category>
		<category><![CDATA[offset]]></category>
		<category><![CDATA[parquet]]></category>
		<category><![CDATA[primitive]]></category>
		<category><![CDATA[RPC]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[semver]]></category>
		<category><![CDATA[SIMD]]></category>
		<category><![CDATA[slot]]></category>
		<category><![CDATA[sparse]]></category>
		<category><![CDATA[spec]]></category>
		<category><![CDATA[struct]]></category>
		<category><![CDATA[tsdb]]></category>
		<category><![CDATA[union]]></category>
		<category><![CDATA[列式存储]]></category>
		<category><![CDATA[时序数据库]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3915</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2023/06/25/a-guide-of-using-apache-arrow-for-gopher-part1 如果你不是做大数据分析的，提到Arrow这个词，你可能会以为我要聊聊那个箭牌卫浴或是箭牌口香糖(注：其实箭牌口香糖使用的单词并非Arrow)。其实我要聊的是Apache的一个顶级项目：Arrow。 为什么要聊这个项目呢？说来话长，主要是因为前段时间接触到的几个时序数据库开源项目，包括国外大名鼎鼎的InfluxDB(尤指其iox这个新存储引擎)以及国内一个新初创公司的开源项目greptimedb。它们其实是竞争对手，但他们有一个共同的特点，那就是时序数据在内存中的组织都是基于Arrow设计与实现的。 InfluxDB iox的主力开发者Andrew Lamb在他的一次技术分享中曾提到这样一个观点： 如果你在编码实现一个分析型数据库系统，那么你最终将实现Arrow的功能集合。 在上述公司技术人员的眼中，Arrow是构建下一代时序数据库引擎的核心技术之一。 Arrow内容很多，不是一篇文章可以聊完的，因此我计划了一个系列的文章，争取能覆盖到Arrow项目的核心部分的内容，这里是第一篇。 注：Arrow是语言无关的，但这里所有代码示例使用的都是Go语言^_^。 1. Arrow项目简介 按照Arrow项目官方的说法：“Apache Arrow是一个用于内存分析的开发平台。它包含一组技术，这些技术可以使大数据系统能够快速处理和移动数据。它为平面和分层数据指定了一种标准化的独立于语言的列式内存格式，其组织形式为现代硬件上的数据的高效分析操作做了充分考虑”。 简单诠释一下上面这段话： Apache Arrow编写了一套编程语言无关的内存格式规范(当前版本为v1.3)，这是一种列式存储的格式，基于这种格式可以实现高压缩比的数据的压缩存储、高效的性能分析操作以及无需序列化和反序列化的低开销数据传输。 下图是展示了Arrow的列式存储格式。最上面的是一个逻辑表，这个表有三个列：ARCHER、LOCATION和YEAR，左下角是使用行式存储实现逻辑表的内存存储方式，而右下角则是Arrow的方案，即采用列式存储格式实现逻辑表的方式： 注：上图由来自《In-Memory Analytics with Apache Arrow》书中的几幅图拼接而成。 一套规范，大家共尊，这样数据传递和处理时，无需序列化和反序列化 注：上图同样由来自《In-Memory Analytics with Apache Arrow》书中的2幅图拼接而成。 多种主流语言的实现 下面是Arrow项目的各个编程语言的实现和支持矩阵情况： 我们看到，目前C++、Java、Go和Rust等对Arrow的支持较为全面。 通信传输与磁盘存储 Arrow的子项目Arrow Flight RPC为使用Arrow内存格式的系统提供了标准的通信传输方式。 Apache的另外一个顶级项目Parquet则经常被用作Arrow数据的磁盘存储格式，InfluxDB iox项目也是将内存中的Arrow格式数据转换为Parquet后存储在对象存储中的。 了解了Arrow项目的大致情况后，我们接下来再来看看Arrow项目的核心规范：Arrow Columnar Format。 2. Arrow Columnar Format规范 很多人最厌恶读所谓的“规范”了，太抽象，太概念化了，啃起来很烧脑。很不巧，Arrow Columnar Format规范也归属在这一类规范中。 不过，再难啃也得啃。如果不了解规范中的术语和概念，后面我们很可能就走不下去了。好在我们有《In-Memory Analytics with Apache Arrow》的帮助，算是有了抓手，将书与规范结合在一起看，略微降低一些理解上的难度。 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part1-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2023/06/25/a-guide-of-using-apache-arrow-for-gopher-part1">本文永久链接</a> &#8211; https://tonybai.com/2023/06/25/a-guide-of-using-apache-arrow-for-gopher-part1</p>
<p>如果你不是做大数据分析的，提到Arrow这个词，你可能会以为我要聊聊那个箭牌卫浴或是箭牌口香糖(注：其实箭牌口香糖使用的单词并非Arrow)。其实我要聊的是<a href="https://arrow.apache.org">Apache的一个顶级项目：Arrow</a>。</p>
<p>为什么要聊这个项目呢？说来话长，主要是因为前段时间接触到的几个时序数据库开源项目，包括国外大名鼎鼎的<a href="https://github.com/influxdata/influxdb_iox">InfluxDB(尤指其iox这个新存储引擎)</a>以及国内一个新初创公司的开源项目<a href="https://github.com/GrepTimeTeam/greptimedb">greptimedb</a>。它们其实是竞争对手，但他们有一个共同的特点，那就是时序数据在内存中的组织都是基于Arrow设计与实现的。</p>
<p>InfluxDB iox的主力开发者<a href="https://www.youtube.com/watch?v=dQFjKa9vKhM">Andrew Lamb在他的一次技术分享</a>中曾提到这样一个观点：</p>
<blockquote>
<p>如果你在编码实现一个分析型数据库系统，那么你最终将实现Arrow的功能集合。</p>
</blockquote>
<p>在上述公司技术人员的眼中，<strong>Arrow是构建下一代时序数据库引擎的核心技术之一</strong>。</p>
<p>Arrow内容很多，不是一篇文章可以聊完的，因此我计划了一个系列的文章，争取能覆盖到Arrow项目的核心部分的内容，这里是第一篇。</p>
<blockquote>
<p>注：Arrow是语言无关的，但这里所有代码示例使用的都是Go语言^_^。</p>
</blockquote>
<h2>1. Arrow项目简介</h2>
<p>按照Arrow项目官方的说法：“Apache Arrow是一个用于内存分析的开发平台。它包含一组技术，这些技术可以使大数据系统能够快速处理和移动数据。它为平面和分层数据指定了一种标准化的独立于语言的列式内存格式，其组织形式为现代硬件上的数据的高效分析操作做了充分考虑”。</p>
<p>简单诠释一下上面这段话：</p>
<ul>
<li>Apache Arrow编写了一套<a href="https://arrow.apache.org/docs/format/Columnar.html">编程语言无关的内存格式规范</a>(当前版本为v1.3)，这是一种列式存储的格式，基于这种格式可以实现<strong>高压缩比的数据的压缩存储、高效的性能分析操作以及无需序列化和反序列化的低开销数据传输</strong>。</li>
</ul>
<p>下图是展示了Arrow的列式存储格式。最上面的是一个逻辑表，这个表有三个列：ARCHER、LOCATION和YEAR，左下角是使用行式存储实现逻辑表的内存存储方式，而右下角则是Arrow的方案，即采用列式存储格式实现逻辑表的方式：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part1-2.png" alt="" /></p>
<blockquote>
<p>注：上图由来自<a href="https://book.douban.com/subject/35954154/">《In-Memory Analytics with Apache Arrow》</a>书中的几幅图拼接而成。</p>
</blockquote>
<ul>
<li>一套规范，大家共尊，这样数据传递和处理时，无需序列化和反序列化</li>
</ul>
<p><img src="https://tonybai.com/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part1-3.png" alt="" /></p>
<blockquote>
<p>注：上图同样由来自<a href="https://book.douban.com/subject/35954154/">《In-Memory Analytics with Apache Arrow》</a>书中的2幅图拼接而成。</p>
</blockquote>
<ul>
<li>多种主流语言的实现</li>
</ul>
<p>下面是Arrow项目的各个编程语言的实现和支持矩阵情况：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part1-4.png" alt="" /></p>
<p>我们看到，目前C++、Java、Go和Rust等对Arrow的支持较为全面。</p>
<ul>
<li>通信传输与磁盘存储</li>
</ul>
<p>Arrow的子项目<a href="https://arrow.apache.org/docs/format/Flight.html">Arrow Flight RPC</a>为使用Arrow内存格式的系统提供了标准的通信传输方式。</p>
<p><a href="https://parquet.apache.org/">Apache的另外一个顶级项目Parquet</a>则经常被用作Arrow数据的磁盘存储格式，InfluxDB iox项目也是将内存中的Arrow格式数据转换为Parquet后存储在对象存储中的。</p>
<p>了解了Arrow项目的大致情况后，我们接下来再来看看Arrow项目的核心规范：<a href="https://arrow.apache.org/docs/format/Columnar.html">Arrow Columnar Format</a>。</p>
<h2>2. Arrow Columnar Format规范</h2>
<p>很多人最厌恶读所谓的“规范”了，太抽象，太概念化了，啃起来很烧脑。很不巧，Arrow Columnar Format规范也归属在这一类规范中。</p>
<p>不过，再难啃也得啃。如果不了解规范中的术语和概念，后面我们很可能就走不下去了。好在我们有<a href="https://book.douban.com/subject/35954154/">《In-Memory Analytics with Apache Arrow》</a>的帮助，算是有了抓手，将书与规范结合在一起看，略微降低一些理解上的难度。</p>
<p>Arrow的列式格式有一些关键特性，这里引述一下：</p>
<ul>
<li>顺序访问(扫描)的数据邻接性</li>
<li>O(1)（恒定时间）随机访问</li>
<li>对SIMD和矢量化友好</li>
<li>可重新定位，没有”指针摆动”，允许在共享内存中实现真正的零拷贝访问</li>
</ul>
<p>这些关键特性都在告诉我们Arrow具备一个优点：快！这也是为什么influxdb iox引擎使用Arrow作为数据在内存中组织形式的原因，Andrew Lamb在他的分享中给出了Rust使用Arrow和不使用Arrow的性能对比：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part1-5.png" alt="" /></p>
<p>我们看到基于Arrow的实现比原生Rust实现还要快很多！</p>
<p>前面说过：Arrow是列式存储格式，它的核心型态就是<strong>Array</strong>。</p>
<p>Array是已知长度的同构类型值的序列，Array中一个值称为一个<strong>slot</strong>：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part1-6.png" alt="" /></p>
<p>规范同时定义了承载Array的内存表示(physical layout)，通常一个Array的内存表示由多个buffer构成，每个buffer实际上就是<strong>一个固定长度的连续内存区域</strong>：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part1-7.png" alt="" /></p>
<p>Array支持嵌套，像List\&lt;U&#62;就是一个嵌套类型(Nested type)，而List\&lt;U&#62;称为parent array类型，而U则称为child array type。如果一个Array不是嵌套类型，那么称之为Primitive type。</p>
<p>要真正了解Arrow，就要了解每个Array type的physical layout，一个array type也被称为一个logical type。Arrow定义了多种logical type，它们拥有不同的physical layout，当然也可以拥有相同的physical layout。相同physical layout的logical type可以划为一类，按layout type进行分类，我们能得到下面这张表(摘自<a href="https://book.douban.com/subject/35954154/">《In-Memory Analytics with Apache Arrow》</a>一书)：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part1-8.png" alt="" /></p>
<p>我们看到不同layout中有一些buffer并非用来存储data，比如多数layout的buffer0存储的是一个bitmap，有的buffer1存储的是offset，这些非data的信息被称为metadata。实际上，一个array是由一些metadata和真正的data组合而成的。</p>
<p>下面我们逐个来看看这些layout不同的Arrow array类型。</p>
<h2>3. 数据类型</h2>
<h3>3.1 metadata</h3>
<p>在介绍Arrow的array类型之前，我们简单说说metadata。</p>
<p>Arrow array有如下几个常见的属性是存放在metadata中的：</p>
<ul>
<li>Array length：array中slot的数量，即array有几个元素，通常用64-bit signed integer表示；</li>
<li>Null count：null value slot的数量，同样也通常用64-bit signed integer表示；</li>
<li>Validity bitmaps：bitmap中的bit用来指示对应的array slot是否为null。并且arrow使用的是“小端bit序”，以一个字节(8bit)为一组，bitmap的最右侧bit指示的是array中第一个slot是否为null(未置位代表是null)，下面是一个示意图：</li>
</ul>
<p><img src="https://tonybai.com/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part1-9.png" alt="" /></p>
<p>下面是用arrow的go包实现的上述示意图中的代码示例：</p>
<pre><code>// bitmap_of_array.go
package main

import (
    "encoding/hex"
    "fmt"

    "github.com/apache/arrow/go/v13/arrow/array"
    "github.com/apache/arrow/go/v13/arrow/memory"
)

func main() {
    bldr := array.NewInt64Builder(memory.DefaultAllocator)
    defer bldr.Release()
    bldr.AppendValues([]int64{1, 2}, nil)
    bldr.AppendNull()
    bldr.AppendValues([]int64{4, 5, 6, 7, 8, 9, 10}, nil)
    arr := bldr.NewArray()
    defer arr.Release()
    bitmaps := arr.NullBitmapBytes()
    fmt.Println(hex.Dump(bitmaps)) // fb 03 00 00
    fmt.Println(arr)               // [1 2 (null) 4 5 6 7 8 9 10]
}
</code></pre>
<p>如果一个array没有null元素，那也可以省略bitmap。</p>
<p>看完metadata，我们接下来就来看一些arrow定义的array逻辑类型。</p>
<h3>3.2 Null type</h3>
<p>Null type并非null，它是一种无需真正分配内存的logical type，下面是arrow go实现中NullType的定义：</p>
<pre><code>// NullType describes a degenerate array, with zero physical storage.
type NullType struct{}
</code></pre>
<p>我们知道struct{}不占用任何真实内存空间，NullType则“继承”了这点。</p>
<h3>3.3 Primitive Type</h3>
<p>Primitive type指的是slot元素类型相同且定长的arrow array type，从Go的源码中我们能找到如下这些Primitive Types:</p>
<pre><code>var (
    PrimitiveTypes = struct {
        Int8    DataType
        Int16   DataType
        Int32   DataType
        Int64   DataType
        Uint8   DataType
        Uint16  DataType
        Uint32  DataType
        Uint64  DataType
        Float32 DataType
        Float64 DataType
        Date32  DataType
        Date64  DataType
    }{
        ... ...
    }
)
</code></pre>
<p>下面挑重点说说。</p>
<h4>3.3.1 Boolean Type</h4>
<p>Boolean Type不在上面的Primitive Types行列，但实质上，Boolean Type也属于PrimitiveType这一类。在Arrow中，Boolean array Type是使用bit对每一个slot进行存储的。我们来看一个例子：</p>
<pre><code>// boolean_array_type.go
package main

import (
    "encoding/hex"
    "fmt"

    "github.com/apache/arrow/go/v13/arrow/array"
    "github.com/apache/arrow/go/v13/arrow/memory"
)

func main() {
    bldr := array.NewBooleanBuilder(memory.DefaultAllocator)
    defer bldr.Release()
    bldr.AppendValues([]bool{true, false}, nil)
    bldr.AppendNull()
    bldr.AppendValues([]bool{true, true, true, false, false, false, true}, nil)
    arr := bldr.NewArray()
    defer arr.Release()
    bitmaps := arr.NullBitmapBytes()
    fmt.Println(hex.Dump(bitmaps))
    bufs := arr.Data().Buffers()
    for _, buf := range bufs {
        fmt.Println(hex.Dump(buf.Buf()))
    }
    fmt.Println(arr)
}
</code></pre>
<p>这个例子输出的结果如下：</p>
<pre><code>$go run boolean_array_type.go
00000000  fb 03 00 00                                       |....|

00000000  fb 03 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000000  39 02 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |9...............|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

[true false (null) true true true false false false true]
</code></pre>
<p>输出结果的第一行是bitmap的部分。</p>
<p>后面两段则是构成boolean array的两个buffer的layout，我们看到第一个buffer存储的是bitmap，第二个buffer则是存储的是boolean data。</p>
<p>大家看到这个输出结果的第一感觉是：为什么用了这么多字节？我们数了一数，每个buffer用了64字节，这与arrow对buffer的对齐要求是分不开的，默认情况下，要求buffer按64字节对齐。</p>
<h4>3.3.2 Integer types</h4>
<p>arrow支持各种integer type作为primitive types，这里以int32为例：</p>
<pre><code>// int32_array_type.go
func main() {
    bldr := array.NewInt32Builder(memory.DefaultAllocator)
    defer bldr.Release()
    bldr.AppendValues([]int32{1, 2}, nil)
    bldr.AppendNull()
    bldr.AppendValues([]int32{4, 5, 6, 7, 8, 9, 10}, nil)
    arr := bldr.NewArray()
    defer arr.Release()
    bitmaps := arr.NullBitmapBytes()
    fmt.Println(hex.Dump(bitmaps))
    bufs := arr.Data().Buffers()
    for _, buf := range bufs {
        fmt.Println(hex.Dump(buf.Buf()))
    }
    fmt.Println(arr)
}
</code></pre>
<p>输出上述程序的执行结果：</p>
<pre><code>$go run int32_array_type.go
00000000  fb 03 00 00                                       |....|

00000000  fb 03 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000000  01 00 00 00 02 00 00 00  00 00 00 00 04 00 00 00  |................|
00000010  05 00 00 00 06 00 00 00  07 00 00 00 08 00 00 00  |................|
00000020  09 00 00 00 0a 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

[1 2 (null) 4 5 6 7 8 9 10]
</code></pre>
<p>值得注意的是：data buffer中是以小端字节序存储的int32。</p>
<h4>3.3.3 Float types</h4>
<p>Go对arrow的实现支持float16、float32和float64三个精度的浮点数类型，下面以float32为例，看看其layout：</p>
<pre><code>// float32_array_type.go
func main() {
    bldr := array.NewFloat32Builder(memory.DefaultAllocator)
    defer bldr.Release()
    bldr.AppendValues([]float32{1.0, 2.0}, nil)
    bldr.AppendNull()
    bldr.AppendValues([]float32{4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.1}, nil)
    arr := bldr.NewArray()
    defer arr.Release()
    bitmaps := arr.NullBitmapBytes()
    fmt.Println(hex.Dump(bitmaps))
    bufs := arr.Data().Buffers()
    for _, buf := range bufs {
        fmt.Println(hex.Dump(buf.Buf()))
    }
    fmt.Println(arr)
}
</code></pre>
<p>输出上述程序的执行结果：</p>
<pre><code>$go run float32_array_type.go
00000000  fb 03 00 00                                       |....|

00000000  fb 03 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000000  00 00 80 3f 00 00 00 40  00 00 00 00 00 00 80 40  |...?...@.......@|
00000010  00 00 a0 40 00 00 c0 40  00 00 e0 40 00 00 00 41  |...@...@...@...A|
00000020  00 00 10 41 9a 99 21 41  00 00 00 00 00 00 00 00  |...A..!A........|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

[1 2 (null) 4 5 6 7 8 9 10.1]
</code></pre>
<h3>3.4 Variable-size Binary Type</h3>
<p>Primitive Types的slot是定长类型的，针对变长类型slot，Arrow定义了Variable-size Binary Type。在前面的那张不同类型的layout表中我们看到Variable-size Binary Type除了有bitmap buffer、data buffer外，还有一个offset buffer。</p>
<p>下面我们就以最为典型的字符串(string) array为例，看看Variable-size Binary Type的layout是什么样子的：</p>
<pre><code>// string_array_type.go

func main() {
    bldr := array.NewStringBuilder(memory.DefaultAllocator)
    defer bldr.Release()
    bldr.AppendValues([]string{"hello", "apache arrow"}, nil)
    arr := bldr.NewArray()
    defer arr.Release()
    bitmaps := arr.NullBitmapBytes()
    fmt.Println(hex.Dump(bitmaps))
    bufs := arr.Data().Buffers()
    for _, buf := range bufs {
        fmt.Println(hex.Dump(buf.Buf()))
    }
    fmt.Println(arr)
}
</code></pre>
<p>运行该示例：</p>
<pre><code>$go run string_array_type.go
00000000  03                                                |.|

00000000  03 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000000  00 00 00 00 05 00 00 00  11 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000000  68 65 6c 6c 6f 61 70 61  63 68 65 20 61 72 72 6f  |helloapache arro|
00000010  77 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |w...............|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

["hello" "apache arrow"]
</code></pre>
<p>我们看到Variable-size Binary Type使用了三个buffer，除了第一个bitmap buffer和最后的data buffer外，中间的那个是offset buffer。在offset buffer中，arrow使用一个整型数来指示每个slot的起始offset，这里将上面例子整理成一张示意图，大家可以看的更清晰一些：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part1-10.png" alt="" /></p>
<h3>3.5 Fixed-Size List type</h3>
<p>在上面Primitive Types的基础上，arrow提供了“嵌套类型”，比如List type。list type分为两类，一类是Fixed-Size List type，另一类则是Variable-Size List type。我们先来看Fixed-Size List type。</p>
<p>顾名思义，Fixed-Size List type就是list的每个slot存储的都是类型相同且定长的值，可记作：FixedSizeList\&lt;T&#62;[N]。T可以是Primitive type或其他嵌套类型，N是T的长度。</p>
<p>下面是一个fixed-size list type的示例，这里的Fixed-Size List type可以表示为FixedSizeList\&lt;Int32&#62;[3]，即list中每个slot存储的都是一个[3]int32数组：</p>
<pre><code>// fixed_list_array_type.go
func main() {
    const N = 3
    var (
        vs = [][N]int32{{0, 1, 2}, {3, 4, 5}, {6, 7, 8}, {9, -9, -8}}
    )

    lb := array.NewFixedSizeListBuilder(memory.DefaultAllocator, N, arrow.PrimitiveTypes.Int32)
    defer lb.Release()

    vb := lb.ValueBuilder().(*array.Int32Builder)
    vb.Reserve(len(vs))

    for _, v := range vs {
        lb.Append(true)
        vb.AppendValues(v[:], nil)
    }

    arr := lb.NewArray().(*array.FixedSizeList)
    defer arr.Release()
    bitmaps := arr.NullBitmapBytes()
    fmt.Println(hex.Dump(bitmaps))

    varr := arr.ListValues().(*array.Int32)
    bufs := varr.Data().Buffers()

    for _, buf := range bufs {
        fmt.Println(hex.Dump(buf.Buf()))
    }
    fmt.Println(arr)
}
</code></pre>
<p>我们不再像前面那样直接打印FixedSizeList的Buffer layout，我们仅输出FixedSizeList的bitmap buffer，其value的buffer需要获取到其values，然后通过values type的buffer输出。上述示例输出结果如下：</p>
<pre><code>$go run fixed_list_array_type.go
00000000  0f 00 00 00                                       |....|

00000000  ff 0f 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000000  00 00 00 00 01 00 00 00  02 00 00 00 03 00 00 00  |................|
00000010  04 00 00 00 05 00 00 00  06 00 00 00 07 00 00 00  |................|
00000020  08 00 00 00 09 00 00 00  f7 ff ff ff f8 ff ff ff  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

[[0 1 2] [3 4 5] [6 7 8] [9 -9 -8]]
</code></pre>
<p>这里有两个bitmap，一个是FixedSizeList的，一个是其values类型的，其value类型就是一个定长的int32 primitive array type。大家也可以借助《In-Memory Analytics with Apache Arrow》书中的一幅示意图再深刻理解一下FixedSizeList的layout：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part1-11.png" alt="" /></p>
<h3>3.6 Variable-Size List type</h3>
<p>有了FixedSizeList做铺垫，那么Variable-Size List type理解起来就容易了。和variable-size binary type一样，相较于FixedSizeList，Variable-Size List type在bitmap buffer基础上又多了一个offset buffer，我们看下面例子：</p>
<pre><code>// variable_list_array_type.go

func main() {
    var (
        vs = [][]int32{{0, 1}, {2, 3, 4, 5}, {6}, {7, 8, 9}}
    )

    lb := array.NewListBuilder(memory.DefaultAllocator, arrow.PrimitiveTypes.Int32)
    defer lb.Release()

    vb := lb.ValueBuilder().(*array.Int32Builder)
    vb.Reserve(len(vs))

    for _, v := range vs {
        lb.Append(true)
        vb.AppendValues(v[:], nil)
    }

    arr := lb.NewArray().(*array.List)
    defer arr.Release()
    bitmaps := arr.NullBitmapBytes()
    fmt.Println(hex.Dump(bitmaps))
    bufs := arr.Data().Buffers()
    for _, buf := range bufs {
        fmt.Println(hex.Dump(buf.Buf()))
    }

    varr := arr.ListValues().(*array.Int32)
    bufs = varr.Data().Buffers()

    for _, buf := range bufs {
        fmt.Println(hex.Dump(buf.Buf()))
    }
    fmt.Println(arr)
}
</code></pre>
<p>输出上述示例的运行结果：</p>
<pre><code>$go run variable_list_array_type.go
00000000  0f 00 00 00                                       |....|

00000000  0f 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000000  00 00 00 00 02 00 00 00  06 00 00 00 07 00 00 00  |................|
00000010  0a 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000000  ff 03 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000000  00 00 00 00 01 00 00 00  02 00 00 00 03 00 00 00  |................|
00000010  04 00 00 00 05 00 00 00  06 00 00 00 07 00 00 00  |................|
00000020  08 00 00 00 09 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

[[0 1] [2 3 4 5] [6] [7 8 9]]
</code></pre>
<p>前两大块数据是Variable-Size List type的bitmap buffer和offset buffer。后两大段数据则是int32 array type的bitmap buffer和data buffer。Variable-Size List type的offset buffer有四个偏移量：0, 2, 6, 7，分别指向int32 array type的data buffer中的相应位置。</p>
<p>《In-Memory Analytics with Apache Arrow》书中的一幅示意图可以帮助我们理解Variable-size List的layout：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part1-12.png" alt="" /></p>
<h3>3.7 Struct type</h3>
<p>struct也是一个嵌套类型，它可以包含多个field，而每个field又是一个arrow array type。struct的layout中包含bitmap buffer，之后就是各个field value buffer。每个field也都有自己的layout，具体layout是什么样子的需根据field的type而定。下面是一个示例，这个示例中的struct有两个field：name和age，name是一个String类型的array，而age则是int32类型的array：</p>
<pre><code>// struct_array_type.go
func main() {
    fields := []arrow.Field{
        arrow.Field{Name: "name", Type: arrow.BinaryTypes.String},
        arrow.Field{Name: "age", Type: arrow.PrimitiveTypes.Int32},
    }
    structType := arrow.StructOf(fields...)
    sb := array.NewStructBuilder(memory.DefaultAllocator, structType)
    defer sb.Release()

    names := []string{"Alice", "Bob", "Charlie"}
    ages := []int32{25, 30, 35}
    valid := []bool{true, true, true}

    nameBuilder := sb.FieldBuilder(0).(*array.StringBuilder)
    ageBuilder := sb.FieldBuilder(1).(*array.Int32Builder)

    sb.Reserve(len(names))
    nameBuilder.Resize(len(names))
    ageBuilder.Resize(len(names))

    sb.AppendValues(valid)
    nameBuilder.AppendValues(names, valid)
    ageBuilder.AppendValues(ages, valid)

    arr := sb.NewArray().(*array.Struct)
    defer arr.Release()

    bitmaps := arr.NullBitmapBytes()
    fmt.Println(hex.Dump(bitmaps))
    bufs := arr.Data().Buffers()
    for _, buf := range bufs {
        fmt.Println(hex.Dump(buf.Buf()))
    }

    nameArr := arr.Field(0).(*array.String)
    bufs = nameArr.Data().Buffers()
    for _, buf := range bufs {
        fmt.Println(hex.Dump(buf.Buf()))
    }

    ageArr := arr.Field(1).(*array.Int32)
    bufs = ageArr.Data().Buffers()
    for _, buf := range bufs {
        fmt.Println(hex.Dump(buf.Buf()))
    }

    fmt.Println(arr)
}
</code></pre>
<p>执行上述代码，我们将得到如下输出：</p>
<pre><code>$go run struct_array_type.go
00000000  07 00 00 00                                       |....|

00000000  07 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000000  07 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000000  00 00 00 00 05 00 00 00  08 00 00 00 0f 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000000  41 6c 69 63 65 42 6f 62  43 68 61 72 6c 69 65 00  |AliceBobCharlie.|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000000  07 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000000  19 00 00 00 1e 00 00 00  23 00 00 00 00 00 00 00  |........#.......|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

{["Alice" "Bob" "Charlie"] [25 30 35]}
</code></pre>
<p>第一大块数据是struct的bitmap buffer，之后的三大块数据是name field的bitmap、offset和data buffer，最后两大块数据则是age field的bitmap和data buffer。</p>
<p>下面是那本书中的一个struct类型layout的示意图，可以帮助大家理解这个结构：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part1-13.png" alt="" /></p>
<h3>3.8 Union type</h3>
<p>学过C语言的都知道union，名为联合体，说白了就是一堆类型共享一块内存，好比现代医学中的“多重人格”，能表现出哪种人格全由你来定。</p>
<p>Arrow的union array type就是每个slot中放置一个union类型的序列。Arrow的union array type还分为两种，一种为dense union type，一种是sparse union type。至于他们有什么区别，我们可以通过下面的两个示例直观的看到。union array type相对于上面的primitive type和list、struct这样的嵌套类型来说都相对难于理解一些。</p>
<p>我们先来看看dense union array type。</p>
<h4>3.8.1 dense union array type</h4>
<p>我们先看一个这样的union array: [{i32=5} {f32=1.2} {f32=\&lt;nil&#62;} {f32=3.4} {i32=6}]。我们看到这个union array实例有两种union type: float32和int32。其中float32有三个值：1.2、null和3.4；int32有两个值：5和6。我们编写go代码来构建一下这个union array：</p>
<pre><code>// dense_union_array_type.go 

var (
    F32 arrow.UnionTypeCode = 7
    I32 arrow.UnionTypeCode = 13
)

func main() {

    childFloat32Bldr := array.NewFloat32Builder(memory.DefaultAllocator)
    childInt32Bldr := array.NewInt32Builder(memory.DefaultAllocator)

    defer func() {
        childFloat32Bldr.Release()
        childInt32Bldr.Release()
    }()

    ub := array.NewDenseUnionBuilderWithBuilders(memory.DefaultAllocator,
        arrow.DenseUnionOf([]arrow.Field{
            {Name: "f32", Type: arrow.PrimitiveTypes.Float32, Nullable: true},
            {Name: "i32", Type: arrow.PrimitiveTypes.Int32, Nullable: true},
        }, []arrow.UnionTypeCode{F32, I32}),
        []array.Builder{childFloat32Bldr, childInt32Bldr})
    defer ub.Release()

    ub.Append(I32)
    childInt32Bldr.Append(5)
    ub.Append(F32)
    childFloat32Bldr.Append(1.2)
    ub.AppendNull()
    ub.Append(F32)
    childFloat32Bldr.Append(3.4)
    ub.Append(I32)
    childInt32Bldr.Append(6)

    arr := ub.NewDenseUnionArray()
    defer arr.Release()

    // print type buffer
    buf := arr.TypeCodes().Buf()
    fmt.Println(hex.Dump(buf))

    // print offsets
    offsets := arr.RawValueOffsets()
    fmt.Println(offsets)
    fmt.Println()

    // print buffer of child array
    bufs := arr.Field(0).Data().Buffers()
    for _, buf := range bufs {
        fmt.Println(hex.Dump(buf.Buf()))
    }

    bufs = arr.Field(1).Data().Buffers()
    for _, buf := range bufs {
        fmt.Println(hex.Dump(buf.Buf()))
    }

    fmt.Println(arr)
}
</code></pre>
<p>我们看到union array的构建也是非常复杂的。按照前面的表格，dense union array type的layout中metadata占用两个buffer，第一个buffer是typeIds，第二个buffer则是offset。没有data buffer，真正的数据存储在child array的layout中。我们运行一下上面的示例直观看一下：</p>
<pre><code>$go run dense_union_array_type.go
00000000  0d 07 07 07 0d 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

[0 0 1 2 1]

00000000  05 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000000  9a 99 99 3f 00 00 00 00  9a 99 59 40 00 00 00 00  |...?......Y@....|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000000  03 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000000  05 00 00 00 06 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

[{i32=5} {f32=1.2} {f32=&lt;nil&gt;} {f32=3.4} {i32=6}]
</code></pre>
<p>第一块数据是union typeid buffer，这里我们的union array type一共有两类子类型，我分为赋予他们的typeid为float32(0&#215;07)和int32(0x0d)。union array type一共有5个slot(3个float32，2个int32)，typeids buffer这里用一个字节表示一个slot的类型，因此有3个07和2个0d。</p>
<p>下面输出的[0 0 1 2 1]则是一个offset buffer。表示同类type的value buffer的offset(一个offset值是一个4字节的int32)。以int32 slot为例，我们有两个int32 slot，分为位于总union array type 的第一个和第五个。但int32 slot是放在一起存储为int32 primitive array type的，因此union array type的第一个slot是int32 primitive array type的第一个slot，即其offset在int32 type中的偏移为0。而union array type的第5个slot是int32 primitive array type的第2个slot，即其offset在int32 type中的偏移为1。这就是[0 0 1 2 1]中第一个值为0和最后一个值为1的原因。依次类推，你可以算一下为何中间的三个值为0 1 2。</p>
<p>后面的四块数据则分别是float32 array type的buffer和int32 array type的buffer layout。我们用下图可以更直观地看到union array type的laytout：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part1-14.png" alt="" /></p>
<h4>3.8.2 sparse union array type</h4>
<p>接下来，趁热打铁，我们再来看看sparse union array type。我们还以union array: [{i32=5} {f32=1.2} {f32=\&lt;nil&#62;} {f32=3.4} {i32=6}]为例，看看用sparse union array type来表示其layout是什么样子的。我们先用go构建出这个union array type：</p>
<pre><code>// sparse_union_array_type.go

var (
    F32 arrow.UnionTypeCode = 7
    I32 arrow.UnionTypeCode = 13
)

func main() {
    childFloat32Bldr := array.NewFloat32Builder(memory.DefaultAllocator)
    childInt32Bldr := array.NewInt32Builder(memory.DefaultAllocator)

    defer func() {
        childFloat32Bldr.Release()
        childInt32Bldr.Release()
    }()

    ub := array.NewSparseUnionBuilderWithBuilders(memory.DefaultAllocator,
        arrow.SparseUnionOf([]arrow.Field{
            {Name: "f32", Type: arrow.PrimitiveTypes.Float32, Nullable: true},
            {Name: "i32", Type: arrow.PrimitiveTypes.Int32, Nullable: true},
        }, []arrow.UnionTypeCode{F32, I32}),
        []array.Builder{childFloat32Bldr, childInt32Bldr})
    defer ub.Release()

    ub.Append(I32)
    childInt32Bldr.Append(5)
    childFloat32Bldr.AppendEmptyValue()

    ub.Append(F32)
    childFloat32Bldr.Append(1.2)
    childInt32Bldr.AppendEmptyValue()

    ub.AppendNull()

    ub.Append(F32)
    childFloat32Bldr.Append(3.4)
    childInt32Bldr.AppendEmptyValue()

    ub.Append(I32)
    childInt32Bldr.Append(6)
    childFloat32Bldr.AppendEmptyValue()

    arr := ub.NewSparseUnionArray()
    defer arr.Release()

    // print type buffer
    buf := arr.TypeCodes().Buf()
    fmt.Println(hex.Dump(buf))

    // print child
    bufs := arr.Field(0).Data().Buffers()
    for _, buf := range bufs {
        fmt.Println(hex.Dump(buf.Buf()))
    }

    bufs = arr.Field(1).Data().Buffers()
    for _, buf := range bufs {
        fmt.Println(hex.Dump(buf.Buf()))
    }

    fmt.Println(arr)
}
</code></pre>
<p>和dense union type相比，sparse union type要求所有child type的length都要与union type相同。这就是上述代码为什么在append一个float32后，还要append一个emtpy的int32的原因。下面是上述程序的执行结果：</p>
<pre><code>$go run sparse_union_array_type.go

00000000  0d 07 07 07 0d 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000000  1b 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000000  00 00 00 00 9a 99 99 3f  00 00 00 00 9a 99 59 40  |.......?......Y@|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000000  1f 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000000  05 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  06 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

[{i32=5} {f32=1.2} {f32=&lt;nil&gt;} {f32=3.4} {i32=6}]
</code></pre>
<p>同样，我们用一幅示意图可以直观的展现上述结果：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part1-15.png" alt="" /></p>
<p>到这里，我们可以简单对比一下dense和sparse union了。显然sparse由于特殊的要求，它实际占用的内存空间会更大。</p>
<p>那么sparse union type用在何种场景呢？按《In Memory Analytics With Apache Arrow》书中的说法，sparse union更容易与矢量表达式求值(vectorized expression evaluation)一起使用。</p>
<h3>3.9 Dictionary-encoded type</h3>
<p>最后说说字典编码类型。如果现在我们要存储一组字符串，这组字符串中存在重复的值，比如：["foo", "bar", "foo", "bar", null, "baz"]，若使用之前提到variable-size binary type来表示，相同的字符串不会只存储一份，而是分别存储。</p>
<p>针对这样的问题，Arrow提供了采用dictionary-encode的array type，在这种type下重复的字符串只会存储一份。我们看一个示例：</p>
<pre><code>// dictionary_encoded_array_type.go

func main() {
    dictType := &amp;arrow.DictionaryType{IndexType: &amp;arrow.Int8Type{}, ValueType: &amp;arrow.StringType{}}
    bldr := array.NewDictionaryBuilder(memory.DefaultAllocator, dictType)
    defer bldr.Release()

    bldr.AppendValueFromString("foo")
    bldr.AppendValueFromString("bar")
    bldr.AppendValueFromString("foo")
    bldr.AppendValueFromString("bar")
    bldr.AppendNull()
    bldr.AppendValueFromString("baz")

    arr := bldr.NewDictionaryArray()
    defer arr.Release()
    bufs := arr.Data().Buffers()
    for _, buf := range bufs {
        fmt.Println(hex.Dump(buf.Buf()))
    }

    dict := arr.Dictionary()
    // print value string in dict
    bufs = dict.Data().Buffers()
    for _, buf := range bufs {
        if buf == nil {
            continue
        }
        fmt.Println(hex.Dump(buf.Buf()))
    }

    fmt.Println(arr)
}
</code></pre>
<p>输出上述程序的执行结果：</p>
<pre><code>$go run dictionary_encoded_array_type.go
00000000  2f 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |/...............|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000000  00 01 00 01 00 02 00 00  00 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000000  00 00 00 00 03 00 00 00  06 00 00 00 09 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000000  66 6f 6f 62 61 72 62 61  7a 00 00 00 00 00 00 00  |foobarbaz.......|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

{ dictionary: ["foo" "bar" "baz"]
  indices: [0 1 0 1 (null) 2] }
</code></pre>
<p>对照的下面的示意图，我们可以更好的理解这大段输出：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part1-16.png" alt="" /></p>
<p>我们看到dictionary array type实际上是通过一个indices建立了到底层存储字符串的array的offset的映射来实现字典编码的，这可以大大节省内存空间。</p>
<p>还有一些类型，比如Time32/Time64、Date32/Date64等，其存储结构与上面的一些类型大同小异，大家可以自行研读规范以及做编码实践来理解体会。</p>
<h2>4. Arrow格式规范的版本管理与稳定性</h2>
<p>Arrow格式规范自1.0开始便承诺遵循semver规范，即采用major.minor.fix的版本格式。只有当major版本发生变更时，才会引入不兼容的变化。当前format的版本是1.3，所以我们可以将其视作是向后兼容的。</p>
<h2>5. 小结</h2>
<p>本文介绍了Apache顶级项目Arrow，这是一个旨在在内存中建立各个类型的统一格式规范的项目，基于Arrow，各个大数据系统便可以省去序列化/反序列化的动作直接操作Arrow数据；同时Arrow采用列式模型，天生适合数据处理与分析。</p>
<p>文中对arrow的常见array type的layout进行了分析。虽然都叫type，但arrow定义的array type是描述一个“列”的，比如primitive types中的int32 type，它表示的是一个什么样的列呢？列中元素定长：sizeof(int32)、列的长度(array length)也是fixed的。只有理解到这一层次，才能更好的理解arrow。</p>
<p>本文的代码和layout适用于： Arrow Columnar Format Version: 1.3版本。</p>
<blockquote>
<p>注：本文涉及的源代码在<a href="https://github.com/bigwhite/experiments/blob/master/arrow/array-types">这里</a>可以下载。</p>
</blockquote>
<h2>6. 参考资料</h2>
<ul>
<li>Arrow FAQ &#8211; https://arrow.apache.org/faq/</li>
<li>Arrow implementation matrix &#8211; https://arrow.apache.org/docs/status.html</li>
<li>influxdb团队将arrow的Go实现捐献给apache arrow项目 &#8211; https://arrow.apache.org/blog/2018/03/22/go-code-donation/</li>
<li>Go and Apache Arrow: building blocks for data science &#8211; https://arrow.apache.org/blog/2018/03/22/go-code-donation/</li>
<li>Use Apache Arrow and Go for Your Data Workflows &#8211; https://voltrondata.com/resources/use-apache-arrow-and-go-for-your-data-workflows</li>
<li>Make Data Files Easier to Work With Using Golang and Apache Arrow &#8211; https://voltrondata.com/resources/make-data-files-easier-to-work-with-golang-arrow</li>
<li>《In-Memory Analytics with Apache Arrow》- https://book.douban.com/subject/35954154/</li>
<li>Apache Arrow的起源及其在当今数据领域的适用性 &#8211; https://www.dremio.com/blog/the-origins-of-apache-arrow-its-fit-in-todays-data-landscape/</li>
</ul>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2023, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2023/06/25/a-guide-of-using-apache-arrow-for-gopher-part1/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
	</channel>
</rss>
