乐于分享
好东西不私藏

一起读Go源码(第11期) bytes:bytes

一起读Go源码(第11期) bytes:bytes

概述

朋友们好啊,这一期我们来看bytes提供的主流功能及其源码实现。Go官方给的描述是:bytes包实现了对字节切片进行操作的相关函数,她与strings包中的功能相似。因此bytes包也实现了如字节切片比较、包含、查找、前缀后缀匹配、分割与拼接、修剪、大小写转换、替换、统计等功能,下面我们来看看具体实例吧。

字节切片比较 Equal与Compare

Equal和Compare都用于比较字节切片是否一致,但它们的返回值不一样,Equal返回bool类型,而Compare会根据字典序比较大小返回int类型;下面是EqualCompare的使用示例:

a, b, c := []byte("abc"), []byte("abc"), []byte("abcd")log.Println(bytes.Equal(a, b), bytes.Equal(a, c), bytes.Equal(nil, []byte{}))log.Println(bytes.Compare(a, b), bytes.Equal(a, c))// 2026/01/29 20:22:41 true false true// 2026/01/29 20:22:41 0 false

我们再看看这两个函数的源码:

// Equal reports whether a and b// are the same length and contain the same bytes.// A nil argument is equivalent to an empty slice.func Equal(a, b []byte) bool {    // Neither cmd/compile nor gccgo allocates for these string conversions.    return string(a) == string(b)}

首先是Equal函数,先把[]byte零拷贝成string,在通过string的 == 比较,通过判断长度和字节是否一致返回结果,这时候有彦祖要问了什么是零拷贝?首先Go有两种拷贝,一种是底层数据拷贝,会复制切片指向的字节数组,[]byte数据是多大内存,拷贝就要新占多一份内存;另一种是结构体,复制切片的元数据结构体(指针,长度,容量三个数),拷贝性能开销忽略不计。基于Go中规定字符串内容不可以被修改,使string和[]byte共享内存不会有安全问题,这里[]byte零拷贝转成string的零拷贝是Go编译器专门做过优化——当编译器在代码中检测到string(a)会接编译阶段直接生成特殊的机器码:

  1. 1. 在[]byte结构体中读取ptr指针和len长度
  2. 2. 在栈上新分配一个string结构体(16字节大小)
  3. 3. 将读取到的ptr指针和len长度赋值到string结构体上
  4. 4. 完成[]byte转string,过程完全没有复制底层字节数组。

一通操作下来极大节省了内存开销,提高了判断的性能。

// Compare returns an integer comparing two byte slices lexicographically.// The result will be 0 if a == b, -1 if a < b, and +1 if a > b.// A nil argument is equivalent to an empty slice.func Compare(a, b []byte) int {    return bytealg.Compare(a, b)}

那么Compare函数会按字典序比较两个字节切片,返回一个整数;规则是:若a等于b则返回0,若a小于b则返回-1,若a>b则返回1Compare函数内部调用了bytealg.Compare方法,bytealg是Go内部私有包,不属于对外暴露的标准库。

字节切片包含、索引 Contains与Index

Contains报告原切片范围内是否存在子切片,返回bool类型的结果;衍生周边还有ContainsAny,ContainsRune,ContainsFunc等方法;而Index函数会字符串s中第一个出现字符串sep的索引位置,若sep不存在则返回-1,Index返回的是Int类型结果;衍生还有IndexByte,IndexByteProtable,LastIndex,LastIndexByte,IndexRune,IndexAny等函数。

str := []byte("abcdefg")log.Println(bytes.Contains(str, []byte("abc")))log.Println(bytes.Index(str, []byte("f")))// 2026/01/29 20:22:41 true// 2026/01/29 20:22:41 5

先来看看Contains的源码:

// Contains reports whether subslice is within b.func Contains(b, subslice []byte) bool {    return Index(b, subslice) != -1}

芜湖里面直接用Index判断,返回Index中若sep不存在则返回-1的bool结果,那我们看看Index函数的源码:

  1. 1. 首先使用Switch将类型分类,
    • • n等于0直接返回0
    • • n等于1放到IndexByte处理
    • • n等于len(s),只需一次全量比较就知道结果,交给Equal处理
    • • n>len(s),肯定是错的,返回-1
    • • n<=bytealg.MaxLen,bytealg.MaxLen是Go中内部字节操作包bytealg中定义的常量阈值(int类型),是作为子串和子切片长短的分界线;而bytealg.MaxBruteForce则是原切片/原字符串的长度阈值(int类型)。若子切片和原切片都是短的交给bytealg.Index(s, sep)处理(反正都很小,暴力匹配)。
  2. 2. 然后剩下的就是短子切片长原切片长子切片长原切片这两种场景的处理。
    • • 短子切片长原切片的情况
    • • 长子切片长原切片的情况
