700字范文,内容丰富有趣,生活中的好帮手!
700字范文 > 《Go语言圣经》学习笔记 第三章 基础数据类型

《Go语言圣经》学习笔记 第三章 基础数据类型

时间:2023-02-05 21:33:45

相关推荐

《Go语言圣经》学习笔记 第三章 基础数据类型

《Go语言圣经》学习笔记 第三章 基础数据类型

目录

整型浮点数复数布尔型字符串常量

注:学习《Go语言圣经》笔记,PDF点击下载,建议看书。

Go语言小白学习笔记,书上的内容照搬,大佬看了勿喷,以后熟悉了会总结成自己的读书笔记。

1. 整型

Go语言的数值类型包括几种不同大小的整形数、 浮点数和复数。 每种数值类型都决定了对应的大小范围和是否支持正负符号。 让我们先从整形数类型开始介绍。Go语言同时提供了有符号和无符号类型的整数运算。 这里有int8、 int16、 int32和int64四种截然不同大小的有符号整形数类型, 分别对应8、 16、 32、 64bit大小的有符号整形数, 与此对应的是uint8、 uint16、 uint32和uint64四种无符号整形数类型。这里还有两种一般对应特定CPU平台机器字大小的有符号和无符号整数int和uint; 其中int是应用最广泛的数值类型。 这两种类型都有同样的大小, 32或64bit, 但是我们不能对此做任何的假设; 因为不同的编译器即使在相同的硬件。Unicode字符rune类型是和int32等价的类型, 通常用于表示一个Unicode码点。 这两个名称可以互换使用。 同样byte也是uint8类型的等价类型, byte类型一般用于强调数值是一个原始的数据而不是一个小的整数。最后, 还有一种无符号的整数类型uintptr, 没有指定具体的bit大小但是足以容纳指针。 uintptr类型只有在底层编程是才需要, 特别是Go语言和C语言函数库或操作系统接口相交互的地方。 我们将在第十三章的unsafe包相关部分看到类似的例子。不管它们的具体大小, int、 uint和uintptr是不同类型的兄弟类型。 其中int和int32也是不同的类型, 即使int的大小也是32bit, 在需要将int当作int32类型的地方需要一个显式的类型转换操作, 反之亦然。其中有符号整数采用2的补码形式表示, 也就是最高bit位用作表示符号位, 一个n-bit的有符号数的值域是从−2^(n-1) 到2^(n-1) − 1。 无符号整数的所有bit位都用于表示非负数, 值域是0到2^(n-1) − 1。 例如, int8类型整数的值域是从-128到127, 而uint8类型整数的值域是从0到255。下面是Go语言中关于算术运算、 逻辑运算和比较运算的二元运算符, 它们按照先级递减的顺序的排列:

二元运算符有五种优先级。 在同一个优先级, 使用左优先结合规则, 但是使用括号可以明确优先顺序, 使用括号也可以用于提升优先级, 例如 mask & (1 << 28) 。对于上表中前两行的运算符, 例如+运算符还有一个与赋值相结合的对应运算符+=, 可以用于简化赋值语句。算术运算符+、 -、 * 和 / 可以适用与于整数、 浮点数和复数, 但是取模运算符%仅用于整数间的运算。 对于不同编程语言, %取模运算的行为可能并不相同。 在Go语言中, %取模运算符的符号和被取模数的符号总是一致的, 因此 -5%3 和 -5%-3 结果都是-2。 除法运算符 / 的行为则依赖于操作数是否为全为整数, 比如 5.0/4.0 的结果是1.25, 但是5/4的结果是1, 因为整数除法会向着0方向截断余数。如果一个算术运算的结果, 不管是有符号或者是无符号的, 如果需要更多的bit位才能正确表示的话, 就说明计算结果是溢出了。 超出的高位的bit位部分将被丢弃。 如果原始的数值是有符号类型, 而且最左边的bit为是1的话, 那么最终结果可能是负的, 例如int8的例子:

两个相同的整数类型可以使用下面的二元比较运算符进行比较; 比较表达式的结果是布尔类

型。

事实上, 布尔型、 数字类型和字符串等基本类型都是可比较的, 也就是说两个相同类型的值可以用==和!=进行比较。 此外, 整数、 浮点数和字符串可以根据比较结果排序。 许多其它类型的值可能是不可比较的, 因此也就可能是不可排序的。 对于我们遇到的每种类型, 我们要保证规则的一致性。这里是一元的加法和减法运算符:

