GO标准库巡礼-bytes与strings

声明:该系列文章是基于对@benbjohnson的《Go Walkthrough》、go官方文档、《Go语言标准库》的学习汇总而成

bytes和strings简单对比

[]byte表示了一组可修改、可扩展、连续性的byte列表。而string表示不可修改、固定长度、连续的byte列表。这意味着你不能更新字符串,只能创建,而这可能带来很大的负担

从使用者角度来看,字符串更易于使用,能作为map的键。而bytes能够更好地处理字节流,也能更好地减少内存分配以及重用

字符串和bytes与字节流的交互

创建内存reader

bytes.NewReader和strings.NewReader都可以包裹[]byte或者string返回一个实现了所有read相关接口的reader

func NewReader(b []byte) *Reader
func NewReader(s string) *Reader

经常我们可以看到下述的写法,将[]byte或者string写入到buffer,然后作为一个reader,这样涉及到内存分配会很慢

var buf bytes.Buffer
buf.WriteString("foo")
http.Post("http://example.com/", "text/plain", &buf)

事实上,我们完全可以直接使用NewReader

r := strings.NewReader("foobar")
http.Post("http://example.com", "text/plain", r)
创建内存Writer

我们可以通过声明bytes.Buffer来创建一个新的writer,它实现了除io.Seeker和io.Closer以外的所有写接口,而且同样实现了WriteString辅助函数

一个常用的场景是在单元测试中捕捉日志输出

var buf bytes.Buffer
myService.Logger = log.New(&buf, "", log.LstdFlags)
myService.Run()
if !strings.Contains(buf.String(), "service failed") {
        t.Fatal("expected log message")
}

但是实际生产中可能更多的使用bufio包来做缓存读写

包的组织结构

乍一看下,bytes和strings都是规模较大的包。但实质上它们只是一系列简单的辅助函数,总的来说,可以分为五种

  • 比较函数
  • 查找函数
  • 前缀/后缀函数
  • 替换函数
  • 切分/组合函数

比较函数

相等性

我们可以使用Equal函数来判断二者是否相等,该函数只出现在bytes包因为string可以直接用==比较。

func Equal(a, b []byte) bool

有些时候我们会希望获知A、B如果不考虑大小写是否相等,一个常见的错误写法如下图

if strings.ToUpper(a) == strings.ToUpper(b) {
       return true
}

上述的做法会造成两次重新内存分配(为新的字符串),一个正确的做法应该是使用EqualFold函数

func EqualFold(s, t []byte) bool
func EqualFold(s, t string) bool

Fold一词来自于 Unicode case-folding,因此该函数不仅能处理a-z的大小写问题,还能处理其他语言如φ to ϕ的转换

比较

我们可以使用Compare来比较[]byte或者string,如果返回-1表示a<b,0表示相等,1表示a>b

func Compare(a, b []byte) int
func Compare(a, b string) int

其中strings.Compare的出现仅仅是为了对称,事实上,该函数的comment里都指出几乎没有人应该使用strings.Compare,字符串比较可以直接使用<,>

通常来说我们比较两个[]byte是为了排序,然后sort.Interface要求实现Less函数,因此我们可以自行实现一个转换

type ByteSlices [][]byte
func (p ByteSlices) Less(i, j int) bool {
        return bytes.Compare(p[i], p[j]) == -1
}

查找函数

统计出现次数

我们可以用Contains函数来审查是否特定[]byte或者string是否存在

func Contains(b, subslice []byte) bool
func Contains(s, substr string) bool

func ContainsAny(s, chars string) bool
func ContainsAny(b []byte, chars string) bool

func ContainsRune(s string, r rune) bool
func ContainsRune(b []byte, r rune) bool 

其中ContainsAny会在chars中有任意一个码点在s中出现就成立

如果我们需要统计出现的次数,我们可以使用Count,Count会计算的是无重叠的次数fmt.Println(strings.Count("fivevev", "vev"))//1

func Count(s, sep []byte) int
func Count(s, sep string) int

Count的另外一个用途适用于统计字符串所有的runes(实际字符数),通过传入"",Count会返回实际字符数+1