// Index returns the index of the first instance of sep in s, or -1 if sep is not present in s.func Index(s, sep []byte) int {    n := len(sep)    switch {    case n == 0:       return 0    case n == 1:       return IndexByte(s, sep[0])    case n == len(s):       if Equal(sep, s) {          return 0       }       return -1    case n > len(s):       return -1    case n <= bytealg.MaxLen:       // Use brute force when s and sep both are small       if len(s) <= bytealg.MaxBruteForce {          return bytealg.Index(s, sep)       }       c0 := sep[0]       c1 := sep[1]       i := 0       t := len(s) - n + 1       fails := 0       for i < t {          if s[i] != c0 {             // IndexByte is faster than bytealg.Index, so use it as long as             // we're not getting lots of false positives.             o := IndexByte(s[i+1:t], c0)             if o < 0 {                return -1             }             i += o + 1          }          if s[i+1] == c1 && Equal(s[i:i+n], sep) {             return i          }          fails++          i++          // Switch to bytealg.Index when IndexByte produces too many false positives.          if fails > bytealg.Cutover(i) {             r := bytealg.Index(s[i:], sep)             if r >= 0 {                return r + i             }             return -1          }       }       return -1    }    c0 := sep[0]    c1 := sep[1]    i := 0    fails := 0    t := len(s) - n + 1    for i < t {       if s[i] != c0 {          o := IndexByte(s[i+1:t], c0)          if o < 0 {             break          }          i += o + 1       }       if s[i+1] == c1 && Equal(s[i:i+n], sep) {          return i       }       i++       fails++       if fails >= 4+i>>4 && i < t {          // Give up on IndexByte, it isn't skipping ahead          // far enough to be better than Rabin-Karp.          // Experiments (using IndexPeriodic) suggest          // the cutover is about 16 byte skips.          // TODO: if large prefixes of sep are matching          // we should cutover at even larger average skips,          // because Equal becomes that much more expensive.          // This code does not take that effect into account.          j := bytealg.IndexRabinKarp(s[i:], sep)          if j < 0 {             return -1          }          return i + j       }    }    return -1}

字节切片前缀、后缀匹配 HasPrefix

HasPrefix函数会报告字节切片s是否以前缀开头,HasSuffix函数会报告字节切片s是否以后缀suffix结尾,简单示例如下:

log.Println(bytes.HasPrefix(str, []byte("ab")), bytes.HasSuffix(str, []byte("fg")))// 2026/01/29 20:22:41 true true

这两个函数都返回bool类型,在HasPrefix函数中判断同时满足原字节s长度比前缀prefix长,且s原字节[0:len(prefix)]与前缀prefix完全相同;HasSuffix则相反,判断同时满足原字节s长度比后缀suffix长,且s原字节s[len(s)-len(suffix):]与后缀suffix完全相同。

// HasPrefix reports whether the byte slice s begins with prefix.func HasPrefix(s, prefix []byte) bool {    return len(s) >= len(prefix) && Equal(s[0:len(prefix)], prefix)}// HasSuffix reports whether the byte slice s ends with suffix.func HasSuffix(s, suffix []byte) bool {    return len(s) >= len(suffix) && Equal(s[len(s)-len(suffix):], suffix)}

字节切片分割与拼接 Split Join

先来看看字节切片分割Split,将切片s按照分隔符sep分割成所有子切片,并返回这些分隔符之间的子切片的切片。若sep为空,则Split会在每个UFT-8序列之后进行分割,它等同于将SplitN的count参数设置为-1。若要在分隔符的首次出现进行拆分,就看[Cut]

str2 := []byte("a,b,c,defg")sp := []byte(",")log.Println(bytes.Split(str2, sp))// 2026/01/29 20:22:41 [[97] [98] [99] [100 101 102 103]]s3 := [][]byte{}s3 = append(s3, []byte("1234"))s3 = append(s3, []byte("5678"))log.Println(string(bytes.Join(s3, []byte("@"))))// 2026/01/29 20:22:41 1234@5678
// Split slices s into all subslices separated by sep and returns a slice of// the subslices between those separators.// If sep is empty, Split splits after each UTF-8 sequence.// It is equivalent to SplitN with a count of -1.//// To split around the first instance of a separator, see [Cut].func Split(s, sep []byte) [][]byte { return genSplit(s, sep, 0, -1) }