对于整数, +x是0+x的简写, -x则是0-x的简写; 对于浮点数和复数, +x就是x, -x则是x 的负数。Go语言还提供了以下的bit位操作运算符, 前面4个操作运算符并不区分是有符号还是无符号数:

位操作运算符 ^ 作为二元运算符时是按位异或( XOR) , 当用作一元运算符时表示按位取反; 也就是说, 它返回一个每个bit位都取反的数。 位操作运算符 &^ 用于按位置零( ANDNOT) : 表达式 z = x &^ y 结果z的bit位为0, 如果对应y中bit位为1的话, 否则对应的bit位等于x相应的bit位的值。下面的代码演示了如何使用位操作解释uint8类型值的8个独立的bit位。 它使用了Printf函数的%b参数打印二进制格式的数字; 其中%08b中08表示打印至少8个字符宽度, 不足的前缀部分用0填充。

在 x<<n 和 x>>n 移位运算中, 决定了移位操作bit数部分必须是无符号数; 被操作的x数可以是有符号或无符号数。 算术上, 一个 x<<n 左移运算等价于乘以2 , 一个 x>>n 右移运算等价于除以2 。左移运算用零填充右边空缺的bit位, 无符号数的右移运算也是用0填充左边空缺的bit位, 但是有符号数的右移运算会用符号位的值填充左边空缺的bit位。 因为这个原因, 最好用无符号运算, 这样你可以将整数完全当作一个bit位模式处理。尽管Go语言提供了无符号数和运算, 即使数值本身不可能出现负数我们还是倾向于使用有符号的int类型, 就像数组的长度那样, 虽然使用uint无符号类型似乎是一个更合理的选择。 事实上, 内置的len函数返回一个有符号的int, 我们可以像下面例子那样处理逆序循环。

另一个选择对于上面的例子来说将是灾难性的。 如果len函数返回一个无符号数, 那么i也将是无符号的uint类型, 然后条件 i >= 0 则永远为真。 在三次迭代之后, 也就是 i == 0 时, i–语句将不会产生-1, 而是变成一个uint类型的最大值( 可能是2^64 − 1) , 然后medals[i]表达式将发生运行时panic异常 , 也就是试图访问一个slice范围以外的元素。出于这个原因, 无符号数往往只有在位运算或其它特殊的运算场景才会使用, 就像bit集合、分析二进制文件格式或者是哈希和加密操作等。 它们通常并不用于仅仅是表达非负数量的场合。一般来说, 需要一个显式的转换将一个值从一种类型转化位另一种类型, 并且算术和逻辑运算的二元操作中必须是相同的类型。 虽然这偶尔会导致需要很长的表达式, 但是它消除了所有和类型相关的问题, 而且也使得程序容易理解。在很多场景, 会遇到类似下面的代码通用的错误:

当尝试编译这三个语句时, 将产生一个错误信息:

这种类型不匹配的问题可以有几种不同的方法修复, 最常见方法是将它们都显式转型为一个常见类型:

如2.5节所述, 对于每种类型T, 如果转换允许的话, 类型转换操作T(x)将x转换为T类型。 许多整形数之间的相互转换并不会改变数值; 它们只是告诉编译器如何解释这个值。 但是对于将一个大尺寸的整数类型转为一个小尺寸的整数类型, 或者是将一个浮点数转为整数, 可能会改变数值或丢失精度:

浮点数到整数的转换将丢失任何小数部分, 然后向数轴零方向截断。 你应该避免对可能会超出目标类型表示范围的数值类型转换, 因为截断的行为可能依赖于具体的实现:

任何大小的整数字面值都可以用以0开始的八进制格式书写, 例如0666; 或用以0x或0X开头的十六进制格式书写, 例如0xdeadbeef。 十六进制数字可以用大写或小写字母。 如今八进制数据通常用于POSIX操作系统上的文件访问权限标志, 十六进制数字则更强调数字值的bit位模式。当使用fmt包打印一个数值时, 我们可以用%d、 %o或%x参数控制输出的进制格式, 就像下面的例子:

