GoLang 学习笔记(八)-- 基础类型,以及 go 的字符串编码

747 阅读11分钟
$ ./goBible --type=书籍 --name=Go 语言圣经(二)--chapter=Basic Data Type

字符串编码相关章节在 #2.4

(不得不说,go 看源码真的太方便了)

1. number(integer/float/complex)

1.1 类型

  1. int 是一种明确的类型,和 int32 是不同的类型。int 的大小不确定,是通过 32 << (^uint(0) >> 63) 计算出来的。源代码(src/strconv/atoi.go)如下:image.png
  2. int32 和 rune,unit8 和 byte 是同一个类型,且不是别名。就是说这两者在任何情况下都可以互换使用。 可以看看源代码(src/builtin/builtin.go): image.png

1.2 计算

  1. % 计算结果的符号和 % 左边的值保持一致。-5%3 == -2, -5%-3 == -2
  2. / 计算结果是整数还是小数要看两边有没有小数,只要有一方是小数那就是小数。小数的情况下,类型为 float64.
  3. 这里要提一下,如果要使用 float,就请使用 float64 而不是 float32,后者经常会出各种奇怪的错误。

1.3 unsigned 和 signed

  1. 即使是像长度这种肯定不会小于 0 的数,也请使用 int 而不是 unit,因为会有赋值的情况出现,比如:
    var uint32 length = getLength()
    for i := length; i >= 0; i-- {
        // do sth
    }
    
    这个会无限循环,因为 i := length 这一步,i 其实已经是一个 uint32 了,永远不会小于 0,但是我们只想让 length 为 uint32,没想到一不小心 i 也变成这样了。
    • 所以除非在使用 bit 计算,或者一些特殊计算的时候,再使用 unsigned 类型。

1.4 浮点数

  1. 把 float 类型转为 int 类型,相当于 floor
  2. float 类型有几个特殊值,0, -0, Inf, -Inf,NaN。

1.5 复数

go 在 builtin.go 里提供了 complex 函数,可以用于创建 complex number。结果到底是 complex64 还是 complex128 看赋值表达式(complex64 和 complex128 分别对应 float32 和 float64):

var a complex64 = complex(1, 2) // a 为 complex64 类型
var b complex128 = complex(1, 2) // b 为 complex128 类型

或者使用字面量:

a := 1 + 2i

你猜一下这个 a 是什么类型,64 还是 128? 答案是 128,原因也很简单,因为 128 对应的是 float64,而 float 类型默认是 float64。

2. 字符串

2.1 在 golang 里,字符串是 byte 构成的,不是 char 构成的。

  • 字符串的 len 函数,返回的不是字符的数量,而是 byte 的数量。如果是英文,倒没什么,如果是中文或者其他字符,就有偏差了: image.png
  • 同理,s[i] 的取值方式也是取第 i 个 byte,即使是英文也有坑。
    image.png

2.1 字符串的各种取值方式,以及一些基础操作

  • 字符串的类型,就是 string

  • s[i] 的类型,是 uint8,或者说 byte。不是 string。想要得到 string 还得类型转换:string(s[i])

  • s[0:3] 的类型,注意:这和切片毫无关系。只是用同一种语法,产出的是一个新的 string 类型。换句话说,s[i:j] 相当于一个 substring 函数。 也就是说,如果你想从 s := "human" 中得到 "h",你不应该使用 s[0],而是 s[0:1]。如果是 s := "人类",想得到 "人",就得使用 s[0:3](这并不安全,中文占用字节数和 utf8 编码相关,不是所有中文都是 3 个 bytes 哦,这里只是为了说明才这么用的)。

  • 可以用 + 对两个字符串进行拼接,请注意两边都得是字符串。byte 也不行。

2.2 字符串不可变

正如大多数语言中的那样,字符串是不可变的。+ 运算符会产生一个新的字符串。
也不能给 s[i] 赋值。

字符串不可变,不代表不能共享内存,正相反,不可变性让字符串能够安全地共享内存。

s := "human"
t := s[0:3]
// t 和 s 共享了前三个字符的内存。但是由于无论如何你都改变不了这块内存,因此是安全的。
// 如果你改变了 t,那么 t 用的也是新的内存(或者共享其他字符串的内存)
// s 的前三个字符的内存并不会收到影响。

2.3 literal

除了一些通用语法外。
可以用 `` 来包裹 raw string literal,不会进行任何转义,包括换行符 \n
不过有一个唯一例外,就是 carriage return。也就是通常所说的 cr 符。这个会被移除,为了保持所有机器上的打印保持一致。

2.4 编码知识

2.4.1 ASCII

