<?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; Arrow</title>
	<atom:link href="http://tonybai.com/tag/arrow/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Thu, 09 Apr 2026 00:20:15 +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>2023年Go语言盘点：稳中求新，稳中求变</title>
		<link>https://tonybai.com/2023/12/31/the-2023-review-of-go-programming-language/</link>
		<comments>https://tonybai.com/2023/12/31/the-2023-review-of-go-programming-language/#comments</comments>
		<pubDate>Sun, 31 Dec 2023 05:27:21 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[2023]]></category>
		<category><![CDATA[Apache]]></category>
		<category><![CDATA[arena]]></category>
		<category><![CDATA[Arrow]]></category>
		<category><![CDATA[Camel]]></category>
		<category><![CDATA[ChatGPT]]></category>
		<category><![CDATA[clear]]></category>
		<category><![CDATA[comparable]]></category>
		<category><![CDATA[coverage]]></category>
		<category><![CDATA[devlake]]></category>
		<category><![CDATA[dubbo]]></category>
		<category><![CDATA[forrange]]></category>
		<category><![CDATA[gemini]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Githut]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[go1.20]]></category>
		<category><![CDATA[go1.21]]></category>
		<category><![CDATA[GODEBUG]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[gonew]]></category>
		<category><![CDATA[gopherchina]]></category>
		<category><![CDATA[GopherCon]]></category>
		<category><![CDATA[gorilla]]></category>
		<category><![CDATA[govulncheck]]></category>
		<category><![CDATA[IEEE]]></category>
		<category><![CDATA[index]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[LLM]]></category>
		<category><![CDATA[loopvar]]></category>
		<category><![CDATA[max]]></category>
		<category><![CDATA[min]]></category>
		<category><![CDATA[mux]]></category>
		<category><![CDATA[ollama]]></category>
		<category><![CDATA[panic]]></category>
		<category><![CDATA[Performance]]></category>
		<category><![CDATA[PGO]]></category>
		<category><![CDATA[PYPL]]></category>
		<category><![CDATA[redmonk]]></category>
		<category><![CDATA[RobPike]]></category>
		<category><![CDATA[RussCox]]></category>
		<category><![CDATA[SDK]]></category>
		<category><![CDATA[slog]]></category>
		<category><![CDATA[Spectrum]]></category>
		<category><![CDATA[sync]]></category>
		<category><![CDATA[Telemetry]]></category>
		<category><![CDATA[Tesla]]></category>
		<category><![CDATA[TIOBE]]></category>
		<category><![CDATA[unsafe]]></category>
		<category><![CDATA[v2]]></category>
		<category><![CDATA[wasi]]></category>
		<category><![CDATA[wasm]]></category>
		<category><![CDATA[zap]]></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>
		<category><![CDATA[类型推断]]></category>
		<category><![CDATA[编程语言排行榜]]></category>
		<category><![CDATA[编译器优化]]></category>
		<category><![CDATA[软件工程]]></category>
		<category><![CDATA[远控]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4092</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2023/12/31/the-2023-review-of-go-programming-language 时光荏苒，转眼间已经是2023年的最后一天了。《2022年Go语言盘点：泛型落地，无趣很好，稳定为王》仿佛就写在昨天。 回首这一年，全球彻底从新冠大流行中得以复苏，Go语言也不例外，最直观的表现就是全球各地的GopherCon技术大会或小型Meetup都纷纷从停办/线上的状态来到了线下，并获得Gopher们的热烈欢迎和踊跃参与，比如下图中的GopherCon、GopherCon UK、GopherCon Europe、GopherCon Australia、Golab等。 尤其值得一提的是我们本土最大的Gopher技术大会GopherChina 2023，今年为了满足不同地域Gopher的需求，GoCN社区在6月和11月分别在北京和上海举办了两次GopherChina大会，这也是历史首次。 Go语言团队的大神们也开始重新“乐此不疲”地参与到上述这些大会中，以推进全球Go社区与生态的建设。就连已经退居二线的Go语言之父Rob Pike也亲自“现身说法”，在年底的GopherCon Australia 2023上发表了“What We Got Right, What We Got Wrong”的主题演讲来回顾Go诞生以来的得与失。 大神回顾一生，我们盘点一年。在这篇文章中，我就和大家一起聊聊Go在2023年的状态、所处的位置以及Go未来演进的机制与策略。 1. Go的2023 1.1 稳 一如往年，Go在2023年发布了两个大版本，分别是2023年2月份的Go 1.20和8月份的Go 1.21。 在这两个版本中，Go语法特性一如既往的求稳，除了支持切片类型到数组类型(或数组类型的指针)的类型转换，其余更是像语法的修修补补，比如：comparable“放宽”了对泛型实参的限制、unsafe包继续添加“语法糖”、增加min、max和clear预定义函数、增强type inference能力等。 这些并不会让Gopher感到“意外”，因为这与Russ Cox在2022年宣称的“Go is boring”的精神是一脉相承的。 不过，除了Go语法特性变化方面的“寡淡”之外，Go在其他方面还是求新和求变的，接下来我们先来看看Go是如何求新的。 注：求新与求变可能存在交集的地方，边界可能也有一定模糊性，也存在相互促进的情况，希望大家阅读下面内容时不要吹毛求疵:)。 1.2 求新 Go在语法特性求稳的同时，在编译器、工具链、运行时以及标准库等方面都在努力优化和打磨，旨在进一步提升Go兼具的生产力与运行时效率，其中很多优化和打磨的措施不乏新颖。 Go 1.20版本中首次引入的PGO(profile-guided optimization)技术预览版，到Go 1.21版本变为默认开启，Go官方给出的PGO优化的效果数据是：PGO优化带来的性能提升一般是2%~7%，而在最新的Go 1.22rc1中，这个数字已经变为2%~14%了。 在内存管理方面，Go 1.20引入了试验特性arena包，虽然它没能在Go 1.21中按时转正，如今处于proposal-hold状态，但这也算是一次在内存管理机制上的求新。 Go是一门面向软件工程的编程语言，在这一年中，Go在软件工程领域的求新例子也是不少。比如：可用于大幅简化Go项目创建的gonew工具，它支持基于go project template clone并创建一个属于你的Go项目；再比如对应用执行时的代码覆盖率的采集，可以帮助开发者更进一步了解最终可执行程序代码执行路径上的测试覆盖情况；而govulncheck工具则是Go在软件工程与供应链安全领域的求新尝试，该工具丰富了我们对Go项目进行安全漏洞检查的手段。 注：关于供应链安全问题，Russ Cox近期有一个专门的Talk：Open Source [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/the-2023-review-of-go-programming-language-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2023/12/31/the-2023-review-of-go-programming-language">本文永久链接</a> &#8211; https://tonybai.com/2023/12/31/the-2023-review-of-go-programming-language</p>
<p>时光荏苒，转眼间已经是2023年的最后一天了。《<a href="https://tonybai.com/2022/12/29/the-2022-review-of-go-programming-language">2022年Go语言盘点：泛型落地，无趣很好，稳定为王</a>》仿佛就写在昨天。</p>
<p>回首这一年，全球彻底从新冠大流行中得以复苏，Go语言也不例外，最直观的表现就是<strong>全球各地的GopherCon技术大会或小型Meetup都纷纷从停办/线上的状态来到了线下</strong>，并获得Gopher们的热烈欢迎和踊跃参与，比如下图中的<a href="https://www.gophercon.com/">GopherCon</a>、<a href="https://www.gophercon.co.uk/">GopherCon UK</a>、<a href="https://gophercon.eu/">GopherCon Europe</a>、<a href="https://gophercon.com.au/">GopherCon Australia</a>、<a href="https://golab.io/">Golab</a>等。</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-2023-review-of-go-programming-language-2.png" alt="" /></p>
<p>尤其值得一提的是我们本土最大的Gopher技术大会<a href="https://mp.weixin.qq.com/s?__biz=MzIyNzM0MDk0Mg==&amp;mid=2247494867&amp;idx=1&amp;sn=9bf0dfa3ef48867da891aac4359a0c5e&amp;chksm=e8600b32df178224428d5ee27fd11e379011afc0bdb7e0445617275c4c43c484f72200b585dc#rd">GopherChina 2023</a>，今年为了满足不同地域Gopher的需求，GoCN社区在6月和11月分别在北京和上海举办了两次GopherChina大会，这也是历史首次。</p>
<p>Go语言团队的大神们也开始重新“乐此不疲”地参与到上述这些大会中，以推进全球Go社区与生态的建设。就连已经退居二线的<a href="https://tonybai.com/2023/12/11/simplicity/">Go语言之父Rob Pike</a>也亲自“现身说法”，在年底的GopherCon Australia 2023上发表了“What We Got Right, What We Got Wrong”的主题演讲来回顾Go诞生以来的得与失。</p>
<p>大神回顾一生，我们盘点一年。在这篇文章中，我就和大家一起聊聊Go在2023年的状态、所处的位置以及Go未来演进的机制与策略。</p>
<h2>1. Go的2023</h2>
<h3>1.1 稳</h3>
<p>一如往年，Go在2023年发布了两个大版本，分别是2023年2月份的<a href="https://tonybai.com/2023/02/08/some-changes-in-go-1-20/">Go 1.20</a>和8月份的<a href="https://tonybai.com/2023/08/20/some-changes-in-go-1-21">Go 1.21</a>。</p>
<p>在这两个版本中，Go语法特性一如既往的求稳，除了支持<strong>切片类型到数组类型(或数组类型的指针)的类型转换</strong>，其余更是像语法的修修补补，比如：comparable“放宽”了对泛型实参的限制、unsafe包继续添加“语法糖”、增加min、max和clear预定义函数、增强type inference能力等。</p>
<p>这些并不会让Gopher感到“意外”，因为这与<a href="https://tonybai.com/2022/12/29/the-2022-review-of-go-programming-language">Russ Cox在2022年宣称的“Go is boring”</a>的精神是一脉相承的。</p>
<p>不过，除了Go语法特性变化方面的“寡淡”之外，Go在其他方面还是求新和求变的，接下来我们先来看看Go是如何求新的。</p>
<blockquote>
<p>注：求新与求变可能存在交集的地方，边界可能也有一定模糊性，也存在相互促进的情况，希望大家阅读下面内容时不要吹毛求疵:)。</p>
</blockquote>
<h3>1.2 求新</h3>
<p>Go在语法特性求稳的同时，在编译器、工具链、运行时以及标准库等方面都在努力优化和打磨，旨在进一步提升Go兼具的生产力与运行时效率，其中很多优化和打磨的措施不乏新颖。</p>
<p><a href="https://go.dev/blog/pgo-preview">Go 1.20版本中首次引入的PGO(profile-guided optimization)技术预览版</a>，到Go 1.21版本变为默认开启，Go官方给出的PGO优化的效果数据是：PGO优化带来的性能提升一般是2%~7%，而在<a href="https://tonybai.com/2023/12/25/go-1-22-foresight">最新的Go 1.22rc1</a>中，这个数字已经变为2%~14%了。</p>
<p>在内存管理方面，<a href="https://github.com/golang/go/issues/51317">Go 1.20引入了试验特性arena包</a>，虽然它没能在Go 1.21中按时转正，如今处于proposal-hold状态，但这也算是一次在内存管理机制上的求新。</p>
<p>Go是一门面向软件工程的编程语言，在这一年中，Go在软件工程领域的求新例子也是不少。比如：可用于大幅简化Go项目创建的<a href="https://tonybai.com/2023/08/11/introduction-to-the-gonew-tool">gonew</a>工具，它支持基于go project template clone并创建一个属于你的Go项目；再比如<a href="https://go.dev/blog/integration-test-coverage">对应用执行时的代码覆盖率的采集</a>，可以帮助开发者更进一步了解最终可执行程序代码执行路径上的测试覆盖情况；而<a href="https://tonybai.com/2022/09/10/an-intro-of-govulncheck">govulncheck工具</a>则是Go在软件工程与<a href="https://tonybai.com/2022/03/14/software-supply-chain-security-in-go">供应链安全</a>领域的求新尝试，该工具丰富了我们对Go项目进行安全漏洞检查的手段。</p>
<blockquote>
<p>注：关于供应链安全问题，Russ Cox近期有一个专门的Talk：<a href="https://research.swtch.com/acmscored">Open Source Supply Chain Security at Google</a>，感兴趣的童鞋可以学习一下。</p>
</blockquote>
<p>Go始终对IT界出现的新技术、新趋势以及Go社区的新想法保持open。在WASM出现早期，<a href="https://tonybai.com/2018/11/19/some-changes-in-go-1-11/">Go就提供了对wasm的porting支持</a>，如今在Go 1.21中，Go还对尚未形成最终规范的<a href="https://go.dev/blog/wasi">WASI(WebAssembly System Interface)</a>提供了支持。</p>
<p>Go社区的反馈也是Go团队求新的来源，比如一个典型例子就是<a href="https://tonybai.com/2023/09/01/slog-a-new-choice-for-logging-in-go">log/slog加入标准库</a>，让Go标准库原生支持了结构化日志输出，且日志性能不输<a href="https://tonybai.com/2021/07/14/uber-zap-advanced-usage">像zap这样的第三方开源log包</a>。</p>
<p>Go社区也跟随Go团队的节奏，走在求新的道路上。2023年，IT界最大的事件就是<strong>以ChatGPT为代表的大语言模型的横空出世</strong>，这很可能是一个百年不遇的、对人类文明进步有着重要里程碑意义的事件。各行各业，言必称大模型，言必称AI。在传统机器学习、深度学习以及<a href="https://tonybai.com/2023/05/21/go-and-nn-part1-tensor-operations">神经网络</a>方面生态并不丰富的Go，也在尝试与大模型对接，比如：支持快速在本地启动和运行llama2、mistral 7B、codellama、vicuna等大模型的<a href="https://github.com/jmorganca/ollama">ollama开源项目</a>在短短几个月就收获近30k个小星星的关注；再比如Gemini大模型推出后，Google一并开源了支持与Google各种大模型项目对接的<a href="https://github.com/google/generative-ai-go">Google AI Go SDK开源项目</a>，并提供了详细的教程<a href="https://ai.google.dev/tutorials/go_quickstart?hl=zh-cn">指导Gopher如何通过该SDK与大模型交互</a>。</p>
<blockquote>
<p>注：Google把Gemini Pro的API免费提供给个人用户了，该模型具备GPT 3.5 级别的能力，32k 上下文，38 种语言支持以及多模态支持，唯一的约束是每分钟60个请求。</p>
</blockquote>
<p>在<a href="https://go.dev/blog/survey2023-h2-results">2023年第二次Go用户调查报告</a>中，Go 开发者表示，他们对改善其编写代码的质量、可靠性和性能的人工智能/机器学习工具感兴趣，而不是编写代码的工具。一位时刻警醒、从不忙碌的专家“审阅者”可能是一种更有帮助的AI开发者辅助形式。Go官方表示了对该调查结果的重视，也许在后续的Go工具链中“AI加持”会成为常态。</p>
<h3>1.3 求变</h3>
<p>2023年8月，在Go 1.21版本刚刚发布后，Go官博就发布了Russ Cox编写的两篇文章：《<a href="https://go.dev/blog/compat">Backward Compatibility, Go 1.21, and Go 2</a>》和《<a href="https://go.dev/blog/toolchain">Forward Compatibility and Toolchain Management in Go 1.21</a>》，进一步明确了Go承诺的向后兼容的范围和方案，并<a href="https://tonybai.com/2023/09/10/understand-go-forward-compatibility-and-toolchain-rule/">第一次阐述了向前兼容性的具体方案</a>，这两篇文章为Go语言后续的“求变”奠定了理论基础。</p>
<p>在向后兼容方面，从Go 1.21开始Russ Cox提出一些举措，比如：Go将扩展和规范化了GODEBUG的使用，其大致思路如下：</p>
<ul>
<li>对于每个在Go1兼容性承诺范围内的且可能会破坏(break)现有代码的新特性/新改变(比如：panic(nil)语义的改变)加入时，Go会向GODEBUG设置<br />
中添加一个新选项(比如GODEBUG=panicnil=1)，以保留采用原语义进行编译的兼容能力；</li>
<li>GODEBUG中新增的选项将至少保留两年(4个Go release版本)，对于一些影响重大的GODEBUG选项(比如http2client和http2server)，保留的时间可能更长，甚至一直保留；</li>
<li>GODEBUG的选项设置与go.mod的go version是匹配的。例如，即便你现在的工具链是Go 1.21，如果go.mod中的go version为1.20，那么GODEBUG控制的新特性语义将不起作用，依旧保持Go 1.20时的行为。除非你将go.mod中的go version升级为go 1.21.0；</li>
<li>在Go 1.21及以后版本中，除了可以使用像GODEBUG=panicnil=1的环境变量恢复原先语义外，还可以在main包中使用//go:debug指示符。</li>
</ul>
<p>在向前兼容方面，Russ Cox提出的方案有些复杂难懂，这里就不赘述了，感兴趣的童鞋可以阅读一下我之前的文章《<a href="https://tonybai.com/2023/09/10/understand-go-forward-compatibility-and-toolchain-rule/">聊聊Go语言的向前兼容性和toolchain规则</a>》了解更多细节。</p>
<h4>1.3.1 语法填坑</h4>
<p>在Go的诸多“求变”中，影响最大的还是对已有语法坑的“修正”，这些“填坑”工作或多或少都会对存量代码带去影响，甚至是break change，Go社区的反对声音也是不少。但无论怎样，这些工作已经在Go 1.21版本拉开帷幕了。比如：改变panic(nil)的语义以及对<a href="https://go.dev/blog/loopvar-preview">循环变量语义的变更</a>，大家可以在《<a href="https://tonybai.com/2023/08/20/some-changes-in-go-1-21/">Go 1.21中值得关注的几个变化</a>》一文中了解更多细节。</p>
<p>对现有语法坑的修正也进一步促进了“求新”，比如在修正loopvar语义的同时，for range支持对更多类型表达式的迭代也在进行中，比如Go 1.22中，<a href="https://tonybai.com/2023/12/25/go-1-22-foresight/">for range将支持迭代整型表达式</a>，并以试验特性提供了对函数迭代器的支持。</p>
<h4>1.3.2 标准库v2示范</h4>
<p>Go号称是“自带电池”的语言，其高质量的标准库得到了广大Gopher的欢迎。Go团队也一直努力推进Go标准库功能的丰富性，比如：Go 1.22中对http.ServeMux功能进行了增强，使其像第三方的gorilla/mux那样增加对带有通配符路由的匹配。</p>
<p><a href="https://tonybai.com/2023/12/25/go-1-22-foresight/">Go 1.22中，标准库还首次出现了v2版本包：math/rand/v2</a>，这为后续标准库的vN方式演进提供了示范，从Go团队的官方issue、discussion中了解到，后续如sync/v2、encoding/json/v2等已经列上日程了。</p>
<h2>2. Go所处的位置</h2>
<p>很多人关注Go当前的状态：国内大厂用的多么？小厂是不是也在广泛采纳。这些问题我在往年的Go语言盘点时也都做过梳理，今年就不再提了。没有哪个大厂在广泛采用一门语言后，会在一年内全部推翻重写的；小厂对Go的采纳也是有惯性的。</p>
<p>今年先从我的两个意外“收获”开始。</p>
<h3>2.1 两个意外的“收获”</h3>
<p>2023年10月中旬，世界知名电动车厂商Tesla发布了<a href="https://developer.tesla.com/docs/fleet-api">新版fleet API</a>和<a href="https://github.com/teslamotors/vehicle-command">vehicle command SDK</a>，鉴于本人也在智能网联汽车行业内打拼，于是对Tesla的此次发布做了一些深入了解。在Tesla的github主页上我赫然发现：Go是目前Tesla开源项目的第二大语言。</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-2023-review-of-go-programming-language-3.png" alt="" /></p>
<p>相对于传统的主机厂(车厂)，Telsa算是比较开放的了。开放包含两个含义，一是将车端能力的开放，二是项目的开源。就目前了解到，国内主机厂还鲜有将车端能力开放出来的，开源就更是鲜见。但Tesla在这两方面都做到了，既开放了车端API，又做了针对性的开源，虽然目前其开源项目并不多。以前Tesla涉及到云端服务的项目多用<a href="">Ruby</a>，但从2022年开始，Go语言的使用逐渐增多，包括前面提到的Fleet API的<a href="https://github.com/teslamotors/fleet-telemetry">Fleet Telemetry的参考server实现</a>以及<a href="https://github.com/teslamotors/vehicle-command">Tesla车辆远控SDK</a>。</p>
<p>我们再来看看Apache基金会。众所周知，Apache基金会的开源项目多以Java语言为主，但一次偶然的机会翻看Apache基金会的github项目主页，我发现Go语言在Apache开源项目中已经悄悄地跻身到第五名，如果仅算后端语言的话，Go排名第三，仅次于Java和Python。</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-2023-review-of-go-programming-language-4.png" alt="" /></p>
<p>并且，Apache基金会下面的Go项目实际也不少，大家可以通过https://github.com/orgs/apache/repositories?language=go&amp;type=all查询。其中还不乏优秀之作，比如：<a href="https://github.com/apache/incubator-answer">构建Q&amp;A知识系统的answer</a>、<a href="https://github.com/apache/dubbo-go">Apache Dubbo的go实现dubbo-go</a>、<a href="https://github.com/apache/trafficcontrol">CDN实现trafficcontrol</a>、<a href="https://github.com/apache/camel-k">Kubernetes原生的轻量级企业应用集成框架Camel K</a>、<a href="https://tonybai.com/2023/06/25/a-guide-of-using-apache-arrow-for-gopher-part1">Apache Arrow的Go实现</a>以及<a href="https://github.com/apache/incubator-devlake">针对开发过程的聚合数据平台devlake</a>等。</p>
<p>我们知道：Apache项目在企业级应用和平台方面具有广泛的应用，从Go语言在Apache基金会项目中的使用比例的提升现象来看，Go在企业应用市场中的普及度和受欢迎程度确实有所增长。</p>
<h3>2.2 Go语言排名</h3>
<p>编程语言之间的竞争与争议，通常被称为“编程语言战争”(programming language war)，它其实反映了不同技术群体和范式之间的碰撞。这些“火药味”比较浓的语言之争通常比较主观。近10年来，业界出现了一些被广泛接受的编程语言排行榜，它们基于一些相对客观的数据来反映不同编程语言在现实开发中的真实状态。但不同编程语言排行榜都有不同的数据来源和数据模型，单一的排行榜往往是“盲人摸象”，无法反映全貌。但目前又没有一个可以让我们一窥全貌的权威排行榜。因此，要想更客观地、更全面的反映一门编程语言的实际情况，我们需要将多个排行榜参照着看。</p>
<p>下面我们就来看看在目前世界上著名的编程语言排行榜上，Go语言在其中的最新排名情况(请注意：各个榜单的发布时间不同，导致各榜单的数据会有一定时间差)。</p>
<h4>2.2.1 <a href="https://pypl.github.io/PYPL.html">PYPL编程语言排行榜</a></h4>
<p>PYPL编程语言流行指数是通过分析语言教程在谷歌上的搜索频率而创建的。语言教程被搜索的次数越多，说明该语言越受欢迎，原始数据来自Google Trends：</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-2023-review-of-go-programming-language-5.png" alt="" /><br />
<center>PYPL编程语言排行榜，数据时间：2023.12</center></p>
<h4>2.2.2 <a href="https://spectrum.ieee.org/the-top-programming-languages-2023">IEEE Spectrum排行榜</a></h4>
<p>IEEE Spectrum排行榜是通过调查来自全球软件工程师和招聘网站的数据，统计各语言的流行度的：</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-2023-review-of-go-programming-language-6.png" alt="" /><br />
<center>IEEE Spectrum排行榜，数据时间：2023.8</center></p>
<h4>2.2.3 <a href="https://redmonk.com/sogrady/2023/05/16/language-rankings-1-23/">RedMonk编程语言排行榜</a></h4>
<p>RedMonk排行榜是根据GitHub和Stack Overflow这两个开发者社区上的讨论数量来推算语言的受关注度。</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-2023-review-of-go-programming-language-7.png" alt="" /><br />
<center>RedMonk编程语言排行榜，数据时间：2023.5</center></p>
<h4>2.3.4 <a href="https://github.blog/2023-11-08-the-state-of-open-source-and-ai/">Github Octoverse</a></h4>
<p>GitHub Octoverse排行榜直观反映了过去一年GitHub上各编程语言的实际使用和流行趋势，是从开源项目量的维度来衡量编程语言活跃度的。在Top 10语言榜单上，2023年Go超越Ruby第一次跻身Github Top10语言：</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-2023-review-of-go-programming-language-8.png" alt="" /><br />
<center>Github Octoverse编程语言排行榜，数据时间：2023.11</center></p>
<p><img src="https://tonybai.com/wp-content/uploads/the-2023-review-of-go-programming-language-9.png" alt="" /><br />
<center>Github Octoverse编程语言排行榜，数据时间：2023.11</center></p>
<h4>2.3.5 <a href="https://madnight.github.io/githut/">Github Language Stats(githut)</a></h4>
<p>Github Language Stats是一个个人项目，它基于github公开数据，按时间、pr数量、star数量等维度对各个语言在github上的使用情况进行分析：</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-2023-review-of-go-programming-language-10.png" alt="" /><br />
<center>Githut按PR数量，数据时间：2023第三季度</center></p>
<p><img src="https://tonybai.com/wp-content/uploads/the-2023-review-of-go-programming-language-11.png" alt="" /><br />
<center>Githut按Star数量，数据时间：2023第三季度</center></p>
<h4>2.3.6 <a href="https://www.tiobe.com/tiobe-index/">TIOBE编程语言排行榜</a></h4>
<p>TIOBE编程语言排行榜理论上来说，是世界上最知名的编程语言排行榜，它根据各大搜索引擎编程语言相关的搜索查询量来计算一个综合指数。但这些年TIOBE榜单数据的“上蹿下跳”，让开发者对该榜单是“又爱又恨”。下面是TIOBE index 2023年12月份的榜单：</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-2023-review-of-go-programming-language-12.png" alt="" /></p>
<p>当你看到Fortran排在Go的前面，你就get到该榜单的抽风式的“不靠谱”了:)。</p>
<p>综合上述6个榜单，我们可以看到Go语言的2023基本处于稳定发展状态，没有“大踏步”的前进，也没有意想不到的大幅退步。</p>
<p>今年在国内某乎上总有一些有关“Go在国内是否已凉”的话题，从上面实际情况来看，话题中那些抹黑Go的观点可以不攻自破了。有人会说Rust的强势上升对Go会有一定冲击，这的确不可否认，就像Go当年火速蹿升给Java带去一定冲击一样，这是一门编程语言在演进阶段必会经历的过程，没有什么值得大惊小怪的。5年后，Rust可能同样也会受到来自其他语言的冲击。</p>
<p>Go语言未来会变得如何，关键还要看Go团队对Go未来演进方向的把握是否得当以及Go社区与生态是否给力。2023年，Go团队也明确了未来的演进机制和策略，接下来我们就来看看。</p>
<h2>3. Go的未来演进</h2>
<p>2023年是<a href="https://tonybai.com/2023/11/11/go-opensource-14-years/">Go语言开源的第14个年头</a>，Go语言早已蜕下了少年期的青涩，进入到了青年期。这意味着它拥有了越来越成熟稳定的语言特性，同时生态系统也日益丰富完善。作为一门青壮年语言，Go语言在系统设计方面展现出的高度工程化思想，使其轻松应对复杂系统的构建。以go module为主的模块化支持帮助大规模程序更加清晰化，丰富的并发控制手段使其可以处理海量请求。与此同时，Go语言生态也在蓬勃成长——各种高质量框架应运而生，无数module可复用，大量的云原生组件可供选择。这为开发者极大减轻了从零开始搭建系统的工作量。</p>
<p>和我们人类一样，一门语言进入青年期后的成熟特征并不能完全掩饰其未来演进的迷茫！在Ken Thompson、Rob Pike相继退休后，Russ Cox成为了Go这艘大船的“掌舵者”，Russ Cox与Go团队对编程语言的思考，对Go语言价值观的判断将直接决定Go未来的航向。</p>
<p>好在，在2023年的GopherCon大会上，我们得到了Russ Cox的答案：那就是<a href="https://tonybai.com/2023/12/10/go-changes/">基于共同目标和数据驱动的决策</a>。这里借用Russ Cox在演讲中给出的结论来看看具体的演进驱动机制：</p>
<ul>
<li>首先，Go需要不断变化，特别是随着计算世界的变化。</li>
<li>其次，任何改变的目标都是为了使Go在软件工程中变得更好，尤其是在规模化(scaling)方面。</li>
<li>第三，一旦我们确定了目标，达成共识的下一个最重要的部分是拥有共享数据来做出决策。</li>
<li>第四，Go工具链遥测是增补我们现有调查和代码分析数据的重要数据来源。</li>
</ul>
<p>综上来看，Go团队要“拥抱变化”，但不能“无头苍蝇”一样的胡乱改变，而是严谨地基于广泛的数据反馈，包括来自用户调查、vscode插件运行的用户反馈、全年进行的研究访谈和用户体验研究等，以及来自即将<a href="https://research.swtch.com/telemetry">加入Go工具链的可选遥测(opt-in Telemetry)功能</a>获取到的更多真实的Go使用数据。</p>
<p>相信在Go工具链的可选遥测(opt-in Telemetry)功能加入后，Go团队能基于这些用户数据拿到更准确地决策依据，继续让Go这艘大船行驶在正确、光明的航向上！</p>
<h2>4. 小结</h2>
<p>在2023年，Go语言继续保持了其稳定性和可靠性的特点。发布了两个大版本，Go 1.20和Go 1.21，其中语法特性的改变相对较少，注重修复和优化。然而，Go语言在其他方面仍然保持着求新和求变的态势。</p>
<p>Go语言团队致力于优化编译器、工具链、运行时和标准库，以提升生产力和运行时效率。引入了一些新的特性和优化措施，例如PGO（profile-guided optimization）技术的引入和优化、内存管理方面的改进等。同时，Go语言在软件工程领域也进行了一些创新，如简化项目创建的gonew工具、代码覆盖率的采集工具、供应链安全领域的govulncheck工具等。</p>
<p>Go语言始终保持对新技术、新趋势和社区的开放姿态。在2023年，Go语言对WASM和WASI的支持得到了进一步加强。同时，Go社区也积极响应并跟随Go团队的步伐，面对IT界出现的大语言模型等新兴技术，Go社区也在不断探索和应用。</p>
<p>总体而言，2023年对于Go语言来说是一个稳中求新、稳中求变的年份。Go语言保持着其简洁、高效和易用的特点，同时积极适应和采纳新的技术和需求，为开发者提供更好的开发体验和工具支持。</p>
<p>展望未来，Go团队已经明确了更加以共识和用户数据为驱动的演进机制，保证Go的发展方向与实际需求保持同步。随着可选的工具链遥测功能加入，相信他们能基于更丰富的用户数据做出更正确、更具预见性的正确决策。</p>
<p>我个人依旧坚持我之前的判断：<strong>Go将进入或已处于自己的黄金5-10年</strong>。</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/31/the-2023-review-of-go-programming-language/feed/</wfw:commentRss>
		<slash:comments>0</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>使用testify包辅助Go测试指南</title>
		<link>https://tonybai.com/2023/07/16/the-guide-of-go-testing-with-testify-package/</link>
		<comments>https://tonybai.com/2023/07/16/the-guide-of-go-testing-with-testify-package/#comments</comments>
		<pubDate>Sun, 16 Jul 2023 07:09:56 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[apache-arrow]]></category>
		<category><![CDATA[Arrow]]></category>
		<category><![CDATA[Assert]]></category>
		<category><![CDATA[Check]]></category>
		<category><![CDATA[CheckEqual]]></category>
		<category><![CDATA[DeepEqual]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gotest]]></category>
		<category><![CDATA[Go语言第一课]]></category>
		<category><![CDATA[Go语言精进之路]]></category>
		<category><![CDATA[grank.io]]></category>
		<category><![CDATA[Mock]]></category>
		<category><![CDATA[mockery]]></category>
		<category><![CDATA[quick]]></category>
		<category><![CDATA[Reflect]]></category>
		<category><![CDATA[require]]></category>
		<category><![CDATA[Setup]]></category>
		<category><![CDATA[stake]]></category>
		<category><![CDATA[Stub]]></category>
		<category><![CDATA[suite]]></category>
		<category><![CDATA[teardown]]></category>
		<category><![CDATA[testcase]]></category>
		<category><![CDATA[testify]]></category>
		<category><![CDATA[Testing]]></category>
		<category><![CDATA[testsuite]]></category>
		<category><![CDATA[uber]]></category>
		<category><![CDATA[unit-test]]></category>
		<category><![CDATA[xUnit]]></category>
		<category><![CDATA[反射]]></category>
		<category><![CDATA[标准库]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3942</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2023/07/16/the-guide-of-go-testing-with-testify-package 我虽然算不上Go标准库的“清教徒”，但在测试方面还多是基于标准库testing包以及go test框架的，除了需要mock的时候，基本上没有用过第三方的Go测试框架。我在《Go语言精进之路》一书中对Go测试组织的讲解也是基于Go testing包和go test框架的。 最近看Apache arrow代码，发现arrow的Go实现使用了testify项目组织和辅助测试： // compute/vector_hash_test.go func TestHashKernels(t *testing.T) { suite.Run(t, &#38;PrimitiveHashKernelSuite[int8]{}) suite.Run(t, &#38;PrimitiveHashKernelSuite[uint8]{}) suite.Run(t, &#38;PrimitiveHashKernelSuite[int16]{}) suite.Run(t, &#38;PrimitiveHashKernelSuite[uint16]{}) ... ... } type PrimitiveHashKernelSuite[T exec.IntTypes &#124; exec.UintTypes &#124; constraints.Float] struct { suite.Suite mem *memory.CheckedAllocator dt arrow.DataType } func (ps *PrimitiveHashKernelSuite[T]) SetupSuite() { ps.dt = exec.GetDataType[T]() } func (ps *PrimitiveHashKernelSuite[T]) SetupTest() { [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/the-guide-of-go-testing-with-testify-package-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2023/07/16/the-guide-of-go-testing-with-testify-package">本文永久链接</a> &#8211; https://tonybai.com/2023/07/16/the-guide-of-go-testing-with-testify-package</p>
<p>我虽然算不上Go标准库的“清教徒”，但在测试方面还多是基于标准库testing包以及go test框架的，除了需要mock的时候，基本上没有用过第三方的Go测试框架。我在<a href="https://book.douban.com/subject/35720729/">《Go语言精进之路》</a>一书中对Go测试组织的讲解也是基于Go testing包和go test框架的。</p>
<p>最近看Apache arrow代码，发现arrow的Go实现使用了<a href="https://github.com/stretchr/testify">testify项目</a>组织和辅助测试：</p>
<pre><code>// compute/vector_hash_test.go

func TestHashKernels(t *testing.T) {
    suite.Run(t, &amp;PrimitiveHashKernelSuite[int8]{})
    suite.Run(t, &amp;PrimitiveHashKernelSuite[uint8]{})
    suite.Run(t, &amp;PrimitiveHashKernelSuite[int16]{})
    suite.Run(t, &amp;PrimitiveHashKernelSuite[uint16]{})
    ... ...
}

type PrimitiveHashKernelSuite[T exec.IntTypes | exec.UintTypes | constraints.Float] struct {
    suite.Suite

    mem *memory.CheckedAllocator
    dt  arrow.DataType
}

func (ps *PrimitiveHashKernelSuite[T]) SetupSuite() {
    ps.dt = exec.GetDataType[T]()
}

func (ps *PrimitiveHashKernelSuite[T]) SetupTest() {
    ps.mem = memory.NewCheckedAllocator(memory.DefaultAllocator)
}

func (ps *PrimitiveHashKernelSuite[T]) TearDownTest() {
    ps.mem.AssertSize(ps.T(), 0)
}

func (ps *PrimitiveHashKernelSuite[T]) TestUnique() {
    ... ...
}
</code></pre>
<p>同期，我在<a href="https://www.grank.io/">grank.io</a>上看到testify这个项目综合排名第一：</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-guide-of-go-testing-with-testify-package-2.png" alt="" /></p>
<p>这说明testify项目在Go社区有着广泛的受众，testify为何能从众多go test第三方框架中脱颖而出？它有哪些与众不同的地方？如何更好地利用testify来辅助我们的Go测试？带着这些问题，我写下了这篇有关testify的文章，供大家参考。</p>
<h2>1. testify简介</h2>
<p>testify是一个用于Go语言的测试框架，与go testing包可以很好的融合在一起，并由go test驱动运行。testify提供的功能特性可以辅助Go开发人员更好地组织和更高效地编写测试用例，以保证软件的质量和可靠性。</p>
<p>testify能够得到社区的广泛接纳，与testify项目中<strong>包的简洁与独立的设计</strong>是密不可分的。下面是testify包的目录结构(去掉了用于生成代码的codegen和已经deprecated的http目录后)：</p>
<pre><code>$tree -F -L 1 testify |grep "/" |grep -v codegen|grep -v http
├── assert/
├── mock/
├── require/
└── suite/
</code></pre>
<blockquote>
<p>关于Go项目代码布局设计的系统讲解，可以参见我的<a href="http://gk.link/a/10AVZ">《Go语言第一课》专栏</a>的第5讲。</p>
</blockquote>
<p>包目录名直接反映了testify可以提供给Go开发者的功能特性：</p>
<ul>
<li>assert和require：断言工具包，辅助做测试结果判定；</li>
<li>mock：辅助编写mock test的工具包；</li>
<li>suite：提供了suite这一层的测试组织结构。</li>
</ul>
<p>下面我们就<strong>由浅入深</strong>的介绍testify的这几个重要的、可各自独立使用的包。我们先从<strong>使用门槛最低</strong>的assert包和require包开始，它们是一类的，这里放在一个章节中介绍。</p>
<h2>2. assert和require包</h2>
<p>我们在使用go testing包编写Go单元测试用例时，通常会用下面代码来判断目标函数执行结果是否符合预期：</p>
<pre><code>func TestFoo(t *testing.T) {
    v := Foo(5, 6) // Foo为被测目标函数
    if v != expected {
        t.Errorf("want %d, actual %d\n", expected, v)
    }
}
</code></pre>
<p>这样，如果测试用例要判断的结果很多，那么测试代码中就会存在很多if xx != yy以及Errorf/Fatalf之类的代码。有过一些其他语言编程经验的童鞋此时此刻肯定会说：<strong>是时候上assert了</strong>! 不过很遗憾，Go标准库包括其<a href="https://github.com/golang/exp">实验库(exp)</a>都没有提供带有assert断言机制的包。</p>
<blockquote>
<p>注：Go标准库testing/quick包中提供的Check和CheckEqual并非assert，它们用于测试两个函数参数在相同输入的情况下是否有相同的输出。如果不同，则输出导致输出不同的输入。此外，该quick包已经frozen，不再接受新Feature。</p>
</blockquote>
<p>testify为Go开发人员提供了assert包，为Go开发人员很大程度“解了近渴”。</p>
<p>assert包使用起来非常简单，下面是assert使用的常见场景示例：</p>
<pre><code>// assert/assert_test.go

func Add(a, b int) int {
    return a + b
}

func TestAssert(t *testing.T) {
    // Equal断言
    assert.Equal(t, 4, Add(1, 3), "The result should be 4")

    sl1 := []int{1, 2, 3}
    sl2 := []int{1, 2, 3}
    sl3 := []int{2, 3, 4}
    assert.Equal(t, sl1, sl2, "sl1 should equal to sl2 ")

    p1 := &amp;sl1
    p2 := &amp;sl2
    assert.Equal(t, p1, p2, "the content which p1 point to should equal to which p2 point to")

    err := errors.New("demo error")
    assert.EqualError(t, err, "demo error")

    // assert.Exactly(t, int32(123), int64(123)) // failed! both type and value must be same

    // 布尔断言
    assert.True(t, 1+1 == 2, "1+1 == 2 should be true")
    assert.Contains(t, "Hello World", "World")
    assert.Contains(t, []string{"Hello", "World"}, "World")
    assert.Contains(t, map[string]string{"Hello": "World"}, "Hello")
    assert.ElementsMatch(t, []int{1, 3, 2, 3}, []int{1, 3, 3, 2})

    // 反向断言
    assert.NotEqual(t, 4, Add(2, 3), "The result should not be 4")
    assert.NotEqual(t, sl1, sl3, "sl1 should not equal to sl3 ")
    assert.False(t, 1+1 == 3, "1+1 == 3 should be false")
    assert.Never(t, func() bool { return false }, time.Second, 10*time.Millisecond) //1秒之内condition参数都不为true，每10毫秒检查一次
    assert.NotContains(t, "Hello World", "Go")
}
</code></pre>
<p>我们看到assert包提供了Equal类、布尔类、反向类断言，assert包提供的断言函数有几十种，这里无法一一枚举，选择最适合你的测试场景的断言就好。</p>
<p>另外要注意的是，在Equal对切片作比较时，比较的是切片底层数组存储的内容是否相等；对指针作比较时，比较的是指针指向的内存块儿的数据是否相等，而不是指针本身的值是否相等。</p>
<blockquote>
<p>注：assert.Equal底层实现使用的是reflect.DeepEqual。</p>
</blockquote>
<p>我们看到assert包提供的断言函数第一个参数是testing.T的实例，如果一个测试用例里多次使用assert包的断言函数，我们每次都要传入testing.T的实例，比如下面示例：</p>
<pre><code>// assert/assert_test.go

func TestAdd1(t *testing.T) {
    result := Add(1, 3)
    assert.Equal(t, 4, result, "The result should be 4")
    result = Add(2, 2)
    assert.Equal(t, 4, result, "The result should be 4")
    result = Add(2, 3)
    assert.Equal(t, 5, result, "The result should be 5")
    result = Add(0, 3)
    assert.Equal(t, 3, result, "The result should be 3")
    result = Add(-1, 1)
    assert.Equal(t, 0, result, "The result should be 0")
}
</code></pre>
<p>这很verbose! assert包提供了替代方法，如下面示例：</p>
<pre><code>// assert/assert_test.go

func TestAdd2(t *testing.T) {
    assert := assert.New(t)

    result := Add(1, 3)
    assert.Equal(4, result, "The result should be 4")
    result = Add(2, 2)
    assert.Equal(4, result, "The result should be 4")
    result = Add(2, 3)
    assert.Equal(5, result, "The result should be 5")
    result = Add(0, 3)
    assert.Equal(3, result, "The result should be 3")
    result = Add(-1, 1)
    assert.Equal(0, result, "The result should be 0")
}
</code></pre>
<blockquote>
<p>注：我们当然可以使用表驱动测试的方法将上述示例做进一步优化。</p>
</blockquote>
<p>require包可以理解为assert包的“姊妹包”，require包实现了assert包提供的所有导出的断言函数，因此我们将上述示例中的assert改为require后，代码可以正常编译和运行(见require/require_test.go)。</p>
<p>那么require包与assert包有什么不同呢？我们来简单看一下。</p>
<p>使用assert包的断言时，如果某一个断言失败，该失败不会影响到后续测试代码的执行，或者说后续测试代码会继续执行，比如我们故意将TestAssert中的一些断言条件改为失败：</p>
<pre><code>// assert/assert_test.go

    assert.True(t, 1+1 == 3, "1+1 == 2 should be true")
    assert.Contains(t, "Hello World", "World1")
</code></pre>
<p>再运行assert_test.go中的测试，我们会看到下面结果：</p>
<pre><code>$go test
--- FAIL: TestAssert (1.00s)
    assert_test.go:34:
            Error Trace:
            Error:          Should be true
            Test:           TestAssert
            Messages:       1+1 == 2 should be true
    assert_test.go:35:
            Error Trace:
            Error:          "Hello World" does not contain "World1"
            Test:           TestAssert
FAIL
exit status 1
FAIL    demo    1.016s
</code></pre>
<p>我们看到：两个失败的测试断言都输出了！</p>
<p>我们再换到require/require_test.go下做同样的修改，并执行go test，我们得到如下结果：</p>
<pre><code>$go test require_test.go
--- FAIL: TestRequire (0.00s)
    require_test.go:34:
            Error Trace:
            Error:          Should be true
            Test:           TestRequire
            Messages:       1+1 == 2 should be true
FAIL
FAIL    command-line-arguments  0.012s
FAIL
</code></pre>
<p>我们看到当执行完第一条失败的断言后，测试便结束了！</p>
<p>这就是assert包和require包的区别！这有些类似于Errorf和Fatalf的区别！require包中断言函数一旦执行失败便会导致测试退出，后续的测试代码将无法继续执行。</p>
<p>另外require包还有一个“特点”，那就是它的主体代码(require.go和require_forward.go)都是自动生成的：</p>
<pre><code>// github.com/stretchr/testify/require/reqire.go
/*
  CODE GENERATED AUTOMATICALLY WITH github.com/stretchr/testify/_codegen
* THIS FILE MUST NOT BE EDITED BY HAND
 */
</code></pre>
<p>testify的代码生成采用了基于模板的方法，具体的自动生成原理可以参考[《A case for Go code generation: testify》] (https://levelup.gitconnected.com/a-case-for-go-code-generation-testify-73a4b0d46cb1)这篇文章。</p>
<h2>3. suite包</h2>
<p>Go testing包没有引入testsuite(测试套件)或testcase(测试用例)的概念，只有Test和<a href="https://tonybai.com/2023/03/15/an-intro-of-go-subtest/">SubTest</a>。对于熟悉xUnit那套测试组织方式的开发者来说，这种缺失很“别扭”！要么自己基于testing包来构建这种结构，要么使用第三方包的实现。</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-guide-of-go-testing-with-testify-package-3.jpg" alt="" /><br />
<center>该图来自网络</center></p>
<p>testify的suite包为我们提供了一种基于suite/case结构组织测试代码的方式。下面是一个可以对testify suite定义的suite结构进行全面解析的示例(改编自testify suite包文档中的ExampleTestSuite示例)：</p>
<pre><code>// suite/suite_test.go

package main

import (
    "fmt"
    "testing"

    "github.com/stretchr/testify/suite"
)

type ExampleSuite struct {
    suite.Suite
    indent int
}

func (suite *ExampleSuite) indents() (result string) {
    for i := 0; i &lt; suite.indent; i++ {
        result += "----"
    }
    return
}

func (suite *ExampleSuite) SetupSuite() {
    fmt.Println("Suite setup")
}

func (suite *ExampleSuite) TearDownSuite() {
    fmt.Println("Suite teardown")
}

func (suite *ExampleSuite) SetupTest() {
    suite.indent++
    fmt.Println(suite.indents(), "Test setup")
}

func (suite *ExampleSuite) TearDownTest() {
    fmt.Println(suite.indents(), "Test teardown")
    suite.indent--
}

func (suite *ExampleSuite) BeforeTest(suiteName, testName string) {
    suite.indent++
    fmt.Printf("%sBefore %s.%s\n", suite.indents(), suiteName, testName)
}

func (suite *ExampleSuite) AfterTest(suiteName, testName string) {
    fmt.Printf("%sAfter %s.%s\n", suite.indents(), suiteName, testName)
    suite.indent--
}

func (suite *ExampleSuite) SetupSubTest() {
    suite.indent++
    fmt.Println(suite.indents(), "SubTest setup")
}

func (suite *ExampleSuite) TearDownSubTest() {
    fmt.Println(suite.indents(), "SubTest teardown")
    suite.indent--
}

func (suite *ExampleSuite) TestCase1() {
    suite.indent++
    defer func() {
        fmt.Println(suite.indents(), "End TestCase1")
        suite.indent--
    }()

    fmt.Println(suite.indents(), "Begin TestCase1")

    suite.Run("case1-subtest1", func() {
        suite.indent++
        fmt.Println(suite.indents(), "Begin TestCase1.Subtest1")
        fmt.Println(suite.indents(), "End TestCase1.Subtest1")
        suite.indent--
    })
    suite.Run("case1-subtest2", func() {
        suite.indent++
        fmt.Println(suite.indents(), "Begin TestCase1.Subtest2")
        fmt.Println(suite.indents(), "End TestCase1.Subtest2")
        suite.indent--
    })
}

func (suite *ExampleSuite) TestCase2() {
    suite.indent++
    defer func() {
        fmt.Println(suite.indents(), "End TestCase2")
        suite.indent--
    }()
    fmt.Println(suite.indents(), "Begin TestCase2")

    suite.Run("case2-subtest1", func() {
        suite.indent++
        fmt.Println(suite.indents(), "Begin TestCase2.Subtest1")
        fmt.Println(suite.indents(), "End TestCase2.Subtest1")
        suite.indent--
    })
}

func TestExampleSuite(t *testing.T) {
    suite.Run(t, new(ExampleSuite))
}
</code></pre>
<p>要知道testify.suite包定义的测试结构是什么样的，我们运行一下上述代码即可：</p>
<pre><code>$go test
Suite setup
---- Test setup
--------Before ExampleSuite.TestCase1
------------ Begin TestCase1
---------------- SubTest setup
-------------------- Begin TestCase1.Subtest1
-------------------- End TestCase1.Subtest1
---------------- SubTest teardown
---------------- SubTest setup
-------------------- Begin TestCase1.Subtest2
-------------------- End TestCase1.Subtest2
---------------- SubTest teardown
------------ End TestCase1
--------After ExampleSuite.TestCase1
---- Test teardown
---- Test setup
--------Before ExampleSuite.TestCase2
------------ Begin TestCase2
---------------- SubTest setup
-------------------- Begin TestCase2.Subtest1
-------------------- End TestCase2.Subtest1
---------------- SubTest teardown
------------ End TestCase2
--------After ExampleSuite.TestCase2
---- Test teardown
Suite teardown
</code></pre>
<p>信息量很大，我们慢慢说！</p>
<p>利用testify建立测试套件，我们需要<strong>自行定义嵌入了suite.Suite的结构体类型</strong>，如上面示例中的ExampleSuite。</p>
<p>testify与go testing兼容，由go test驱动执行，因此我们需要在一个TestXXX函数中创建ExampleSuite的实例，调用suite包的Run函数，并将执行权交给suite包的这个Run函数，后续的执行逻辑就是suite包Run函数的执行逻辑。在上述代码中，我们只定义了一个TestXXX，并使用suite.Run函数执行了ExampleSuite中的所有测试用例。</p>
<p>suite.Run函数的执行逻辑大致是：通过<a href="https://tonybai.com/2023/06/04/reflection-programming-guide-in-go">反射机制</a>得到了&#42;ExampleSuite类型的方法集合，并执行方法集合中名字以Test为前缀的所有方法。testify将用户自定义的XXXSuite类型中的每个以Test为前缀的方法当作是一个TestCase。</p>
<p>除了Suite和TestCase的概念外，testify.suite包还“预埋”了很多回调点，包括suite的Setup、TearDown；test case的Setup和TearDown、testcase的before和after；subtest的Setup和TearDown，这些回调点也由suite.Run函数来执行，回调点的执行顺序可以通过上面示例的执行结果看到。</p>
<blockquote>
<p>注意：subtest要通过XXXSuite的Run方法执行，而不要通过标准库testing.T的Run方法执行。</p>
</blockquote>
<p>我们知道：go test工具可以通过-run命令行参数来选择要执行的TestXXX函数，考虑到testify使用TestXXX函数拉起测试套件(XXXSuite)，因此从testify视角来看，通过go test -run可以选择执行哪个XXXSuite，前提是一个TestXXX中仅初始化和运行一种XXXSuite的所有测试用例。</p>
<p>如果要选择XXXSuite的方法(即testify眼中的测试用例)，我们不能用-run了，需要使用testify新增的-m命令行选项，下面是一个仅执行带有Case2关键字测试用例的示例：</p>
<pre><code>$go test -testify.m Case2
Suite setup
---- Test setup
--------Before ExampleSuite.TestCase2
------------ Begin TestCase2
---------------- SubTest setup
-------------------- Begin TestCase2.Subtest1
-------------------- End TestCase2.Subtest1
---------------- SubTest teardown
------------ End TestCase2
--------After ExampleSuite.TestCase2
---- Test teardown
Suite teardown
PASS
ok      demo    0.014s
</code></pre>
<p>综上，如果你使用testify的Suite/Case概念来组织你的测试代码，建议在每个TestXXX中仅初始化和运行一个XXXSuite，这样你可以通过-run选择特定的Suite执行。</p>
<h2>4. mock包</h2>
<p>最后我们来看看testify为辅助Go开发人员编写测试代码而提供的一个高级特性：mock。</p>
<p>在之前的文章中，我提到过：<a href="https://tonybai.com/2023/04/20/provide-fake-object-for-external-collaborators/">尽量使用fake object，而不是mock object</a>。mock这种测试替身有其难于理解、使用场合局限以及给予开发人员信心不足等弊端。</p>
<blockquote>
<p>注：近期原Go官方维护的<a href="https://github.com/golang/mock">golang/mock</a>也将维护权迁移给了uber，迁移后的新的mock库为<a href="https://github.com/uber/mock">go.uber.org/mock</a>。我在<a href="https://book.douban.com/subject/35720729/">《Go语言精进之路 vol2》</a>一书中对golang/mock做过详细的使用介绍，有兴趣的朋友可以去读一读。</p>
</blockquote>
<p>但“存在即合理”，显然mock也有它的用武空间，在社区也有它的拥趸，既然testify提供了mock包，这里就简单介绍一下它的基本使用方法。</p>
<p>我们用一个经典repo service的例子来演示如何使用testify mock，如下面代码示例：</p>
<pre><code>// mock/mock_test.go

type User struct {
    ID   int
    Name string
    Age  int
}

type UserRepository interface {
    CreateUser(user *User) (int, error)
    GetUserById(id int) (*User, error)
}

type UserService struct {
    repo UserRepository
}

func NewUserService(repo UserRepository) *UserService {
    return &amp;UserService{repo: repo}
}

func (s *UserService) CreateUser(name string, age int) (*User, error) {
    user := &amp;User{Name: name, Age: age}
    id, err := s.repo.CreateUser(user)
    if err != nil {
        return nil, err
    }
    user.ID = id
    return user, nil
}

func (s *UserService) GetUserById(id int) (*User, error) {
    return s.repo.GetUserById(id)
}
</code></pre>
<p>我们要提供一个UserService服务，通过该服务可以创建User，也可以通过ID获取User信息。服务的背后是一个UserRepository，你可以用任何方法实现UserRepository，为此我们将其抽象为一个接口UserRepository。UserService要依赖UserRepository才能让它的两个方法CreateUser和GetUserById正常工作。现在我们要测试UserService的这两个方法，但我们手里没有现成的UserRepository实现可用，我们也没有UserRepository的fake object。</p>
<p>这时我们仅能用mock。下面是使用testify mock给出的UserRepository接口的实现UserRepositoryMock：</p>
<pre><code>// mock/mock_test.go

type UserRepositoryMock struct {
    mock.Mock
}

func (m *UserRepositoryMock) CreateUser(user *User) (int, error) {
    args := m.Called(user)
    return args.Int(0), args.Error(1)
}

func (m *UserRepositoryMock) GetUserById(id int) (*User, error) {
    args := m.Called(id)
    return args.Get(0).(*User), args.Error(1)
}
</code></pre>
<p>我们基于mock.Mock创建一个新结构体类型UserRepositoryMock，这就是我们要创建的模拟UserRepository。我们实现了它的两个方法，与正常方法实现不同的是，在方法中我们使用的是mock.Mock提供的方法Called以及它的返回值来满足CreateUser和GetUserById两个方法的参数与返回值要求。</p>
<p>UserRepositoryMock这两个方法的实现是比较“模式化”的，其中调用的Called接收了外部方法的所有参数，然后通过Called的返回值args来构造满足外部方法的返回值。返回值构造的书写格式如下：</p>
<pre><code>args.&lt;ReturnValueType&gt;(&lt;index&gt;) // 其中index从0开始
</code></pre>
<p>以CreateUser为例，它有两个返回值int和error，那按照上面的书写格式，我们的返回值就应该为：args.int(0)和args.Error(1)。</p>
<p>对于复杂结构的返回值类型T，可使用断言方式，书写格式变为：</p>
<pre><code>args.Get(index).(T)
</code></pre>
<p>再以构造GetUserById的返回值&#42;User和error为例，我们按照复杂返回值构造的书写格式来编写，返回值就应该为args.Get(0).(*User)和args.Error(1)。</p>
<p>有了Mock后的UserRepository，我们就可以来编写UserService的方法的测试用例了：</p>
<pre><code>// mock/mock_test.go

func TestUserService_CreateUser(t *testing.T) {
    repo := new(UserRepositoryMock)
    service := NewUserService(repo)

    user := &amp;User{Name: "Alice", Age: 30}
    repo.On("CreateUser", user).Return(1, nil)

    createdUser, err := service.CreateUser(user.Name, user.Age)

    assert.NoError(t, err)
    assert.Equal(t, 1, createdUser.ID)
    assert.Equal(t, "Alice", createdUser.Name)
    assert.Equal(t, 30, createdUser.Age)

    repo.AssertExpectations(t)
}

func TestUserService_GetUserById(t *testing.T) {
    repo := new(UserRepositoryMock)
    service := NewUserService(repo)

    user := &amp;User{ID: 1, Name: "Alice", Age: 30}
    repo.On("GetUserById", 1).Return(user, nil)

    foundUser, err := service.GetUserById(1)

    assert.NoError(t, err)
    assert.Equal(t, 1, foundUser.ID)
    assert.Equal(t, "Alice", foundUser.Name)
    assert.Equal(t, 30, foundUser.Age)

    repo.AssertExpectations(t)
}
</code></pre>
<p>这两个TestXXX函数的编写模式也十分相近，以TestUserService_GetUserById为例，它先创建了UserRepositoryMock和UserService的实例，然后利用UserRepositoryMock来设置即将被调用的GetUserById方法的输入参数与返回值：</p>
<pre><code>user := &amp;User{ID: 1, Name: "Alice", Age: 30}
repo.On("GetUserById", 1).Return(user, nil)
</code></pre>
<p>这样当GetUserById在service.GetUserById方法中被调用时，它返回的就是上面设置的user地址值和nil。</p>
<p>之后，我们像常规测试用例那样，用assert包对返回的值与预期值做断言即可。</p>
<h2>5. 小结</h2>
<p>在本文中，我们讲解了testify这个第三方辅助测试包的结构，并针对其中的assert/require、suite和mock这几个相对独立的Go包的用法做了重点说明。</p>
<p>assert/require包是功能十分全面的测试断言包，即便你不使用suite、mock，你也可以单独使用assert/require包来减少你的测试代码中if != xxx的书写行数。</p>
<p>suite包则为我们提供了一个类xUnit的Suite/Case的测试代码组织形式的实现方案，并且这种方案与go testing包兼容，由go test驱动。</p>
<p>虽然我不建议用mock，但testify mock也实现了mock机制的基本功能。并且文中没有提及的是，结合<a href="https://vektra.github.io/mockery/latest/">mockery</a>工具和testify mock，我们可以针对接口为被测目标自动生成testify的mock部分代码，这会大大提交mock test的编写效率。</p>
<p>综上来看，testify这个项目的确非常有用，可以很好的辅助Go开发者高效的编写和组织测试用例。目前testify正在策划<a href="https://docs.google.com/forms/d/e/1FAIpQLScQweSh4N4QqK3JESHTNyHjx0-lMApCK1--GvbXlB3dKyydeg/">dev v2版本</a> ，相信不久将来落地的v2版本能给Go开发者带来更多的帮助。</p>
<p>本文涉及到的源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/testify-examples">这里</a>下载。</p>
<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/16/the-guide-of-go-testing-with-testify-package/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>Apache Arrow：驱动列式分析性能和连接性的提升[译]</title>
		<link>https://tonybai.com/2023/07/01/arrow-columnar-analytics/</link>
		<comments>https://tonybai.com/2023/07/01/arrow-columnar-analytics/#comments</comments>
		<pubDate>Sat, 01 Jul 2023 14:42:29 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Apache]]></category>
		<category><![CDATA[Arrow]]></category>
		<category><![CDATA[Arrow-Native]]></category>
		<category><![CDATA[ArrowFlight]]></category>
		<category><![CDATA[bigdata]]></category>
		<category><![CDATA[cloudera]]></category>
		<category><![CDATA[Commiter]]></category>
		<category><![CDATA[Cpp]]></category>
		<category><![CDATA[DataFrames]]></category>
		<category><![CDATA[DataFusion]]></category>
		<category><![CDATA[dremio]]></category>
		<category><![CDATA[Drill]]></category>
		<category><![CDATA[DuckDB]]></category>
		<category><![CDATA[FlightSQL]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Google]]></category>
		<category><![CDATA[HBase]]></category>
		<category><![CDATA[HuggingFace]]></category>
		<category><![CDATA[Impala]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[JDBC]]></category>
		<category><![CDATA[JeffDean]]></category>
		<category><![CDATA[Kudu]]></category>
		<category><![CDATA[mapreduce]]></category>
		<category><![CDATA[NumPy]]></category>
		<category><![CDATA[ODBC]]></category>
		<category><![CDATA[pandas]]></category>
		<category><![CDATA[parquet]]></category>
		<category><![CDATA[PMC]]></category>
		<category><![CDATA[ProtobufBuffer]]></category>
		<category><![CDATA[PySpark]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[PyTorch]]></category>
		<category><![CDATA[R]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[Scala]]></category>
		<category><![CDATA[SnowFlake]]></category>
		<category><![CDATA[spark]]></category>
		<category><![CDATA[SparkSQL]]></category>
		<category><![CDATA[Substrait]]></category>
		<category><![CDATA[TensorFlow]]></category>
		<category><![CDATA[thrift]]></category>
		<category><![CDATA[UDF]]></category>
		<category><![CDATA[大数据]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3924</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2023/07/01/arrow-columnar-analytics 本文翻译自Voltron Data公司CTO Wes McKinney的文章《Apache Arrow: Driving Columnar Analytics Performance and Connectivity》。这篇文章回顾了现代大数据分析遇到的问题、Arrow项目的起源、生态发展以及对未来的展望。 以下是正文部分。 引言 自MapReduce以来，大数据已经走了很长一段路。Jeffrey Dean和Sanjay Ghemawat在2004年发表于Google的论文催生了Apache Hadoop开源项目，以及一系列其他新项目，这些项目是因大量开发人员有捕获，存储和处理非常大的数据集的需求而创建的。 图：大数据演进的3V(Volume、Variety和Velocity) 虽然像Hadoop这样的第一个MapReduce框架能够处理大型数据集，但它们是为了大规模弹性（通过将每个处理步骤的结果写回分布式存储）而设计的，而并未过多考虑性能。Apache Spark于2010年首次发布，因其基于容错分布式内存处理的新架构而脱颖而出。Spark的核心是用Scala实现的，Scala是Java虚拟机（JVM）的编程语言。Spark为其他编程语言提供了binding实现，例如 C# .NET、Java、Python （PySpark） 和 R（SparkR 和 sparklyr），这有助于Spark在众多编程语言开发者社区的普及使用。 图：数据处理生态系统演进 在过去的十年中，像Python和R这样的解释式编程语言已经不再局限于其在科学计算和学术统计中的利基市场，一跃发展成为现代数据科学，商业分析和AI的主流工具。这些编程语言完全主导了“笔记本电脑规模”的数据处理工作。像Hadoop和Spark这样的大规模数据处理框架为Python等解释型语言提供了编程接口，但与JVM上运行的“本机”接口相比，使用这些语言绑定的性能和资源利用率通常都很差。 解释型语言在使用主流大数据系统时所付出的性能损失主要源于数据互操作性问题。为了将数据从Java应用程序的核心运行时传递给用户的自定义Python函数（“用户定义函数”或“UDF”），必须将数据转换为可以Python所接受的格式，然后再转换为内置的Python对象，如列表、字典或基于数组的对象，如pandas DataFrames。更糟糕的是，许多框架，包括Spark和Hadoop，最初只为用户定义函数提供“一次一值”的执行模型，其中NumPy或pandas等工具则提供了“一次一数组”的执行模型，以避免Python解释器的开销。数据转换和解释器的双重昂贵开销使得Python基于大数据框架进行大规模数据处理变得愈加不现实。 Apache Spark通过引入Spark DataFrames来改善与Python的一些语言互操作性问题，Spark DataFrames是Spark SQL的一种新的类似pandas的API，它无需在Spark运行时和Python之间传输数据。不幸的是，任何需要使用Python的数据科学或机器学习库的应用程序都不走运。这给数据科学家和数据工程师带来了一个艰难的选择：用Python更快地开发，以换取更慢、更昂贵的工作负载，或者用Scala或Java重写关键工作负载。 Apache Arrow项目的起源 Apache Arrow的起源故事有点像微积分的创建：各自独立的开源开发人员团体在2010年代中期的同一时间都有过“尤里卡时刻”(译注：据说阿基米德洗澡时福至心灵，想出了如何测量皇冠体积的方法，因而惊喜地叫出了一声：“Eureka！”)。 2014年底，我加入了Cloudera，开始与分别由Marcel Kornacker和Todd Lipcon领导的Apache Impala和Apache Kudu团队密切合作。我们对在大规模分布式存储和数据处理引擎之上为Python程序员（特别是pandas用户）构建直观和快速的开发人员体验上有一致的兴趣。当时的一个突出的问题是缺乏标准化的、高速的面向列的“数据协议”，以便在引擎和编程语言之间高效地传输数据。我们不想为我们的这个事情创建自定义数据格式，也不想使用像Google的Protocol Buffers或Apache Thrift这样的数据序列化技术，因为这些技术引入了过多的计算开销。我们开始设计一种新的列式数据格式，但我们知道，如果它是一个主要由Cloudera领导的项目，那么在大数据开源项目的高度政治化氛围中，它可能会有失败的风险。 与此同时，Julien Le Dem和Jacques Nadeau，分别是Apache Parquet文件格式和Apache [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/arrow-columnar-analytics-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2023/07/01/arrow-columnar-analytics">本文永久链接</a> &#8211; https://tonybai.com/2023/07/01/arrow-columnar-analytics</p>
<p>本文翻译自<a href="https://voltrondata.com">Voltron Data公司</a>CTO <a href="https://wesmckinney.com/">Wes McKinney</a>的文章<a href="https://voltrondata.com/resources/arrow-columnar-analytics">《Apache Arrow: Driving Columnar Analytics Performance and Connectivity》</a>。这篇文章回顾了现代大数据分析遇到的问题、<a href="https://tonybai.com/tag/arrow">Arrow项目</a>的起源、生态发展以及对未来的展望。</p>
<p>以下是正文部分。</p>
<hr />
<h2>引言</h2>
<p>自MapReduce以来，大数据已经走了很长一段路。<a href="http://static.googleusercontent.com/media/research.google.com/es/us/archive/mapreduce-osdi04.pdf">Jeffrey Dean和Sanjay Ghemawat</a>在2004年发表于Google的论文催生了Apache Hadoop开源项目，以及一系列其他新项目，这些项目是因大量开发人员有捕获，存储和处理非常大的数据集的需求而创建的。</p>
<p><img src="https://tonybai.com/wp-content/uploads/arrow-columnar-analytics-2.png" alt="" /><br />
<center>图：大数据演进的3V(Volume、Variety和Velocity)</center></p>
<p>虽然像Hadoop这样的第一个MapReduce框架能够处理大型数据集，但它们是为了大规模弹性（通过将每个处理步骤的结果写回分布式存储）而设计的，而并未过多考虑性能。Apache Spark于2010年首次发布，因其基于容错分布式内存处理的新架构而脱颖而出。Spark的核心是用Scala实现的，Scala是Java虚拟机（JVM）的编程语言。Spark为其他编程语言提供了binding实现，例如 C# .NET、Java、Python （PySpark） 和 R（SparkR 和 sparklyr），这有助于Spark在众多编程语言开发者社区的普及使用。</p>
<p><img src="https://tonybai.com/wp-content/uploads/arrow-columnar-analytics-3.png" alt="" /><br />
<center>图：数据处理生态系统演进</center></p>
<p>在过去的十年中，像Python和R这样的解释式编程语言已经不再局限于其在科学计算和学术统计中的利基市场，一跃发展成为现代数据科学，商业分析和AI的主流工具。这些编程语言完全主导了“笔记本电脑规模”的数据处理工作。像Hadoop和Spark这样的大规模数据处理框架为Python等解释型语言提供了编程接口，但与JVM上运行的“本机”接口相比，使用这些语言绑定的性能和资源利用率通常都很差。</p>
<p>解释型语言在使用主流大数据系统时所付出的性能损失主要源于数据互操作性问题。为了将数据从Java应用程序的核心运行时传递给用户的自定义Python函数（“用户定义函数”或“UDF”），必须将数据转换为可以Python所接受的格式，然后再转换为内置的Python对象，如列表、字典或基于数组的对象，如pandas DataFrames。更糟糕的是，许多框架，包括Spark和Hadoop，最初只为用户定义函数提供“一次一值”的执行模型，其中NumPy或pandas等工具则提供了“一次一数组”的执行模型，以避免Python解释器的开销。数据转换和解释器的双重昂贵开销使得Python基于大数据框架进行大规模数据处理变得愈加不现实。</p>
<p>Apache Spark通过引入Spark DataFrames来改善与Python的一些语言互操作性问题，Spark DataFrames是Spark SQL的一种新的类似pandas的API，它无需在Spark运行时和Python之间传输数据。不幸的是，任何需要使用Python的数据科学或机器学习库的应用程序都不走运。这给数据科学家和数据工程师带来了一个艰难的选择：用Python更快地开发，以换取更慢、更昂贵的工作负载，或者用Scala或Java重写关键工作负载。</p>
<h2>Apache Arrow项目的起源</h2>
<p>Apache Arrow的起源故事有点像微积分的创建：各自独立的开源开发人员团体在2010年代中期的同一时间都有过“尤里卡时刻”(译注：据说阿基米德洗澡时福至心灵，想出了如何测量皇冠体积的方法，因而惊喜地叫出了一声：“Eureka！”)。</p>
<p>2014年底，<a href="https://www.cloudera.com/about/news-and-blogs/press-releases/2014-09-30-cloudera-acquires-datapad-technology-assets-and-team-to-strength.html">我加入了Cloudera</a>，开始与分别由Marcel Kornacker和Todd Lipcon领导的Apache Impala和Apache Kudu团队密切合作。我们对在大规模分布式存储和数据处理引擎之上为Python程序员（特别是pandas用户）构建直观和快速的开发人员体验上有一致的兴趣。当时的一个突出的问题是缺乏标准化的、高速的面向列的“数据协议”，以便在引擎和编程语言之间高效地传输数据。我们不想为我们的这个事情创建自定义数据格式，也不想使用像Google的Protocol Buffers或Apache Thrift这样的数据序列化技术，因为这些技术引入了过多的计算开销。我们开始设计一种新的列式数据格式，但我们知道，如果它是一个主要由Cloudera领导的项目，那么在大数据开源项目的高度政治化氛围中，它可能会有失败的风险。</p>
<p>与此同时，Julien Le Dem和Jacques Nadeau，分别是Apache Parquet文件格式和Apache Drill查询引擎的共同创建者，他们正在探索一种方法，将Drill用于查询执行的内存列格式转变为独立的开源项目。这种数据格式被用作<a href="https://www.dremio.com/">Dremio</a>的基础，Dremio是一个基于SQL的开源数据湖引擎，使用它可以使得云中不同存储和数据处理系统之间更快，更高效的进行连接。</p>
<p>值得庆幸的是，Julien、Marcel和Todd在几年前就已经合作设计了Parquet文件格式，所以我们取得了联系并决定共同解决问题，而不是启动单独的、几乎肯定不会兼容的项目。我们举行了一系列快速的面对面会议（现在来看，在2022年那几乎是不可想象的！），我们开始招募其他开源大数据领导者加入我们创建一个新项目，包括 Julian Hyde（Apache Calcite）、Reynold Xin （Apache Spark）、Michael Stack （Apache HBase）等等。</p>
<p>2016年，在<a href="https://blogs.apache.org/foundation/entry/the_apache_software_foundation_announces87">将Apache Arrow作为Apache软件基金会的顶级项目推出后</a>，我们一直致力于使Arrow成为需要快速移动和处理数据的数据分析系统的首选项目。从那时起，该项目已成为高效的内存中列式分析和低开销数据传输的事实标准，它支持10多种编程语言。除了提供内存数据格式和互操作性协议外，我们还创建了一个功能全面的模块化计算库工具箱，<a href="https://www.youtube.com/watch?v=wdmf1msbtVs">为下一代分析计算系统打下坚实的基础</a>。</p>
<p>在启动Arrow项目仅一年后，<a href="https://databricks.com/blog/2017/10/30/introducing-vectorized-udfs-for-pyspark.html">与Two Sigma的我的新同事以及IBM的合作者的合作</a>，让我们能够加速PySpark与Arrow的使用，在某些情况下实现了10-100倍的性能提升，并显著改善了将Python和pandas与Apache Spark一起使用的体验。看到我们对更快、更具互操作性的未来的愿景开始逐步实现，这真是令人兴奋。</p>
<p>2018年，我与RStudio和Two Sigma合作<a href="https://wesmckinney.com/blog/announcing-ursalabs/">成立了Ursa Labs</a>，作为一个非营利性行业联盟，其使命是使Arrow成为下一代数据科学工具的强大计算基础。我参与Arrow的工作，除了解决数据互操作性问题外，还旨在解决现代硬件上的内存管理和内存计算效率问题。我們很幸运地获得了NVIDIA、Intel、G-Research、Bloomberg、ODSC和OneSixtyTwo Technologies的额外赞助。</p>
<p>经过4年多的Apache Arrow开发，我们清楚地认识到，要促使Arrow下一阶段的增长和对企业的影响，仅通过行业赞助还不够，还需要获得更大的资本投资才行。于是在2020年底，我们决定将Ursa Labs团队从RStudio（为Ursa Labs提供了大部分资金和运营支持）中剥离出来，组建一家营利性公司<a href="https://ursalabs.org/blog/ursa-computing/">Ursa Computing</a>，并在2020年底筹集了一轮风险投资。不久之后，在2021年初，我们有机会与Arrow上的GPU分析、BlazingSQL和RAPIDS领导层的创新者联手，组建了一家统一的Arrow原生(Arrow-native)计算公司<a href="https://voltrondata.com/resources/joining-forces-for-an-arrow-native-future/">Voltron Data</a>。Ursa Labs已成为<a href="http://voltrondata.com/labs/">Voltron Data Labs</a>，Voltron Data内部的一个团队，其持续的使命是发展和支持Arrow生态系统，同时维护Apache Way的开放和透明的治理模型。</p>
<h2>Apache Arrow项目的增长</h2>
<p>如今，Arrow开发人员社区已发展到700多人，其中67人拥有提交权限。我们以创建跨语言开放标准和构建模块化软件组件为动力，以降低系统复杂性，同时提高性能和效率。我们一直在考虑将该项目视为一个软件开发工具包，旨在使开发人员能够释放Apache Arrow内存格式的好处，并解决随之而来的一阶和二阶问题（例如从云存储中读取Parquet文件，然后进行一些内存分析处理）。如果没有一个可信的、“自带电池”的软件堆栈来构建支持Arrow的计算应用程序来配合它，Arrow的列式格式本质上只能作为一种替代文件格式。</p>
<p>最近，在将Arrow列式格式和协议稳定用于生产用途后，社区一直专注于提供快速的Arrow原生计算组件。这项工作在C++和Rust社区中最为活跃。使用这些语言的查询引擎项目（DataFusion for Rust 和尚未命名的C++子项目），您可以轻松地将嵌入式Arrow原生列式数据处理特性添加到您的应用程序中。这可能包括您可能使用SQL或数据帧(dataframe)库（如 pandas 或 dplyr）表示的工作负载。新的高性能数据帧库（如<a href="https://github.com/pola-rs/polars">Polars</a>）从一开始就被构建为Arrow原生。在Voltron Data，我们正在积极努力使这些功能无缝地提供给Python和R程序员。</p>
<p>让这些项目采用Arrow数据互操作性协议的一个令人信服的理由是，与任何其他使用Arrow的项目可以实现简单快速的连接。早期采用者出于信任并收获了巨大的回报。现在，任何可以读写Arrow的项目都可以通过一个快速路径连接到数据帧库（如 pandas 和 R）和许多机器学习系统（<a href="https://github.com/facebookresearch/torcharrow">PyTorch</a>、<a href="https://www.tensorflow.org/io/api_docs/python/tfio/arrow">TensorFlow</a>、<a href="https://huggingface.co/docs/datasets/package_reference/table_classes.html">Hugging Face</a>）。</p>
<p>Arrow的贡献者通过与其他开源项目的密切合作，扩展了项目的能力。最近，与<a href="https://duckdblabs.com/">DuckDB实验室</a>合作，使用<a href="https://duckdb.org/">DuckDB</a>作为嵌入式执行引擎实现了无缝查询。R或Python现在能够使用DuckDB无缝查询其Arrow数据，可以使用类似数据帧的API（如dplyr）或SQL。此集成是经由Arrow的<a href="https://arrow.apache.org/docs/format/CDataInterface.html">C数据接口</a>实现的。</p>
<p>使数据服务和分布式系统更容易使用Arrow的二进制格式是推动Arrow被更广泛接纳的一个重要工作。由于将Arrow协议与一些通用数据服务框架（如 gRPC 或 Apache Thrift）联合最佳使用需要一些中间件代码，因此社区开发了<a href="https://arrow.apache.org/docs/format/Flight.html">Flight</a>，这是一个用于Arrow原生数据服务的开发者框架和客户端-服务器协议。Flight提供了用于实现服务器和客户端逻辑的高级库，同时使用行业标准<a href="https://grpc.io/">gRPC库</a>进行内部通信。通过在客户端和服务器中使用通用内存格式来消除不必要的数据序列化，用户可以实现以前在独立于语言的协议中无法想象的数据吞吐级别（在某些情况下每秒几千兆字节）。Flight库现在在许多Arrow语言库（C++、Python、R、Java、Rust、Go）中可用，未来肯定会添加更多语言。</p>
<p><img src="https://tonybai.com/wp-content/uploads/arrow-columnar-analytics-4.png" alt="" /></p>
<p>数据库是最普遍使用的数据服务之一，ODBC和JDBC等标准数据库接口根本上是为实现互操作性和兼容性而设计，而不是为了速度。因此，Flight带来了两全其美的可能性：互操作性而又不影响性能。但是，作为开发者框架和协议的Flight没有任何关于SQL数据库工作方式的内置概念，包括用户会话、执行查询的生命周期或预处理语句等内容。还有一个风险是，每个数据库系统实现其Flight服务器的方式略有不同，因此用户必须使用不同的Flight客户端来访问每种数据库。为了解决这些问题，包括SQL数据库的客户端/服务器标准化以及与ODBC和JDBC相似的高级功能，Arrow创建了一个称为<a href="https://www.dremio.com/subsurface/arrow-flight-sql-a-universal-jdbc-driver/">Flight SQL</a>的Flight应用程序扩展。现在，数据库开发人员可以实现一个通用的Flight SQL服务器，用户将能够使用标准的Flight SQL客户端访问任何启用Flight SQL的数据库。</p>
<p><img src="https://tonybai.com/wp-content/uploads/arrow-columnar-analytics-5.png" alt="" /><br />
<center>来源：https://www.dremio.com/subsurface/arrow-flight-sql-a-universal-jdbc-driver</center></p>
<h2>Apache Arrow生态系统的发展和采用</h2>
<p>Arrow项目及其生态系统的发展得益于其早期采纳者的成功。总的来说，Arrow已经成为Python用户与以Parquet等文件格式存储的数据集进行交互的标准工具。如上所述，在项目早期，我们与Spark社区合作，使用Arrow更快地将数据传输到pandas来加速PySpark。在这些早期成功案例之后，许多其他项目都采用了Arrow来实现更快的互操作性和内存处理，并删除了以前的定制解决方案。</p>
<p>通过采用Arrow进行数据传输，Streamlit能够删除自定义代码，同时大幅提高应用程序性能。Streamlit的传统序列化框架基于Protocol Buffers，用于将表格数据从Python后端发送到JavaScript前端。通过将自定义序列化程序替换为Arrow，Streamlit的性能提高了15倍，并且能够通过使用现成的解决方案来简化其代码库。</p>
<p><img src="https://tonybai.com/wp-content/uploads/arrow-columnar-analytics-6.png" alt="" /><br />
<center>来源： https://blog.streamlit.io/content/images/2021/07/legacy-vs-arrow-2-1.png#shadow</center></p>
<p>Dremio是从头开始就以Apache Arrow为核心构建的系统。Dremio由Jacques Nadeau共同创立，是一个用于数据湖的分布式查询引擎。Dremio开发了一种基于LLVM的即时表达式编译器，称为<a href="https://github.com/dremio/gandiva">Gandiva</a>（现在是Arrow项目的一部分），它可以针对Arrow列式内存的操作生成高效的机器代码。与在JVM中执行的解释表达式相比，这可实现更快的性能。</p>
<p>最近，Databricks发布了<a href="https://databricks.com/blog/2021/08/11/how-we-achieved-high-bandwidth-connectivity-with-bi-tools.html">Cloud Fetch connector</a>，用于将商业智能工具（如Tableau或Power BI）与存储在云中的数据连接起来。过去，从传统数据仓库检索数据的速度受到了在单个线程上从单个SQL端点提取数据的速度的限制。这限制了交互式数据探索工具的有用性。Cloud Fetch 使用Arrow wire协议从云存储并行流式传输数据，与传统方法相比，性能提高了12倍。</p>
<p>这些只是使用Arrow项目的某些部分来加速数据移动或在内存中处理数据的项目的几个示例。随着越来越多的项目启用Arrow，用户将获得复合效率的优势。例如，在Snowflake实现以Arrow格式从其系统中检索数据后，<a href="https://www.snowflake.com/blog/fetching-query-results-from-snowflake-just-got-a-lot-faster-with-apache-arrow/">他们的Python和JDBC客户端的数据检索速度提高了5倍</a>。这不仅使Snowflake查询运行得更快，而且使得与Snowflake集成的产品运行得更快。例如，人工智能驱动的分析平台Tellius能够使用Arrow<a href="https://www.tellius.com/lightning-fast-data-transfer-between-tellius-and-snowflake-powered-by-apache-arrow/">将他们与Snowflake的集成速度提高3倍</a>，相比于之前的实现。</p>
<h2>社区</h2>
<p>Apache Arrow的受欢迎程度正在不断增长。事实上，Arrow的Python库PyArrow在2022年1月的下载量为4600w次，这一数字比2021年10月份创造的之前的记录增加了近800w次。我们预计，随着越来越多的项目采用Arrow作为依赖项，这一趋势将继续下去。</p>
<p><img src="https://tonybai.com/wp-content/uploads/arrow-columnar-analytics-7.png" alt="" /><br />
<center>资料来源：https://pypistats.org/，沃尔创数据</center></p>
<p>Arrow为数据传输、对二进制文件（如 Parquet）的高速访问以及快速发展的计算引擎提供了坚实的基础。这需要多年的工作和一个庞大的社区才能实现。在过去的6年里，Arrow开发者社区得到了相当大的发展：自2016年首次发布以来，已<a href="https://github.com/apache/arrow">有676名独立的开发人员</a>为该项目做出了贡献，其中105名贡献者参与了Arrow 7.0.0版本的开发。</p>
<p>与Apache软件基金会中的所有项目一样，我们遵循Apache Way，这是一个开放透明的开源项目治理框架。项目讨论和决策必须在公开场合进行，例如在邮件列表或GitHub上。贡献者以个人身份参与，而不是作为他们工作的公司的代表。通过公开开展所有项目业务，我们可以保持包容和专业的氛围，欢迎来自世界各地的贡献者的不同观点。Apache Way重视多种贡献：回答用户问题、分类错误报告和编写文档与提出拉取请求一样重要。Arrow项目主要的开发人员邮件列表是dev@arrow.apache.org。</p>
<p>在项目中持续工作一段时间后，贡献者可以通过项目管理委员会（PMC）的投票被提升为“提交者”（对项目git存储库具有写入权限）。表现出致力于发展和指导项目社区的提交者以后可能会被提升加入PMC。PMC成员是项目指导委员会，对项目中的发布和其他重大决策具有约束力的投票权。目前Arrow项目有67个提交者和38个PMC 成员。</p>
<h2>未来</h2>
<p>随着Arrow开发者社区的发展，项目范围也在扩大。该项目始于六年前，旨在设计一个独立于语言的标准来表示面向列的数据，以及一个二进制协议，用于在应用程序之间移动数据。从那时起，该项目稳步发展，提供了一个自带电池的开发工具箱，以简化构建涉及处理大型数据集的高性能分析应用程序。我们预计Arrow将成为下一代大数据系统的关键组成部分。</p>
<p>我们期望开放标准和接口方面的工作能够继续团结和简化分析计算生态系统。我们参与了<a href="https://github.com/substrait-io/substrait">Substrait</a>，这是一个新的开源框架，提供标准化的中间查询语言（低于SQL级别），将前端用户界面（如SQL或data frame库）与后端分析计算引擎连接起来。Substrait由Arrow项目联合创始人Jacques Nadeau创立，并且发展迅速。我们认为，有了这个新项目提供的执行引擎支持，编程语言接口与分析性计算将更容易发展。</p>
<h2>加入我们！</h2>
<p>发展Apache Arrow项目是我们Voltron Data使命的重要组成部分！我们期待继续与社区合作，推动生态系统向前发展。您可以订阅我们的新闻通讯以随时了解情况，并考虑在Twitter上关注我们<a href="http://twitter.com/VoltronData">@voltrondata</a>以获取更多新闻。您还可以探索<a href="https://voltrondata.com/subscription/">Voltron Data Enterprise Support</a>订阅选项，这个订阅列表旨在帮助在Apache Arrow生态系统中工作的开发人员和公司。</p>
<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/01/arrow-columnar-analytics/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>