请注意fmt的两个使用技巧。 通常Printf格式化字符串包含多个%参数时将会包含对应相同数量的额外操作数, 但是%之后的 [1] 副词告诉Printf函数再次使用第一个操作数。 第二, %后的 # 副词告诉Printf在用%o、 %x或%X输出时生成0、 0x或0X前缀。字符面值通过一对单引号直接包含对应字符。 最简单的例子是ASCII中类似’a’写法的字符面值, 但是我们也可以通过转义的数值来表示任意的Unicode码点对应的字符, 马上将会看到这样的例子。字符使用 %c 参数打印, 或者是用 %q 参数打印带单引号的字符:

2. 浮点数

Go语言提供了两种精度的浮点数, float32和float64。 它们的算术规范由IEEE754浮点数国际标准定义, 该浮点数规范被所有现代的CPU支持。

这些浮点数类型的取值范围可以从很微小到很巨大。 浮点数的范围极限值可以在math包找到。 常量math.MaxFloat32表示float32能表示的最大数值, 大约是 3.4e38; 对应的math.MaxFloat64常量大约是1.8e308。 它们分别能表示的最小值近似为1.4e-45和4.9e-324。

一个float32类型的浮点数可以提供大约6个十进制数的精度, 而float64则可以提供约15个十进制数的精度; 通常应该优先使用float64类型, 因为float32类型的累计计算误差很容易扩散,并且float32能精确表示的正整数并不是很大( 译注: 因为float32的有效bit位只有23个, 其它的bit位用于指数和符号; 当整数大于23bit能表达的范围时, float32的表示将出现误差) :

浮点数的字面值可以直接写小数部分, 像这样:

小数点前面或后面的数字都可能被省略( 例如.707或1.) 。 很小或很大的数最好用科学计数法书写, 通过e或E来指定指数部分:

用Printf函数的%g参数打印浮点数, 将采用更紧凑的表示形式打印, 并提供足够的精度, 但是对应表格的数据, 使用%e( 带指数) 或%f的形式打印可能更合适。 所有的这三个打印形式都可以指定打印的宽度和控制打印精度。

上面代码打印e的幂, 打印精度是小数点后三个小数精度和8个字符宽度:

math包中除了提供大量常用的数学函数外, 还提供了IEEE754浮点数标准中定义的特殊值的创建和测试: 正无穷大和负无穷大, 分别用于表示太大溢出的数字和除零的结果; 还有NaN非数, 一般用于表示无效的除法操作结果0/0或Sqrt(-1).

函数math.IsNaN用于测试一个数是否是非数NaN, math.NaN则返回非数对应的值。 虽然可以用math.NaN来表示一个非法的结果, 但是测试一个结果是否是非数NaN则是充满风险的, 因为NaN和任何数都是不相等的( 译注: 在浮点数中, NaN、 正无穷大和负无穷大都不是唯一的, 每个都有非常多种的bit模式表示) :

如果一个函数返回的浮点数结果可能失败, 最好的做法是用单独的标志报告失败, 像这样:

接下来的程序演示了通过浮点计算生成的图形。 它是带有两个参数的z = f(x, y)函数的三维形式, 使用了可缩放矢量图形( SVG) 格式输出, SVG是一个用于矢量线绘制的XML标准。 图3.1显示了sin®/r函数的输出图形, 其中r是sqrt(xx+yy)。

gopl.io/ch3/surface

// Surface computes an SVG rendering of a 3-D surface function.package mainimport ("fmt""math")const (width, height = 600, 320 // canvas size in pixelscells = 100 // number of grid cellsxyrange = 30.0// axis ranges (-xyrange..+xyrange)xyscale = width / 2 / xyrange // pixels per x or y unitzscale = height * 0.4 // pixels per z unitangle = math.Pi / 6 // angle of x, y axes (=30°))var sin30, cos30 = math.Sin(angle), math.Cos(angle) // sin(30°), cos(30°)func main() {fmt.Printf("<svg xmlns='/2000/svg' "+"style='stroke: grey; fill: white; stroke-width: 0.7' "+"width='%d' height='%d'>", width, height)for i := 0; i < cells; i++ {for j := 0; j < cells; j++ {ax, ay := corner(i+1, j)bx, by := corner(i, j)cx, cy := corner(i, j+1)dx, dy := corner(i+1, j+1)fmt.Printf("<polygon points='%g,%g %g,%g %g,%g %g,%g'/>\n",ax, ay, bx, by, cx, cy, dx, dy)}}fmt.Println("</svg>")}func corner(i, j int) (float64, float64) {// Find point (x,y) at corner of cell (i,j).x := xyrange * (float64(i)/cells - 0.5)y := xyrange * (float64(j)/cells - 0.5)// Compute surface height z.z := f(x, y)// Project (x,y,z) isometrically onto 2-D SVG canvas (sx,sy).sx := width/2 + (x-y)*cos30*xyscalesy := height/2 + (x+y)*sin30*xyscale - z*zscalereturn sx, sy}func f(x, y float64) float64 {r := math.Hypot(x, y) // distance from (0,0)return math.Sin(r) / r}