在很久以前,计算机编码使用 ASCII 来表示一切。但是只能用来表达英文,无法表达其他语言,以及各种符号。 ASCII 占用 7 bits,也就是 128 个不同字符。

2.4.2 Unicode

于是出现了 Unicode,或者说 UTF-32,或者 UCS-4。
在 GO 中,Unicode 就是 rune,或者说 int32。
但是 UTF-32 也有缺陷,就是无论什么字符都是 32 bits/4 bytes,即使是只占一个 byte 的英文字符也是。而实际上,现实情况是,目前为止世界上的所有字符,也不过 2 bytes。

在 Go 中,单引号括起来的字符就是 rune 类型。写法类似于其他语言中的 char 类型,但是不同的是 rune 是四个字节的 unicode。

2.4.3 UTF-8

于是出现了 UTF-8。UTF-8 是对 Unicode 再次编码(所以说你看,多加一层可以解决一切问题)。UTF-8 是一个变长的编码方式。对于 ASCII 字符,就只用一个 byte 来表示。对于 1 个 byte 不够的,就用多个 byte 来表示。
这也正是 Go 选择的字符串编码方式。

双引号括起来的,也就是字符串,内部使用的是 UTF-8 编码。不过使用 range 会自动解码成 UTF-32 就是了。使用 fori 循环可以遍历 UTF-8 的 bytes slice。

2.5 特殊的 range

如果使用 range ,range 会为我们自动解码,把 UTF-8 解码成 UTF-32 格式,这样就不用担心每个字符占用多少个字节,要怎么改变 index 了。比如:

s := "Hi, 人类"
for i, r := range s {
    fmt.Println(i, r, string(r))
}
// 输出
// 0 72 H
// 1 105 i
// 2 44 ,
// 3 32  
// 4 20154 人
// 7 31867 类

注意最后一个的 index,可以看到直接跳了 3 个,因为人占了 3 个 bytes。所以类从 7 开始。
当然,r 是 rune 类型,所以想要看字符串还得使用 string 转换,或者 Printf 格式化。

2.5.1 使用 range 得到字符串的显示长度

之前提到过 len 计算的是 bytes 数量。因此这里借用 range 的特性,计算字符串实际展示出来的长度。

length := 0
for _, _ := range s {
    length++
}

或者可以使用 range 的特殊语法,当不需要初始化任何变量的时候可以这样写:

length := 0
for range s {
    length++
}

当然,标准库已经提供了这个功能:utf8.RuneCountInString()

2.5.2 解码失败

如果字符串中有一些解码问题,go 默认会使用一个字符来代替,当你看到它的时候,就说明你的 string 的来源有问题了。这个字符的 unicode 编码为 \uFFFD,打印出来是一个黑色菱形背景的白色问号,这样子的→: � 。

2.6 使用 []rune 类型转换来解码

show my code:

s := "东方"

r := []rune(s) // 解码后变成 []rune

这里要提一点就是,虽然 go 中的 string 和 []byte 很多地方有共通点,但是 []rune 并没有那样的特殊对待,就只是一个 rune 类型的 slice。

2.7 int 类型转 string 的坑

你可能会觉得 i := 65; s := string(i) 会得到 "65",但是实际上会得到 "A"
因为 string 转换会把 int 类型当做 rune 类型(也就是 UTF-32)来解释(即使你已经声明过 i 是整数,在它看来这就是 rune 类型,值为 65。因为 rune 本来也就是 int32,确实挺难和 int 区分开来),然后再编码为 UTF-8(也就是 string)。

如果想要转化成字符串,还是乖乖用 itoa。

小测试:为什么 string() 不直接转换为 byte 类型?而是要这么麻烦先转 unicode,再 utf-8?

  • 答案:
    1. 因为 UTF-8 是 Unicode 的编码,没有 Unicode 怎么可能得到 UTF-8。 2. UTF-8 是由 byte 类型构成的,不代表 byte 类型就是 UTF-8 编码,你怎么不说 ASCII 也是 byte 类型啊。byte 只是表示方式,UTF-8 是解释 bytes 的时候使用的一种规范。要怎么解释看你代码想怎么解释:
      • 你用 %d 去打印一个 btye,得到的就是一个 uint8 的数字。
        • 你用 %q 打印一个 byte,得到的就是 UTF-8 字符(因为 UTF-8 也可以为一个 byte)。
      • 你用 %q 打印 []byte,得到的就是 utf-8 字符。
        • 你用 %d 打印 []byte,就会报错
      • 你用 %q 打印一个 rune,得到的就是 utf-32 字符。
        • 你用 %d 打印一个 rune,得到的就是一个 int32 的数字。
    2. string 和 []byte 不完全是同一个东西,别的不说,string 是不可变的。slice 是可变的。不过很多特性确实是共享的。

