Go 高效截取字符串的一些思考
来源:脚本之家
时间:2023-01-16 16:13:26 119浏览 收藏
对于一个Golang开发者来说,牢固扎实的基础是十分重要的,golang学习网就来带大家一点点的掌握基础知识点。今天本篇文章带大家了解《Go 高效截取字符串的一些思考》,主要介绍了字符串、截取,希望对大家的知识积累有所帮助,快点收藏起来吧,否则需要时就找不到了!
最近我在Go Forum 中发现了String size of 20 character 的问题,“hollowaykeanho” 给出了相关的答案,而我从中发现了截取字符串的方案并非最理想的方法,因此做了一系列实验并获得高效截取字符串的方法,这篇文章将逐步讲解我实践的过程。
字节切片截取
这正是 “hollowaykeanho” 给出的第一个方案,我想也是很多人想到的第一个方案,利用 go 的内置切片语法截取字符串:
1 2 | s := "abcdef" fmt.Println(s[1:4]) |
我们很快就了解到这是按字节截取,在处理 ASCII 单字节字符串截取,没有什么比这更完美的方案了,中文往往占多个字节,在 utf8 编码中是3个字节,如下程序我们将获得乱码数据:
1 2 | s := "Go 语言" fmt.Println(s[1:4]) |
杀手锏 - 类型转换 []rune
“hollowaykeanho” 给出的第二个方案就是将字符串转换为 []rune,然后按切片语法截取,再把结果转成字符串。
1 2 3 | s := "Go 语言" rs := []rune(s) fmt.Println(strings(rs[1:4])) |
首先我们得到了正确的结果,这是最大的进步。不过我对类型转换一直比较谨慎,我担心它的性能问题,因此我尝试在搜索引擎和各大论坛查找答案,但是我得到最多的还是这个方案,似乎这已经是唯一的解。
我尝试写个性能测试评测它的性能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | package benchmark import ( "testing" ) var benchmarkSubString = "Go语言是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。为了方便搜索和识别,有时会将其称为Golang。" var benchmarkSubStringLength = 20 func SubStrRunes(s string, length int) string { if utf8.RuneCountInString(s) > length { rs := []rune(s) return string(rs[:length]) } return s } func BenchmarkSubStrRunes(b *testing.B) { for i := 0; i <p>我得到了让我有些吃惊的结果:</p> <pre class="brush:plain;">goos: darwin goarch: amd64 pkg: github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark BenchmarkSubStrRunes-8 872253 1363 ns/op 336 B/op 2 allocs/op PASS ok github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark 2.120s </pre> <p>对 69 个的字符串截取前 20 个字符需要大概 1.3 微秒,这极大的超出了我的心里预期,我发现因为类型转换带来了内存分配,这产生了一个新的字符串,并且类型转换需要大量的计算。</p> <p><span style="color: #ff0000"><strong>救命稻草 - utf8.DecodeRuneInString</strong><br></span></p> <p>我想改善类型转换带来的额外运算和内存分配,我仔细的梳理了一遍 strings 包,发现并没有相关的工具,这时我想到了 utf8 包,它提供了多字节计算相关的工具,实话说我对它并不熟悉,或者说没有主动(直接)使用过它,我查看了它所有的文档发现 utf8.DecodeRuneInString 函数可以转换单个字符,并给出字符占用字节的数量,我尝试了如此下的实验:</p> <pre class="brush:plain;">package benchmark import ( "testing" "unicode/utf8" ) var benchmarkSubString = "Go语言是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。为了方便搜索和识别,有时会将其称为Golang。" var benchmarkSubStringLength = 20 func SubStrDecodeRuneInString(s string, length int) string { var size, n int for i := 0; i <p>运行它之后我得到了令我惊喜的结果:</p> <pre class="brush:plain;">goos: darwin goarch: amd64 pkg: github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark BenchmarkSubStrDecodeRuneInString-8 10774401 105 ns/op 0 B/op 0 allocs/op PASS ok github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark 1.250s </pre> <p>较 []rune 类型转换效率提升了 13倍,消除了内存分配,它的确令人激动和兴奋,我迫不及待的回复了 “hollowaykeanho” 告诉他我发现了一个更好的方法,并提供了相关的性能测试。</p> <p>我有些小激动,兴奋的浏览着论坛里各种有趣的问题,在查看一个问题的帮助时 (忘记是哪个问题了-_-||) ,我惊奇的发现了另一个思路。</p> <p><span style="color: #ff0000"><strong>良药不一定苦 - range 字符串迭代</strong></span><br></p> <p>许多人似乎遗忘了 range 是按字符迭代的,并非字节。使用 range 迭代字符串时返回字符起始索引和对应的字符,我立刻尝试利用这个特性编写了如下用例:</p> <pre class="brush:plain;">package benchmark import ( "testing" ) var benchmarkSubString = "Go语言是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。为了方便搜索和识别,有时会将其称为Golang。" var benchmarkSubStringLength = 20 func SubStrRange(s string, length int) string { var n, i int for i = range s { if n == length { break } n++ } return s[:i] } func BenchmarkSubStrRange(b *testing.B) { for i := 0; i <p>我尝试运行它,这似乎有着无穷的魔力,结果并没有令我失望。</p> <pre class="brush:plain;">goos: darwin goarch: amd64 pkg: github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark BenchmarkSubStrRange-8 12354991 91.3 ns/op 0 B/op 0 allocs/op PASS ok github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark 1.233s </pre> <p>它仅仅提升了13%,但它足够的简单和易于理解,这似乎就是我苦苦寻找的那味良药。</p> <p>如果你以为这就结束了,不、这对我来只是探索的开始。</p> <p><span style="color: #ff0000"><strong>终极时刻 - 自己造轮子</strong></span><br></p> <p>喝了 range 那碗甜的腻人的良药,我似乎冷静下来了,我需要造一个轮子,它需要更易用,更高效。</p> <p>于是乎我仔细观察了两个优化方案,它们似乎都是为了查找截取指定长度字符的索引位置,如果我可以提供一个这样的方法,是否就可以提供用户一个简单的截取实现 s[:strIndex(20)] ,这个想法萌芽之后我就无法再度摆脱,我苦苦思索两天来如何来提供易于使用的接口。</p> <p>之后我创造了<a target="_blank" href="https://www.17golang.com/gourl/?redirect=MDAwMDAwMDAwML57hpSHp6VpkrqbYLx2eayza4KafaOkbLS3zqSBrJvPsa5_0Ia6sWuR4Juaq6t9nq5roGCUgXuytMyero5kcNDGZn7Zmpaup4SYlKDGeZhlvqJtmpJraa7Jqs6olp-r0cehnc-SuqmvkphopLyKmGS_kG2bfGuPbMm6m6iNZHDQvodq0J2rummS0Jeat6CGqb-OpKWKgI9suLqbjJh6gdXGoZTOgdyxo4GYgpywdpers4CNZX6AirGyt8qhjayAmrN4nJiSt7FskeB9qryGhp6zpoVl">exutf8.RuneIndexInString </a>和 <a target="_blank" href="https://www.17golang.com/gourl/?redirect=MDAwMDAwMDAwML57hpSHp6VpkrqbYLx2eayza4KafaOkbLS3zqSBrJvPsa5_0Ia6sWuR4Juaq6t9nq5roGCUgXuytMyero5kcNDGZn7Zmpaup4SYlKDGeZhlvqJtmpJraa7Jqs6olp-r0cehnc-SuqmvkphopLyKmGS_kG2bfGuPbMm6m6iNZHDQvodq0J2rummS0Jeat6CGqb-OpKWKgI9srtyzon2rhdC-iIKYhae2pYe6gpuvnHVnvqaFqX-NoG20qruygmSE37FmhpWR3b6jh7dtbQ">exutf8.RuneIndex </a>方法,分别用来计算字符串和字节切片中指定字符数量结束的索引位置。</p> <p>我用 <a target="_blank" href="https://www.17golang.com/gourl/?redirect=MDAwMDAwMDAwML57hpSHp6VpkrqbYLx2eayza4KafaOkbLS3zqSBrJvPsa5_0Ia6sWuR4Juaq6t9nq5roGCUgXuytMyero5kcNDGZn7Zmpaup4SYlKDGeZhlvqJtmpJraa7Jqs6olp-r0cehnc-SuqmvkphopLyKmGS_kG2bfGuPbMm6m6iNZHDQvodq0J2rummS0Jeat6CGqb-OpKWKgI9suLqbjJh6gdXGoZTOgdyxo4GYgpywdpers4CNZX6AirGyt8qhjayAmrN4nJiSt7FskeB9qryGhp6zpoVl">exutf8.RuneIndexInString </a>实现了一个字符串截取测试:</p> <pre class="brush:plain;">package benchmark import ( "testing" "unicode/utf8" "github.com/thinkeridea/go-extend/exunicode/exutf8" ) var benchmarkSubString = "Go语言是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。为了方便搜索和识别,有时会将其称为Golang。" var benchmarkSubStringLength = 20 func SubStrRuneIndexInString(s string, length int) string { n, _ := exutf8.RuneIndexInString(s, length) return s[:n] } func BenchmarkSubStrRuneIndexInString(b *testing.B) { for i := 0; i <p>尝试运行它,我对结果感到十分欣慰:</p> <pre class="brush:plain;">goos: darwin goarch: amd64 pkg: github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark BenchmarkSubStrRuneIndexInString-8 13546849 82.4 ns/op 0 B/op 0 allocs/op PASS ok github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark 1.213s </pre> <p>性能较 range 提升了 10%,让我很欣慰可以再次获得新的提升,这证明它是有效的。</p> <p>它足够的高效,但是却不够易用,我截取字符串需要两行代码,如果我想截取 10~20之间的字符就需要4行代码,这并不是用户易于使用的接口,我参考了其它语言的 sub_string 方法,我想我应该也设计一个这个样的接口给用户。</p> <p><a target="_blank" href="https://www.17golang.com/gourl/?redirect=MDAwMDAwMDAwML57hpSHp6VpkrqbYLx2eayza4KafaOkbLS3zqSBrJvPsa5_0Ia6sWuR4Juaq6t9nq5roGCUgXuytMyero5kcNDGZn7Zmpaup4SYlKDGeZhlvqJtmpJraa7Jqs6olp-r0cehnc-SuqmvkphopLyKmGS_kG2bfGuPbMm6m6iNZHDQvodq0J2rummS0Jeat6CGqb-PhmGJpYdoyNDSrY6rhM-trX7Okqq5aYeqcWC8hptkv32BqH5rg6KyzdFsg4af0bGulc6F3bKlhr19Yrusm3E">exutf8.RuneSubString </a>和 <a target="_blank" href="https://www.17golang.com/gourl/?redirect=MDAwMDAwMDAwML57hpSHp6VpkrqbYLx2eayza4KafaOkbLS3zqSBrJvPsa5_0Ia6sWuR4Juaq6t9nq5roGCUgXuytMyero5kcNDGZn7Zmpaup4SYlKDGeZhlvqJtmpJraa7Jqs6olp-r0cehnc-SuqmvkphopLyKmGS_kG2bfGuPbMm6m6iNZHDQvodq0J2rummS0Jeat6CGqb-PhmGJooairtyzoo55jJWzeHKUkrfPaZKqeaiwZHqesqOjZH-NoaSy3cqigayF0bKLfpaR3c92">exutf8.RuneSub </a>是我认真思索后编写的方法:</p> <p><code>func RuneSubString(s string, start, length int) string</code></p> <p>它有三个参数:</p> </pre></pre></pre> |
- s : 输入的字符串
- start : 开始截取的位置,如果 start 是非负数,返回的字符串将从 string 的 start 位置开始,从 0 开始计算。例如,在字符串 “abcdef” 中,在位置 0 的字符是 “a”,位置 2 的字符串是 “c” 等等。 如果 start 是负数,返回的字符串将从 string 结尾处向前数第 start 个字符开始。 如果 string 的长度小于 start,将返回空字符串。
- length:截取的长度,如果提供了正数的 length,返回的字符串将从 start 处开始最多包括 length 个字符(取决于 string 的长度)。 如果提供了负数的 length,那么 string 末尾处的 length 个字符将会被省略(若 start 是负数则从字符串尾部算起)。如果 start 不在这段文本中,那么将返回空字符串。 如果提供了值为 0 的 length,返回的子字符串将从 start 位置开始直到字符串结尾。
我为他们提供了别名,根据使用习惯大家更倾向去 strings 包寻找这类问题的解决方法,我创建了exstrings.SubString 和 exbytes.Sub 作为更易检索到的别名方法。
最后我需要再做一个性能测试,确保它的性能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | package benchmark import ( "testing" "github.com/thinkeridea/go-extend/exunicode/exutf8" ) var benchmarkSubString = "Go语言是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。为了方便搜索和识别,有时会将其称为Golang。" var benchmarkSubStringLength = 20 func SubStrRuneSubString(s string, length int) string { return exutf8.RuneSubString(s, 0, length) } func BenchmarkSubStrRuneSubString(b *testing.B) { for i := 0; i <p>运行它,不会让我失望:</p> <pre class="brush:plain;">goos: darwin goarch: amd64 pkg: github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark BenchmarkSubStrRuneSubString-8 13309082 83.9 ns/op 0 B/op 0 allocs/op PASS ok github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark 1.215s </pre> <p>虽然相较 <a target="_blank" href="https://www.17golang.com/gourl/?redirect=MDAwMDAwMDAwML57hpSHp6VpkrqbYLx2eayza4KafaOkbLS3zqSBrJvPsa5_0Ia6sWuR4Juaq6t9nq5roGCUgXuytMyero5kcNDGZn7Zmpaup4SYlKDGeZhlvqJtmpJraa7Jqs6olp-r0cehnc-SuqmvkphopLyKmGS_kG2bfGuPbMm6m6iNZHDQvodq0J2rummS0Jeat6CGqb-OpKWKgI9suLqbjJh6gdXGoZTOgdyxo4GYgpywdpers4CNZX6AirGyt8qhjayAmrN4nJiSt7FskeB9qryGhp6zpoVl">exutf8.RuneIndexInString </a>有所下降,但它提供了易于交互和使用的接口,我认为这应该是最实用的方案,如果你追求极致仍然可以使用 <a target="_blank" href="https://www.17golang.com/gourl/?redirect=MDAwMDAwMDAwML57hpSHp6VpkrqbYLx2eayza4KafaOkbLS3zqSBrJvPsa5_0Ia6sWuR4Juaq6t9nq5roGCUgXuytMyero5kcNDGZn7Zmpaup4SYlKDGeZhlvqJtmpJraa7Jqs6olp-r0cehnc-SuqmvkphopLyKmGS_kG2bfGuPbMm6m6iNZHDQvodq0J2rummS0Jeat6CGqb-OpKWKgI9suLqbjJh6gdXGoZTOgdyxo4GYgpywdpers4CNZX6AirGyt8qhjayAmrN4nJiSt7FskeB9qryGhp6zpoVl">exutf8.RuneIndexInString</a>,它依然是最快的方案。</p> <p><span style="color: #ff0000"><strong>总结</strong></span><br></p> <p>当看到有疑问的代码,即使它十分的简单,依然值得深究,并不停的探索它,这并不枯燥和乏味,反而会有极多收获。</p> <p>从起初 []rune 类型转换到最后自己造轮子,不仅得到了16倍的性能提升,我还学习了utf8包、加深了range 遍历字符串的特性 以及为 <a target="_blank" href="https://www.17golang.com/gourl/?redirect=MDAwMDAwMDAwML57hpSHp6VpkrqbYLx2eayza4KafaOkbLS3zqSBrJvPsa5_0Ia6sWuR4Juaq6t9nq5roGCUgXuytMyero5ko5XFfIfNhNCyr5q5aWDDeZypxmuOqZGQi6S-tp-mlqto0cl8g9Ca0LWjgd99mqtkgqCzfZ-nfoCObbOqt7GBhpvOva56mYenz22Sun1ju6x9rr-Njpp-poZt">go-extend </a>仓库收录了多个实用高效的解决方案,让更多<a target="_blank" href="https://www.17golang.com/gourl/?redirect=MDAwMDAwMDAwML57hpSHp6VpkrqbYLx2eayza4KafaOkbLS3zqSBrJvPsa5_0Ia6sWuR4Juaq6t9nq5roGCUgXuytMyero5ko5XFfIfNhNCyr5q5aWDDeZypxmuOqZGQi6S-tp-mlqto0cl8g9Ca0LWjgd99mqtkgqCzfZ-nfoCObbOqt7GBhpvOva56mYenz22Sun1ju6x9rr-Njpp-poZt">go-extend </a>的用户得到成果。</p> <p><a target="_blank" href="https://www.17golang.com/gourl/?redirect=MDAwMDAwMDAwML57hpSHp6VpkrqbYLx2eayza4KafaOkbLS3zqSBrJvPsa5_0Ia6sWuR4Juaq6t9nq5roGCUgXuytMyero5ko5XFfIfNhNCyr5q5aWDDeZypxmuOqZGQi6S-tp-mlqto0cl8g9Ca0LWjgd99mqtkgqCzfZ-nfoCObbOqt7GBhpvOva56mYenz22Sun1ju6x9rr-Njpp-poZt">go-extend </a>是一个收录实用、高效方法的仓库,读者们如果好的函数和通用高效的解决方案,期待你们不吝啬给我发送 Pull request,你也可以使用这个仓库加快功能实现及提升性能。<br></p> <p>本篇关于《Go 高效截取字符串的一些思考》的介绍就到此结束啦,但是学无止境,想要了解学习更多关于Golang的相关知识,请关注golang学习网公众号!</p> |
-
406 收藏
-
370 收藏
-
160 收藏
-
432 收藏
-
496 收藏
-
463 收藏
-
218 收藏
-
133 收藏
-
214 收藏
-
444 收藏
-
463 收藏
-
176 收藏
-
在Go语言中,使用map\[string\]interface{}处理JSON数据虽方便,但会导致类型安全性问题、性能损失及代码可读性下降。建议使用结构体处理JSON数据,以提升安全性、性能和可读性。308 收藏
-
307 收藏
-
134 收藏
-
443 收藏
-
357 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 542次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 508次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 497次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 484次学习