要注意的是corner函数返回了两个结果, 分别对应每个网格顶点的坐标参数。

要解释这个程序是如何工作的需要一些基本的几何学知识, 但是我们可以跳过几何学原理,因为程序的重点是演示浮点数运算。 程序的本质是三个不同的坐标系中映射关系, 如图3.2所示。 第一个是100x100的二维网格, 对应整数整数坐标(i,j), 从远处的(0, 0)位置开始。 我们从远处向前面绘制, 因此远处先绘制的多边形有可能被前面后绘制的多边形覆盖。

第二个坐标系是一个三维的网格浮点坐标(x,y,z), 其中x和y是i和j的线性函数, 通过平移转换位网格单元的中心, 然后用xyrange系数缩放。 高度z是函数f(x,y)的值。

第三个坐标系是一个二维的画布, 起点(0,0)在左上角。 画布中点的坐标用(sx, sy)表示。 我们使用等角投影将三维点

(x,y,z)投影到二维的画布中。 画布中从远处到右边的点对应较大的x值和较大的y值。 并且画布中x和y值越大, 则对应的z值越小。 x和y的垂直和水平缩放系数来自30度角的正弦和余弦值。z的缩放系数0.4, 是一个任意选择的参数。

对于二维网格中的每一个网格单元, main函数计算单元的四个顶点在画布中对应多边形ABCD的顶点, 其中B对应(i,j)顶点位置, A、 C和D是其它相邻的顶点, 然后输出SVG的绘制指令。

3. 复数

Go语言提供了两种精度的复数类型: complex64和complex128, 分别对应float32和float64两种浮点数精度。 内置的complex函数用于构建复数, 内建的real和imag函数分别返回复数的实部和虚部:

如果一个浮点数面值或一个十进制整数面值后面跟着一个i, 例如3.141592i或2i, 它将构成一个复数的虚部, 复数的实部是0:

在常量算术规则下, 一个复数常量可以加到另一个普通数值常量( 整数或浮点数、 实部或虚部) , 我们可以用自然的方式书写复数, 就像1+2i或与之等价的写法2i+1。 上面x和y的声明语句还可以简化:

复数也可以用==和!=进行相等比较。 只有两个复数的实部和虚部都相等的时候它们才是相等的( 译注: 浮点数的相等比较是危险的, 需要特别小心处理精度问题) 。

math/cmplx包提供了复数处理的许多函数, 例如求复数的平方根函数和求幂函数。

下面的程序使用complex128复数算法来生成一个Mandelbrot图像。

gopl.io/ch3/mandelbrot