2.8 string 和 []byte

操作 string 有很多种方式,也有很多库来帮我们完成。
其中有四个库特别重要,分别是 string,bytes,unicode,strconv。

2.8.1 strings 库

string 库顾名思义,是对 string 进行各种操作的库。基本上该有的都有了。

2.8.2 bytes 库

依旧顾名思义,是对 []byte 进行操作的库。因为 string 是不可变的,所以很多情况对 string 进行操作,尤其是在 for 循环里改变 string 的操作,是比较消耗性能的。这种时候把 string 变成 []byte 会有很好的效果。
因为这个原因,bytes 库里实现了很多 strings 库里的相同效果函数。

[]bytestring 可以相互转换。无论哪边转换成另外一边,转换的时候都会 copy 一下,因为不能改变 string。所以如果只是对字符串进行寻找等不改动 string 的操作,或者只是少量改动字符串的话,就没必要改成 []byte

2.8.3 strconv 库

由于 string() 类型转换的坑,go 提供了这个库,用于把各种类型转化成 string,或者反过来。比如说大名鼎鼎的 itoa 和 atoi。此外还提供了加引号和去引号的函数。

  • Itoa 只是对 FormatInt 的一个包装。FormatInt 第一个参数得是 int64 格式,Itoa 会进行一次类型转换;第二个参数是 base。
  • Atoi 稍微有点不一样,虽然也有对 ParseInt 的包装,但是对于一些比较小的数字的字符串(和 intSize 有关【intSize 在本文第一节里讲过】,32 位小于 10,64 位小于 19),会直接在 Atoi 里进行转换。如果比较长的话,Atoi 才会调用 ParseInt。
    • image.png

2.8.4 unicode 库

unicode 是对 rune 进行操作的库。提供了很多类似于 IsDigit,IsUpper 这种函数(很不幸,只能判断一个,所以想要判断一整个字符串就要在 for 循环里一个一个判断。好在 range 会帮我们自动解码成 rune,节省了一些解码的工作量)。

func isLetterString(s string) bool {
	for _, r := range s {
		if !unicode.IsLetter(r) {
			return false
		}
	}
	return true
}

2.8.5 bytes.Buffer

bytes 还提供了 bytes.Buffer 类型,以供更好的进行对 []byte 的操作。

var buf bytes.Buffer // 无需初始化,此时一个空的 buf 就可以使用了
buf.WriteRune('A')
fmt.Fprintf(&buf, "%d", byte(65))
// 输出 "A65"

// fmt.Fprintf 也可以改为使用 writeString
// 看了下源代码,Fprintf 内部也使用了 WriteString,那不如直接用 WriteString
buf.WriteString(strconv.FormatInt(65, 10))

3. 常量

3.1 iota

用法就不说了,大家都知道。

不是 itoa,是 iota。
iota 是一个希腊字母,是最小的字母。

3.2 untyped constansts

常量虽然必须有值,但是可以不写类型(虽然大概的类型会被推断出来,如下面的例子会被推断出是一个 integer,但是不会被当做 int,int32,int64 等中的任何一个)

const a = 27891723897213897219837219
const b = 123213213213213213213

3.2.1 untyped constanst 的计算精度

很明显,右边这串数字已经超出了 int64 能代表的限度。但是完全可以储存在一个常量中。甚至可以进行计算。而且精度极高。比如说以上就可以进行 a/b 的计算,不过有个坑,就是结果会被当做 int 类型,所以结果不能太大,比如说上面的 如果 b 是 2 的话,a/b 就会报错,因为装不下了。

3.2.2 untyped constanst 的类型

untyped constanst 可以被当做这个大的类型中的所有小的类型来使用。

  • 比如说 math.Pi 这个常量,也是 untyped constant,所以无论赋值到哪个浮点类型都可以。
    1. 如果赋值给的是一个新的变量,需要通过右边的值推断出类型,比如 var f = 0.0,那么虽然右边的是 untyped 的,但是实际上会推断出 float64 类型。
    2. 但是如果 f 是已经声明过的 float32 类型,如 f = getFloat32(); f = 0.0,那么这个 0.0 就会被隐式转为 float32 类型。

3.2.3 当给变量赋值的时候,右边的其实就是 untyped constant

比如说:

var a = 0 // 右边这个 0 其实就是 untyped int,赋值的时候被自动转成 int 类型。
var b = 0.0 // 右边这个 0.0 是 untyped float,赋值的时候被自动转成 float64 类型