strings.Count("I ❤ ☃", "")  // 6,实际字符数5+1
len("I ❤ ☃")                // 9
查找具体位置

我们可以用下面的查找函数来获取特定子串的具体位置

Index(s, sep []byte) int
IndexAny(s []byte, chars string) int
IndexByte(s []byte, c byte) int
IndexFunc(s []byte, f func(r rune) bool) int
IndexRune(s []byte, r rune) int

这五个函数strings和bytes都有(strings就是以string为第一个参数),Index用于查找一个多字节子串,IndexByte查找一个特定字节,IndexRune查找一个特定码点(将[]byte经utf-8转码后),IndexAny和IndexRune相似但是可以查找多码点,IndexFunc允许你传入一个过滤器查找每一个码点直到发现一个match

同时还有从尾部开始查找第一个符合条件的子串的对应函数

LastIndex(s, sep []byte) int
LastIndexAny(s []byte, chars string) int
LastIndexByte(s []byte, c byte) int
LastIndexFunc(s []byte, f func(r rune) bool) int

通常会比较少使用到,因为很多时候你会发现你需要的是一个parser

前缀、后缀和剪枝

显然,处理前缀、后缀是查找的特殊例子,但是足够特殊使得我们需要提供对应函数

检查是否有某前缀、后缀

我们可以使用HasPrefix和HasSuffix来检查是否有前缀和后缀

func HasPrefix(s, prefix []byte) bool
func HasPrefix(s, prefix string) bool

func HasSuffix(s, suffix []byte) bool
func HasSuffix(s, suffix string) bool

可能有些时候功能看起来太简单以至于不需要使用,但是一个常见的错误就是关于忘记检查长度

if str[0] == '@' {
        return true
}

如果str为空,上述代码会触发panic,而HasPrefix解决了这个特殊情况

if strings.HasPrefix(str, "@") {
        return true
}
剪枝

剪枝意味着从[]byte或者字符串头和尾去掉一些字符,最通用的函数是Trim

func Trim(s []byte, cutset string) []byte
func Trim(s string, cutset string) string

该函数会从s的头部和尾部去掉所有匹配cutset的码点,我们也可以用TrimLeft和TrimRight单独处理头部或者尾部

但通常来说,我们只是想去掉空格,我们可以使用TrimSpace

func TrimSpace(s []byte) []byte
func TrimSpace(s string) string

可能很多人觉得我为什么要用TrimSpace,我可以直接使用Trim(s,"\n\t"),答案是TrimSpace可以去掉unicode定义的所有空格,这不仅仅包括空格、换行、tab还有如thin space和hair space

不过实质上,TrimSpace只是一个TrimFunc的简单包装而已

func TrimSpace(s string) string {
        return TrimFunc(s, unicode.IsSpace)
}

我们可以仿造TrimSpace来设计一个专门只用来清理尾部空格的函数

TrimRightFunc(s, unicode.IsSpace)

最后,如果我们是想去掉实际前缀(而不是字符集),我们可以使用TrimPrefix和TrimSuffix

func TrimPrefix(s, prefix []byte) []byte
func TrimPrefix(s, prefix string) string
func TrimSuffix(s, suffix []byte) []byte
func TrimSuffix(s, suffix string) string

这函数通常与HasPrefix和HasSuffix结合

// Replace tilde prefix with home directory.
if strings.HasPrefix(path, "~/") {
    path = filepath.Join(u.HomeDir, strings.TrimPrefix(path, "~/"))
}

替换函数

简单替换

在绝大多数简单的替换场景下,我们可以使用Replace函数来实现替换

func Replace(s, old, new []byte, n int) []byte
func Replace(s, old, new string, n int) string

该函数会替换s中的old为new,如果n为非负数,那么就最多可以替换n次

该函数可以用于比方说你有一个placeholder如$Now,然后希望转换成当前时间

