本文最后更新于:2023年9月20日 凌晨

统计字符长度踩的坑

import (
    "fmt"
    "testing"
    "unicode/utf8"
)
 
func TestUTF8Length(t *testing.T) {
    {
        s := "hello"
        t.Logf("len(%s) = %d", s, len(s))
    }
    {
        s := "你好"
        t.Logf("len(%s) = %d", s, len(s))
 
    }
}
 
func TestUnicodeLength(t *testing.T) {
    {
        s := "hello"
        t.Logf("unicodeLen(%s) = %d", s, utf8.RuneCountInString(s))
    }
    {
        s := "你好"
        t.Logf("unicodeLen(%s) = %d", s, utf8.RuneCountInString(s))
    }
    {
        s := "😄"
        t.Logf("unicodeLen(%s) = %d", s, utf8.RuneCountInString(s))
    }
}
 
func TestUnicode(t *testing.T) {
    {
        s := "hello z"
        t.Logf("print %s", s)
        RangePrint(s)
    }
    fmt.Println()
    {
        s := "你好"
        t.Logf("print %s", s)
        RangePrint(s)
    }
    fmt.Println()
    {
        s := "😄"
        t.Logf("print %s", s)
        RangePrint(s)
    }
}
 
func TestUtf8Bytes(t *testing.T) {
    {
        s := "😄"
        for _, b := range []byte(s) {
            fmt.Printf("%#02x ", b)
        }
    }
}
 
func RangePrint(s string) {
    for _, char := range s {
        fmt.Printf("%U ", char)
    }
}

UNICODE

UNICODE 万国码 是ISO统一规范的一种字符集,给每个字符一个去标识。一个字符的Unicode编号(又称为码点)是一个 唯一数字 ,通常用十六进制表示,取值范围是0到10FFFF(转换成十进制就是0到1114111)。

UTF

Unicode是数与字符的对应关系,它是一种概念,所以它只对人类有意义。计算机不能处理和存储概念,只能处理和存储数据。所以我们必须想个办法,把Unicode的概念转换成计算机可以处理的具体数据,而这是“UTF”(Unicode Transformation Format,Unicode转换格式,Unicode转写方式)。
现今大多数计算机体系结构都使用8个二进制位做为一个字节,“UTF”的设计目的,是把Unicode规范为字符分配的编号转换为计算机可以直接存储和处理的“八位字节序列”。

UTF-8

https://en.wikipedia.org/wiki/UTF-8
1个unicode字符至少8bit位

image

如果第一个字节是0xxxxxxx,表示该字符只有1字节。
如果第一个字节是110xxxxx,表示该字符有2字节。
如果第一个字节是1110xxxx,表示该字符有3字节。
如果第一个字节是11110xxx,表示该字符有4字节。
10xxxxxx表示尾随字节。
实际上5字节和6字节的编码不会出现,因为Unicode最大码点只到U+10FFFF,用4字节就够了。
把“虎”字及其emoji转换为UTF-8编码。
虎:

  • 码点为 U+864E

  • 转换为二进制,1000011001001110

  • 从末尾开始按6位分组,1000 011001 001110

  • 需要3字节,开头插入1110,中间插入两个10,得到 11101000 10011001 10001110

  • 转换为十六进制,得到 E8 99 8E

虎的emoji:

  • 码点为 U+1F405

  • 转换为二进制,11111010000000101

  • 从末尾开始按6位分组,得到 11111 010000 000101

  • 第一部分为5位,开头插入1110的话就9位了,一个字节放不下,所以前面得补一个字节,用4字节表示。字节中间和末尾空隙填充0,然后每个尾随字节前面都补10,也就是 11110000 10011111 10010000 10000101

  • 转换成十六进制,得到 F0 9F 90 85

https://tool.oschina.net/hexconvert/

解码器读取到E8就能知道第一个字符3字节。然后跳过2字节读取到F0就能知道第二个字符4字节,所以不会有边界问题。

// Numbers fundamental to the encoding.
const (
   RuneError = '\uFFFD'     // 当出现无法解码的字节序列或无效的Unicode字符时,Go语言会使用该替换字符来表示错误或无效的部分。
   RuneSelf  = 0x80         // 由utf-8定义可知,小于该值的即表示该字符本身
   MaxRune   = '\U0010FFFF' // Maximum valid Unicode code point.
   UTFMax    = 4            // 最大的utf-8编码unicode1字节数
    
   ...
    
   xx = 0xF1 // invalid: size 1
   as = 0xF0 // ASCII: size 1
   s1 = 0x02 // accept 0, size 2
   s2 = 0x13 // accept 1, size 3
   s3 = 0x03 // accept 0, size 3
   s4 = 0x23 // accept 2, size 3
   s5 = 0x34 // accept 3, size 4
   s6 = 0x04 // accept 0, size 4
   s7 = 0x44 // accept 4, size 4
)
 
type acceptRange struct {
   lo uint8 // lowest value for second byte.
   hi uint8 // highest value for second byte.
}
 
var acceptRanges = [16]acceptRange{
   0: {locb, hicb}, // 0x80, 0b10111111
   1: {0xA0, hicb}, // 0b10100000, 0b10111111
   2: {locb, 0x9F}, // 0x80, 0b10011111
   3: {0x90, hicb}, // 0b10010000, 0b10111111 (标记位的范围)
   4: {locb, 0x8F}, // 0x80, 10001111
}
 
// first is information about the first byte in a UTF-8 sequence.
var first = [256]uint8{
   //   1   2   3   4   5   6   7   8   9   A   B   C   D   E   F
   as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x00-0x0F
   as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x10-0x1F
   as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x20-0x2F
   as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x30-0x3F
   as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x40-0x4F
   as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x50-0x5F
   as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x60-0x6F
   as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x70-0x7F
   //   1   2   3   4   5   6   7   8   9   A   B   C   D   E   F
   xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0x80-0x8F
   xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0x90-0x9F
   xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0xA0-0xAF
   xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0xB0-0xBF
   xx, xx, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, // 0xC0-0xCF
   s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, // 0xD0-0xDF
   s2, s3, s3, s3, s3, s3, s3, s3, s3, s3, s3, s3, s3, s4, s3, s3, // 0xE0-0xEF
   s5, s6, s6, s6, s7, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0xF0-0xFF
}
 
...
 
func RuneCountInString(s string) (n int) {
   ns := len(s)
   for i := 0; i < ns; n++ {
      c := s[i]
      if c < RuneSelf { // 小于0x80的都表示单个字符,直接继续循环计数n +1
         // ASCII fast path
         i++
         continue
      }
      // first is information about the first byte in a UTF-8 sequence.
      // 第一个字节(无符号256)在utf-8的位置,是16进制的xx-s7变量
      x := first[c]
      if x == xx {
         i++ // invalid.
         continue
      }
      size := int(x & 7) // 与 111 进行与运算 取得末7位的size值
      if i+size > ns {
         i++ // Short or invalid.
         continue
      }
      accept := acceptRanges[x>>4] // 右移4位 取得accept的值
      if c := s[i+1]; c < accept.lo || accept.hi < c { // 标记位范围
         size = 1
      } else if size == 2 {
      } else if c := s[i+2]; c < locb || hicb < c {
         size = 1
      } else if size == 3 {
      } else if c := s[i+3]; c < locb || hicb < c {
         size = 1
      }
      i += size
   }
   return n
}

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

go context 的源码走读 上一篇
go net/http的小总结 下一篇