Join方法将字符串s的各个元素连接起来,从而生成一个新的字节切片,分隔符sep会置于生成的切片中的各个元素之间。从Join函数进行了以下操作:

  1. 1. 边界判断:判断s如果长度为0直接返回空切片;如果s长度是1则返回该切片第一个元素的一个全新副本。
  2. 2. 计算最终字节的长度,避免频繁扩容;并用bytealg.MakeNoZero分配未初始化的内存。
  3. 3. 拼接数据,返回结果
// Join concatenates the elements of s to create a new byte slice. The separator// sep is placed between elements in the resulting slice.func Join(s [][]byte, sep []byte) []byte {    if len(s) == 0 {        return []byte{}    }    if len(s) == 1 {        // Just return a copy.        return append([]byte(nil), s[0]...)    }    var n int    if len(sep) > 0 {        if len(sep) >= maxInt/(len(s)-1) {            panic("bytes: Join output length overflow")        }        n += len(sep) * (len(s) - 1)    }    for _, v := range s {        if len(v) > maxInt-n {            panic("bytes: Join output length overflow")        }        n += len(v)    }    b := bytealg.MakeNoZero(n)[:n:n]    bp := copy(b, s[0])    for _, v := range s[1:] {        bp += copy(b[bp:], sep)        bp += copy(b[bp:], v)    }    return b}

字节切片修剪  Trim

Trim函数会通过从字符串s中切片所有位于cutset中的UFT-8编码的其实和结尾字符来返回一个子字符串。相关类似的还有TrimLeftTrimRight

s5 := []byte("111@qq.com")log.Println(string(bytes.Trim(s5, "111")), string(bytes.TrimLeft(s5, "11")), string(bytes.TrimRight(s5, ".com")))// 2026/01/29 20:22:41 @qq.com @qq.com 111@qq

Trim函数做了以下步骤:

  1. 1. 边界判断,若s长度为0直接返回nil,若cutset为空,直接返回原切片s。
  2. 2. 如果cutset 长度为1,且cueset(cutset是修剪字节切片,只要字符出现在cutset中,就会被s的首尾删除;中间的内容不受影响)内容是ASCII字符(对应值小于128,老朋友了)
  3. 3. 调用trimRightByte从s末尾开始,删除等于cutset[0]的字节
  4. 4. 调用trimLeftByte从s开头开始,删除等于cutset[0]的字节
  5. 5. 最后调用trimLeftUnicode(trimRightUnicode(s, cutset), cutset),会将字节切片s按uft-8规则解析成rune类型,再判断rune是否在cutset中,是的话就删除。
// Trim returns a subslice of s by slicing off all leading and// trailing UTF-8-encoded code points contained in cutset.func Trim(s []byte, cutset string) []byte {    if len(s) == 0 {       // This is what we've historically done.       return nil    }    if cutset == "" {       return s    }    if len(cutset) == 1 && cutset[0] < utf8.RuneSelf {       return trimLeftByte(trimRightByte(s, cutset[0]), cutset[0])    }    if as, ok := makeASCIISet(cutset); ok {       return trimLeftASCII(trimRightASCII(s, &as), &as)    }    return trimLeftUnicode(trimRightUnicode(s, cutset), cutset)}

字节切片大小写转换

ToUpper方法会返回一个与输入字节切片s相同的副本,但其中所有的Unicode字母都会被转换成大写形式。ToLower方法会返回一个与输入字节切片s相同的副本,但其中所有的Unicode字母都会被转换成小写形式

s6 := []byte("abcdefGHI")log.Println(string(bytes.ToUpper(s6)), string(bytes.ToLower(s6)))// 2026/01/29 20:22:41 ABCDEFGHI abcdefghi

我们就来看看ToUpper的操作步骤:

  1. 1. 有isASCII和hasLower两个变量,分别用来标记是否为纯ASCII切片和是否包含ASCII小写字母。
  2. 2. 遍历字节,如果发现有非isASCII字符,例如中文,立即跳出循环。
  3. 3. 然后是判断,若是纯大写ASCII字节,就无需转换直接返回。
  4. 4. 若包含小写ASCII且hasLower是true,高效内存分配bytealg.MakeNoZero(len(s)),遍历s,通过c -=’a’ – ‘A’ 即-32。
  5. 5. 若包含Unicode,调用Map(unicode.ToUpper,s)函数。