// Mandelbrot emits a PNG image of the Mandelbrot fractal.package mainimport ("image""image/color""image/png""math/cmplx""os")func main() {const (xmin, ymin, xmax, ymax = -2, -2, +2, +2width, height= 1024, 1024)img := image.NewRGBA(image.Rect(0, 0, width, height))for py := 0; py < height; py++ {y := float64(py)/height*(ymax-ymin) + yminfor px := 0; px < width; px++ {x := float64(px)/width*(xmax-xmin) + xminz := complex(x, y)// Image point (px, py) represents complex value z.img.Set(px, py, mandelbrot(z))}}png.Encode(os.Stdout, img) // NOTE: ignoring errors}func mandelbrot(z complex128) color.Color {const iterations = 200const contrast = 15var v complex128for n := uint8(0); n < iterations; n++ {v = v*v + zif cmplx.Abs(v) > 2 {return color.Gray{255 - contrast*n}}}return color.Black}//!-// Some other interesting functions:func acos(z complex128) color.Color {v := cmplx.Acos(z)blue := uint8(real(v)*128) + 127red := uint8(imag(v)*128) + 127return color.YCbCr{192, blue, red}}func sqrt(z complex128) color.Color {v := cmplx.Sqrt(z)blue := uint8(real(v)*128) + 127red := uint8(imag(v)*128) + 127return color.YCbCr{128, blue, red}}// f(x) = x^4 - 1//// z' = z - f(z)/f'(z)// = z - (z^4 - 1) / (4 * z^3)// = z - (z - 1/z^3) / 4func newton(z complex128) color.Color {const iterations = 37const contrast = 7for i := uint8(0); i < iterations; i++ {z -= (z - 1/(z*z*z)) / 4if cmplx.Abs(z*z*z*z-1) < 1e-6 {return color.Gray{255 - contrast*i}}}return color.Black}

用于遍历1024x1024图像每个点的两个嵌套的循环对应-2到+2区间的复数平面。 程序反复测试每个点对应复数值平方值加一个增量值对应的点是否超出半径为2的圆。 如果超过了, 通过根据预设置的逃逸迭代次数对应的灰度颜色来代替。 如果不是, 那么该点属于Mandelbrot集合, 使用黑色颜色标记。 最终程序将生成的PNG格式分形图像图像输出到标准输出, 如图3.3所示。

4. 布尔型

一个布尔类型的值只有两种: true和false。 if和for语句的条件部分都是布尔类型的值, 并且==<等比较操作也会产生布尔型的值。 一元操作符!对应逻辑非操作, 因此 !true 的值为 false , 更罗嗦的说法是 (!true==false)==true , 虽然表达方式不一样, 不过我们一般会采用简洁的布尔表达式, 就像用x来表示 x==true 。布尔值可以和&&( AND) 和||( OR) 操作符结合, 并且可能会有短路行为: 如果运算符左边值已经可以确定整个布尔表达式的值, 那么运算符右边的值将不在被求值, 因此下面的表达式总是安全的:

其中s[0]操作如果应用于空字符串将会导致panic异常。因为 && 的优先级比 || 高( 助记: && 对应逻辑乘法, || 对应逻辑加法, 乘法比加法优先级要高) , 下面形式的布尔表达式是不需要加小括弧的:

布尔值并不会隐式转换为数字值0或1, 反之亦然。 必须使用一个显式的if语句辅助转换:

如果需要经常做类似的转换, 包装成一个函数会更方便:

数字到布尔型的逆转换则非常简单, 不过为了保持对称, 我们也可以包装一个函数:

5. 字符串

一个字符串是一个不可改变的字节序列。 字符串可以包含任意的数据, 包括byte值0, 但是通

常是用来包含人类可读的文本。 文本字符串通常被解释为采用UTF8编码的Unicode码点

( rune) 序列, 我们稍后会详细讨论这个问题。

内置的len函数可以返回一个字符串中的字节数目( 不是rune字符数目) , 索引操作s[i]返回第i个字节的字节值, i必须满足0 ≤ i< len(s)条件约束

如果试图访问超出字符串索引范围的字节将会导致panic异常:

第i个字节并不一定是字符串的第i个字符, 因为对于非ASCII字符的UTF8编码会要两个或多个字节。 我们先简单说下字符的工作方式。

子字符串操作s[i:j]基于原始的s字符串的第i个字节开始到第j个字节( 并不包含j本身) 生成一个新字符串。 生成的新字符串将包含j-i个字节。

同样, 如果索引超出字符串范围或者j小于i的话将导致panic异常

不管i还是j都可能被忽略, 当它们被忽略时将采用0作为开始位置, 采用len(s)作为结束的位置。

其中+操作符将两个字符串链接构造一个新字符串:

字符串可以用==和<进行比较; 比较通过逐个字节比较完成的, 因此比较的结果是字符串自然编码的顺序。

字符串的值是不可变的: 一个字符串包含的字节序列永远不会被改变, 当然我们也可以给一个字符串变量分配一个新字符串值。 可以像下面这样将一个字符串追加到另一个字符串:

这并不会导致原始的字符串值被改变, 但是变量s将因为+=语句持有一个新的字符串值, 但是t依然是包含原先的字符串值。