now := time.Now().Format(time.Kitchen)
println(strings.Replace(data, "$NOW", now, -1)

但是如果你有多个映射关系,你就可以使用strings.Replacer,该结构体需要结合strings.NewReplacer和一对对新、旧字符串

r := strings.NewReplacer("$NOW", now, "$USER", "mary")
println(r.Replace("Hello $USER, it is $NOW"))
// Output: Hello mary, it is 3:04PM
大小写替换

你可能会认为大小写很简单,但实质上由于go支持unicode,因此大小写转换就不是单纯的a-A了,我们有三种转换:upper,lower和title

对于绝大多数语言而言,大写和小写是简单的,只需要调用ToUpper和ToLower函数

func ToUpper(s []byte) []byte
func ToUpper(s string) string

func ToLower(s []byte) []byte
func ToLower(s string) string

但是有一些语言有着特别的大小写转换规则,如将i转换成 İ,对于这些特殊的例子,可以使用特别版本的Upper

strings.ToUpperSpecial(unicode.TurkishCase, "i")

接下来,我们有title case以及ToTitle函数

func ToTitle(s []byte) []byte
func ToTitle(s string) string

但是值得注意的是,ToTitle不是我们想象中的将每个单词首字母大写,事实上title case在unicode中是一种特别的casing。绝大多数时候,title case和大写一致,但是有少部分码点不同,如lj,其大写upper case是LJ ,其title case是Lj

如果你希望将每次单词首字母大写,那么你需要调用的是Title函数

func Title(s []byte) []byte
func Title(s string) string
匹配runes

Map函数允许你传入一个函数对每一个rune进行检查并替换

func Map(mapping func(r rune) rune, s []byte) []byte
func Map(mapping func(r rune) rune, s string) string

该函数很少使用

切分拼接函数

很多时候我们有待切分的有明确分隔符的字符串,如csv中的,、unix路径中的:

子字符串切分

对于简单的子字符串,我们有Split函数

func Split(s, sep []byte) [][]byte
func SplitAfter(s, sep []byte) [][]byte
func SplitAfterN(s, sep []byte, n int) [][]byte
func SplitN(s, sep []byte, n int) [][]byte

func Split(s, sep string) []string
func SplitAfter(s, sep string) []string
func SplitAfterN(s, sep string, n int) []string
func SplitN(s, sep string, n int) []string

上述这些函数会将s根据sep切分成若干个部分,期中After结尾表示将分隔符附在子字符串后面,N指定切分允许的次数

strings.Split("a:b:c", ":")       // ["a", "b", "c"]
strings.SplitAfter("a:b:c", ":")  // ["a:", "b:", "c"]
strings.SplitN("a:b:c", ":", 2)   // ["a", "b:c"]

分割是一个很常见的需求,但是往往出现在一个诸如csv文件、路径切分等场景中,一般来说,更适合使用encoding/csv或者path

组切分

有些时候分隔符不是一个字符串,而是一串(相同)字符串。一个最佳例子就是切分有着不定长空格的橘子。如果有连续空格单纯调用Split只会得到空白字符串,这时候可以使用Fields函数

func Fields(s []byte) [][]byte

该函数将不定长空格视作一个分隔符,除此之外我们还可以使用FieldFunc函数来将特定rune视作分隔符

func FieldsFunc(s []byte, f func(rune) bool) [][]byte
拼接

我们可以使用Join函数来拼接字符串

func Join(s [][]byte, sep []byte) []byte
func Join(a []string, sep string) string

一个常见的错误在于很多人试图自行实现拼接,如下图函数所示

var output string
for i, s := range a {
        output += s
        if i < len(a) - 1 {
                output += ","
        }
}
return output

这样实现的问题在于中间有太多不必要的内存分配,因为字符串是不可变的,所以循环的每一次都在创建一个新的字符串,而strings.Join则用一个byte切片作为buffer然后将其转换为字符串,从而最小化堆内存分配花销

其他混杂函数

Repeat函数

Repeat函数允许我们创建一个被重复的字符串,比较少用到

println(strings.Repeat("-", 80))
Runes函数

该函数将一个字符串经UTF-8翻译后返回一个全新的[]rune,这函数很少使用,因为是用for range可以实现类似的功能同时不需要内存分配

发布了31 篇原创文章 · 获赞 32 · 访问量 735

猜你喜欢

转载自blog.csdn.net/a348752377/article/details/104469834