// ToUpper returns a copy of the byte slice s with all Unicode letters mapped to// their upper case.func ToUpper(s []byte) []byte {    isASCII, hasLower := true, false    for i := 0; i < len(s); i++ {        c := s[i]        if c >= utf8.RuneSelf {            isASCII = false            break        }        hasLower = hasLower || ('a' <= c && c <= 'z')    }    if isASCII { // optimize for ASCII-only byte slices.        if !hasLower {            // Just return a copy.            return append([]byte(""), s...)        }        b := bytealg.MakeNoZero(len(s))[:len(s):len(s)]        for i := 0; i < len(s); i++ {            c := s[i]            if 'a' <= c && c <= 'z' {                c -= 'a' - 'A'            }            b[i] = c        }        return b    }    return Map(unicode.ToUpper, s)}

字节切片 Replace

Replace函数会返回一个与原始切片s相同的副本,其中前n个不重叠的旧元素会被替换为新元素。如果旧元素为空,则匹配切片的开头以及每个UTF-8序列之后的部分,最多可进行k+1次替换(对于包含k个字符的切片而言),如果n为负值,则替换没有次数限制。

s7 := []byte("aaaaabbbccc")old, n := []byte("a"), []byte("A")log.Println(string(bytes.Replace(s7, old, n, 2)))// 2026/01/29 20:22:41 AAaaabbbccc
  1. 1. 首先是匹配次数预计,使用Count方法进行统计。若n用户替换次数0,跳过统计,保持m=0;
  2. 2. 若不存在old则直接返回拷贝结果。
  3. 3. 进行内存分配make([]byte, len(s)+n*(len(new)-len(old)))后进入循环进行替换
// Replace returns a copy of the slice s with the first n// non-overlapping instances of old replaced by new.// If old is empty, it matches at the beginning of the slice// and after each UTF-8 sequence, yielding up to k+1 replacements// for a k-rune slice.// If n < 0, there is no limit on the number of replacements.func Replace(s, old, new []byte, n int) []byte {    m := 0    if n != 0 {        // Compute number of replacements.        m = Count(s, old)    }    if m == 0 {        // Just return a copy.        return append([]byte(nil), s...)    }    if n < 0 || m < n {        n = m    }    // Apply replacements to buffer.    t := make([]byte, len(s)+n*(len(new)-len(old)))    w := 0    start := 0    for i := 0; i < n; i++ {        j := start        if len(old) == 0 {            if i > 0 {                _, wid := utf8.DecodeRune(s[start:])                j += wid            }        } else {            j += Index(s[start:], old)        }        w += copy(t[w:], s[start:j])        w += copy(t[w:], new)        start = j + len(old)    }    w += copy(t[w:], s[start:])    return t[0:w]}

字节切片次数统计 Count

Count函数会统计字符串s中不重叠出现的sep的次数,如果sep是一个空切片,则该函数返回1加上字符串s中的uft-8编码字符点的数量。

s8 := []byte("aaaabbbbddd")log.Println(bytes.Count(s8, []byte("a")))// 2026/01/29 20:22:41 4

对于Count函数而言:

  1. 1. 首先是边界判断,若sep为空切片,返回s中的uft-8码点数量+1(可以理解为字符之间的分割位置,就好像空格);若sep不费控就统计其非重叠出现的次数。
  2. 2. 在循环中若找不到则返回i=-1,返回累计次数n;若找得到则n+1,然后继续循环。
// Count counts the number of non-overlapping instances of sep in s.// If sep is an empty slice, Count returns 1 + the number of UTF-8-encoded code points in s.func Count(s, sep []byte) int {    // special case    if len(sep) == 0 {       return utf8.RuneCount(s) + 1    }    if len(sep) == 1 {       return bytealg.Count(s, sep[0])    }    n := 0    for {       i := Index(s, sep)       if i == -1 {          return n       }       n++       s = s[i+len(sep):]    }}

写在最后

本人是新手小白,如果这篇笔记中有任何错误或不准确之处,真诚地希望各位读者能够给予批评和指正,如有更好的实现方法请给我留言,谢谢!欢迎大家在评论区留言!觉得写得还不错的话欢迎大家关注一波!下一篇我们看看fmt。

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » 一起读Go源码(第11期) bytes:bytes

评论 抢沙发

4 + 4 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