因为字符串是不可修改的, 因此尝试修改字符串内部数据的操作也是被禁止的:

不变性意味如果两个字符串共享相同的底层数据的话也是安全的, 这使得复制任何长度的字符串代价是低廉的。 同样, 一个字符串s和对应的子字符串切片s[7:]的操作也可以安全地共享相同的内存, 因此字符串切片操作代价也是低廉的。 在这两种情况下都没有必要分配新的内存。 图3.4演示了一个字符串和两个字串共享相同的底层数据。

1. 字符串面值

字符串值也可以用字符串面值方式编写, 只要将一系列字节序列包含在双引号即可:

"hello world"

因为Go语言源文件总是用UTF8编码, 并且Go语言的文本字符串也以UTF8编码的方式处理,因此我们可以将Unicode码点也写到字符串面值中。在一个双引号包含的字符串面值中, 可以用以反斜杠 \ 开头的转义序列插入任意的数据。 下面的换行、 回车和制表符等是常见的ASCII控制代码的转义方式:可以通过十六进制或八进制转义在字符串面值包含任意的字节。 一个十六进制的转义形式是\xhh, 其中两个h表示十六进制数字( 大写或小写都可以) 。 一个八进制转义形式是\ooo, 包含三个八进制的o数字( 0到7) , 但是不能超过 \377 ( 译注: 对应一个字节的范围, 十进制为255) 。 每一个单一的字节表达一个特定的值。 稍后我们将看到如何将一个Unicode码点写到字符串面值中。一个原生的字符串面值形式是 … , 使用反引号 代替双引号。 在原生的字符串面值中, 没有转义操作; 全部的内容都是字面的意思, 包含退格和换行, 因此一个程序中的原生字符串面值可能跨越多行( 注: 在原生字符串面值内部是无法直接写 字符的, 可以用八进制或十六进制转义或+"```"链接字符串常量完成) 。 唯一的特殊处理是会删除回车以保证在所有平台上的值都是一样的, 包括那些把回车也放入文本文件的系统( 译注: Windows系统会把回车和换行一起放入文本文件中) 。原生字符串面值用于编写正则表达式会很方便, 因为正则表达式往往会包含很多反斜杠。 原生字符串面值同时被广泛应用于HTML模板、 JSON面值、 命令行提示信息以及那些需要扩展到多行的场景。

2. Unicode

在很久以前, 世界还是比较简单的, 起码计算机世界就只有一个ASCII字符集: 美国信息交换标准代码。 ASCII, 更准确地说是美国的ASCII, 使用7bit来表示128个字符: 包含英文字母的大小写、 数字、 各种标点符号和设置控制符。 对于早期的计算机程序来说, 这些就足够了,但是这也导致了世界上很多其他地区的用户无法直接使用自己的符号系统。 随着互联网的发展, 混合多种语言的数据变得很常见( 译注: 比如本身的英文原文或中文翻译都包含了ASCII、 中文、 日文等多种语言字符) 。 如何有效处理这些包含了各种语言的丰富多样的文本数据呢?答案就是使用Unicode( ) , 它收集了这个世界上所有的符号系统, 包括重音符号和其它变音符号, 制表符和回车符, 还有很多神秘的符号, 每个符号都分配一个唯一的Unicode码点, Unicode码点对应Go语言中的rune整数类型( 译注: rune是int32等价类型) 。我们可以将一个符文序列表示为一个int32序列。 这种编码方式叫UTF-32或UCS-4, 每个Unicode码点都使用同样的大小32bit来表示。 这种方式比较简单统一, 但是它会浪费很多存储空间, 因为大数据计算机可读的文本是ASCII字符, 本来每个ASCII字符只需要8bit或1字节就能表示。 而且即使是常用的字符也远少于65,536个, 也就是说用16bit编码方式就能表达常用字符。 但是, 还有其它更好的编码方法吗?

3. UTF-8

UTF8是一个将Unicode码点编码为字节序列的变长编码。 UTF8编码由Go语言之父KenThompson和Rob Pike共同发明的, 现在已经是Unicode的标准。 UTF8编码使用1到4个字节来表示每个Unicode码点, ASCII部分字符只使用1个字节, 常用字符部分使用2或3个字节表示。 每个符号编码后第一个字节的高端bit位用于表示总共有多少编码个字节。 如果第一个字节的高端bit为0, 则表示对应7bit的ASCII字符, ASCII字符每个字符依然是一个字节, 和传统的ASCII编码兼容。 如果第一个字节的高端bit是110, 则说明需要2个字节; 后续的每个高端bit都以10开头。 更大的Unicode码点也是采用类似的策略处理。

变长的编码无法直接通过索引来访问第n个字符, 但是UTF8编码获得了很多额外的优点。 首先UTF8编码比较紧凑, 完全兼容ASCII码, 并且可以自动同步: 它可以通过向前回朔最多2个字节就能确定当前字符编码的开始字节的位置。 它也是一个前缀编码, 所以当从左向右解码时不会有任何歧义也并不需要向前查看( 译注: 像GBK之类的编码, 如果不知道起点位置则可能会出现歧义) 。 没有任何字符的编码是其它字符编码的子串, 或是其它编码序列的字串, 因此搜索一个字符时只要搜索它的字节编码序列即可, 不用担心前后的上下文会对搜索结果产生干扰。 同时UTF8编码的顺序和Unicode码点的顺序一致, 因此可以直接排序UTF8编码序列。 同时因为没有嵌入的NUL(0)字节, 可以很好地兼容那些使用NUL作为字符串结尾的编程语言。Go语言的源文件采用UTF8编码, 并且Go语言处理UTF8编码的文本也很出色。 unicode包提供了诸多处理rune字符相关功能的函数( 比如区分字母和数组, 或者是字母的大写和小写转换等) , unicode/utf8包则提供了用于rune字符序列的UTF8编码和解码的功能。有很多Unicode字符很难直接从键盘输入, 并且还有很多字符有着相似的结构; 有一些甚至是不可见的字符( 译注: 中文和日文就有很多相似但不同的字) 。 Go语言字符串面值中的Unicode转义字符让我们可以通过Unicode码点输入特殊的字符。 有两种形式: \uhhhh对应16bit的码点值, \Uhhhhhhhh对应32bit的码点值, 其中h是一个十六进制数字; 一般很少需要使用32bit的形式。 每一个对应码点的UTF8编码。 例如: 下面的字母串面值都表示相同的值:

上面三个转义序列都为第一个字符串提供替代写法, 但是它们的值都是相同的。Unicode转义也可以使用在rune字符中。 下面三个字符是等价的:

对于小于256码点值可以写在一个十六进制转义字节中, 例如’\x41’对应字符’A’, 但是对于更大的码点则必须使用\u或\U转义形式。 因此, '\xe4\xb8\x96’并不是一个合法的rune字符, 虽然这三个字节对应一个有效的UTF8编码的码点。得益于UTF8编码优良的设计, 诸多字符串操作都不需要解码操作。 我们可以不用解码直接测试一个字符串是否是另一个字符串的前缀:

或者是后缀测试:

或者是包含子串测试 :

对于UTF8编码后文本的处理和原始的字节处理逻辑是一样的。 但是对应很多其它编码则并不是这样的。 ( 上面的函数都来自strings字符串处理包, 真实的代码包含了一个用哈希技术优化的Contains 实现。 )另一方面, 如果我们真的关心每个Unicode字符, 我们可以使用其它处理方式。 考虑前面的第一个例子中的字符串, 它包混合了中西两种字符。 图3.5展示了它的内存表示形式。 字符串包含13个字节, 以UTF8形式编码, 但是只对应9个Unicode字符:

为了处理这些真实的字符, 我们需要一个UTF8解码器。 unicode/utf8包提供了该功能, 我们可以这样使用:

每一次调用DecodeRuneInString函数都返回一个r和长度, r对应字符本身, 长度对应r采用UTF8编码后的编码字节数目。 长度可以用于更新第i个字符在字符串中的字节索引位置。 但是这种编码方式是笨拙的, 我们需要更简洁的语法。 幸运的是, Go语言的range循环在处理字符串的时候, 会自动隐式解码UTF8字符串。 下面的循环运行如图3.5所示; 需要注意的是对于非ASCII, 索引更新的步长将超过1个字节。

我们可以使用一个简单的循环来统计字符串中字符的数目, 像这样:

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。