跳至主要內容

CH3.基础数据类型

2024年4月16日大约 41 分钟

CH3.基础数据类型

Go语言将数据类型分为四类:基础类型、复合类型、引用类型和接口类型

本章介绍基础类型,包括:数字、字符串和布尔型。

复合数据类型——数组(§4.1)和结构体(§4.2)——是通过组合简单类型,来表达更加复杂的数据结构。

引用类型包括指针(§2.3.2)、切片(§4.2))、字典(§4.3)、函数(§5)、通道(§8),虽然数据种类很多,但它们都是对程序中一个变量或状态的间接引用。

这意味着对任一引用类型数据的修改都会影响所有该引用的拷贝。

我们将在第7章介绍接口类型。



CH3.1.整型

整型 - Go语言圣经 (golang-china.github.io)open in new window

Go语言的数值类型包括几种不同大小的整数、浮点数和复数。

每种数值类型都决定了对应的大小范围和是否支持正负符号。

让我们先从整数类型开始介绍。


Go语言同时提供了有符号和无符号类型的整数运算。

这里有 int8int16int32int64 四种截然不同大小的有符号整数类型,分别对应8、16、32、64 bit大小的有符号整数,与此对应的是 uint8uint16uint32uint64 四种无符号整数类型。


这里还有两种一般对应特定CPU平台机器字大小的有符号和无符号整数 int 和 uint;

其中 int 是应用最广泛的数值类型。这两种类型都有同样的大小,32 或 64 bit,但是我们不能对此做任何的假设;

因为不同的编译器即使在相同的硬件平台上可能产生不同的大小。


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的有符号数的值域是从-2n-1到2n-1-1。

无符号整数的所有bit位都用于表示非负数,值域是0到2n-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的例子:

var u uint8 = 255
fmt.Println(u, u+1, u*u) // "255 0 1"

var i int8 = 127
fmt.Println(i, i+1, i*i) // "127 -128 1"


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

==    等于
!=    不等于
<     小于
<=    小于等于
>     大于
>=    大于等于


事实上,布尔型、数字类型和字符串等基本类型都是可比较的,也就是说两个相同类型的值可以用==和!=进行比较。

此外,整数、浮点数和字符串可以根据比较结果排序。许多其它类型的值可能是不可比较的,因此也就可能是不可排序的。

对于我们遇到的每种类型,我们需要保证规则的一致性。

这里是一元的加法和减法运算符:

+      一元加法(无效果)
-      负数

对于整数,+x是0+x的简写,-x则是0-x的简写;对于浮点数和复数,+x就是x,-x则是x 的负数。


Go语言还提供了以下的bit位操作运算符,前面4个操作运算符并不区分是有符号还是无符号数:

&      位运算 AND
|      位运算 OR
^      位运算 XOR
&^     位清空(AND NOT)
<<     左移
>>     右移

位操作运算符^作为二元运算符时是按位异或(XOR),当用作一元运算符时表示按位取反;也就是说,它返回一个每个bit位都取反的数。

位操作运算符&^用于按位置零(AND NOT):如果对应y中bit位为1的话,表达式z = x &^ y结果z的对应的bit位为0,否则z对应的bit位等于x相应的bit位的值。


下面的代码演示了如何使用位操作解释uint8类型值的8个独立的bit位。

它使用了Printf函数的%b参数打印二进制格式的数字;

其中%08b中08表示打印至少8个字符宽度,不足的前缀部分用0填充。

var x uint8 = 1<<1 | 1<<5
var y uint8 = 1<<1 | 1<<2

fmt.Printf("%08b\n", x) // "00100010", the set {1, 5}
fmt.Printf("%08b\n", y) // "00000110", the set {1, 2}

fmt.Printf("%08b\n", x&y)  // "00000010", the intersection {1}
fmt.Printf("%08b\n", x|y)  // "00100110", the union {1, 2, 5}
fmt.Printf("%08b\n", x^y)  // "00100100", the symmetric difference {2, 5}
fmt.Printf("%08b\n", x&^y) // "00100000", the difference {5}

for i := uint(0); i < 8; i++ {
    if x&(1<<i) != 0 { // membership test
        fmt.Println(i) // "1", "5"
    }
}

fmt.Printf("%08b\n", x<<1) // "01000100", the set {2, 6}
fmt.Printf("%08b\n", x>>1) // "00010001", the set {0, 4}

(6.5节给出了一个可以远大于一个字节的整数集的实现。)

x<<nx>>n移位运算中,决定了移位操作的bit数部分必须是无符号数;

被操作的x可以是有符号数或无符号数。

算术上,一个x<<n左移运算等价于乘以2n2^n,一个x>>n右移运算等价于除以2n2^n​。


左移运算用零填充右边空缺的bit位,无符号数的右移运算也是用0填充左边空缺的bit位,但是有符号数的右移运算会用符号位的值填充左边空缺的bit位。因为这个原因,最好用无符号运算,这样你可以将整数完全当作一个bit位模式处理。


尽管Go语言提供了无符号数的运算,但即使数值本身不可能出现负数,我们还是倾向于使用有符号的int类型,就像数组的长度那样,虽然使用uint无符号类型似乎是一个更合理的选择。事实上,内置的len函数返回一个有符号的int,我们可以像下面例子那样处理逆序循环。

medals := []string{"gold", "silver", "bronze"}
for i := len(medals) - 1; i >= 0; i-- {
    fmt.Println(medals[i]) // "bronze", "silver", "gold"
}

另一个选择对于上面的例子来说将是灾难性的。

如果len函数返回一个无符号数,那么i也将是无符号的uint类型,然后条件i >= 0则永远为真。

在三次迭代之后,也就是i == 0时,i--语句将不会产生-1,而是变成一个uint类型的最大值(可能是26412^{64}-1​​),然后medals[i]表达式运行时将发生panic异常(§5.9),也就是试图访问一个slice范围以外的元素。

出于这个原因,无符号数往往只有在位运算或其它特殊的运算场景才会使用,就像bit集合、分析二进制文件格式或者是哈希和加密操作等。它们通常并不用于仅仅是表达非负数量的场合。


一般来说,需要一个显式的转换将一个值从一种类型转化为另一种类型,并且算术和逻辑运算的二元操作中必须是相同的类型。

虽然这偶尔会导致需要很长的表达式,但是它消除了所有和类型相关的问题,而且也使得程序容易理解。

在很多场景,会遇到类似下面代码的常见的错误:

var apples int32 = 1
var oranges int16 = 2
var compote int = apples + oranges // compile error

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

invalid operation: apples + oranges (mismatched types int32 and int16)

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

var compote = int(apples) + int(oranges)

如2.5节所述,对于每种类型T,如果转换允许的话,类型转换操作T(x)将x转换为T类型。许多整数之间的相互转换并不会改变数值;它们只是告诉编译器如何解释这个值。

但是对于将一个大尺寸的整数类型转为一个小尺寸的整数类型,或者是将一个浮点数转为整数,可能会改变数值或丢失精度:

f := 3.141 // a float64
i := int(f)
fmt.Println(f, i) // "3.141 3"
f = 1.99
fmt.Println(int(f)) // "1"

浮点数到整数的转换将丢失任何小数部分,然后向数轴零方向截断。

你应该避免对可能会超出目标类型表示范围的数值做类型转换,因为截断的行为可能依赖于具体的实现:

f := 1e100  // a float64
i := int(f) // 结果依赖于具体实现

任何大小的整数字面值都可以用以0开始的八进制格式书写,例如0666;

或用以0x或0X开头的十六进制格式书写,例如0xdeadbeef。

十六进制数字可以用大写或小写字母。

如今八进制数据通常用于POSIX操作系统上的文件访问权限标志,十六进制数字则更强调数字值的bit位模式。

当使用fmt包打印一个数值时,我们可以用%d、%o或%x参数控制输出的进制格式,就像下面的例子:

o := 0666
fmt.Printf("%d %[1]o %#[1]o\n", o) // "438 666 0666"
x := int64(0xdeadbeef)
fmt.Printf("%d %[1]x %#[1]x %#[1]X\n", x)
// Output:
// 3735928559 deadbeef 0xdeadbeef 0XDEADBEEF

请注意fmt的两个使用技巧。


字符面值通过一对单引号直接包含对应字符。最简单的例子是ASCII中类似'a'写法的字符面值,

但是我们也可以通过转义的数值来表示任意的Unicode码点对应的字符,马上将会看到这样的例子。

字符使用%c参数打印,或者是用%q参数打印带单引号的字符:

ascii := 'a'
unicode := '国'
newline := '\n'
fmt.Printf("%d %[1]c %[1]q\n", ascii)   // "97 a 'a'"
fmt.Printf("%d %[1]c %[1]q\n", unicode) // "22269 国 '国'"
fmt.Printf("%d %[1]q\n", newline)       // "10 '\n'"

CH3.2. 浮点数

浮点数 - Go语言圣经 (gopl-zh.github.io)open in new window

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


这些浮点数类型的取值范围可以从很微小到很巨大。

浮点数的范围极限值可以在math包找到。

常量 math.MaxFloat32 表示 float32 能表示的最大数值,大约是 3.4e38;

对应的 math.MaxFloat64 常量大约是1.8e308。它们分别能表示的最小值近似为1.4e-45和4.9e-324。


一个float32类型的浮点数可以提供大约6个十进制数的精度(2^23 等于 8,388,608),而float64则可以提供约15个十进制数的精度;

通常应该优先使用float64类型,因为float32类型的累计计算误差很容易扩散,并且float32能精确表示的正整数并不是很大(译注:因为float32的有效bit位只有23个,其它的bit位用于指数和符号;

当整数大于23bit能表达的范围时,float32的表示将出现误差):

var f float32 = 16777216 // 1 << 24
fmt.Println(f == f+1)    // "true"!

2242^{24} 是 16,777,216

image-20240416112623152

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

const e = 2.71828 // (approximately)

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

const Avogadro = 6.02214129e23  // 阿伏伽德罗常数
const Planck   = 6.62606957e-34 // 普朗克常数

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

所有的这三个打印形式都可以指定打印的宽度和控制打印精度。

for x := 0; x < 8; x++ {
    fmt.Printf("x = %d e^x = %8.3f\n", x, math.Exp(float64(x)))
}

image-20240416113415561


math包中除了提供大量常用的数学函数外,还提供了IEEE754浮点数标准中定义的特殊值的创建和测试:正无穷大和负无穷大,分别用于表示太大溢出的数字和除零的结果;

还有NaN非数,一般用于表示无效的除法操作结果0/0或Sqrt(-1).

var z float64
fmt.Println(z, -z, 1/z, -1/z, z/z) // "0 -0 +Inf -Inf NaN"

在大多数编程语言中,包括 Go,除数为 0 是不合法的,会导致运行时错误。然而,对于浮点数的除法,规则有所不同。

在 IEEE 754 浮点数标准中,浮点数的除法定义了一些特殊情况:

  • 正浮点数除以 0 结果是正无穷(+Inf)。
  • 负浮点数除以 0 结果是负无穷(-Inf)。
  • 0 除以 0 的结果是 NaN(不是一个数字)。

这些规则允许数学运算在遇到这些特殊情况时继续进行,而不是立即停止并报错

image-20240416113934905


函数 math.IsNaN 用于测试一个数是否是非数NaN,math.NaN则返回非数对应的值。虽然可以用math.NaN来表示一个非法的结果,但是测试一个结果是否是非数NaN则是充满风险的,因为NaN和任何数都是不相等的

译注:在浮点数中,NaN、正无穷大和负无穷大都不是唯一的,每个都有非常多种的bit模式表示

nan := math.NaN()
fmt.Println(nan == nan, nan < nan, nan > nan) // "false false false"

image-20240416114141463


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

func compute() (value float64, ok bool) {
    // ...
    if failed {
        return 0, false
    }
    return result, true
}

接下来的程序演示了通过浮点计算生成的图形。

它是带有两个参数的 z = f(x, y) 函数的三维形式,使用了可缩放矢量图形(SVG)格式输出,SVG是一个用于矢量线绘制的XML标准。

图3.1显示了 sin(r)/r 函数的输出图形,其中r是sqrt(x*x+y*y)

img

// Surface computes an SVG rendering of a 3-D surface function.
package main

import (
    "fmt"
    "math"
)

const (
    width, height = 600, 320            // canvas size in pixels
    cells         = 100                 // number of grid cells
    xyrange       = 30.0                // axis ranges (-xyrange..+xyrange)
    xyscale       = width / 2 / xyrange // pixels per x or y unit
    zscale        = height * 0.4        // pixels per z unit
    angle         = 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='http://www.w3.org/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*xyscale
    sy := height/2 + (x+y)*sin30*xyscale - z*zscale
    return 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)投影到二维的画布中。

img

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

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

image-20240416134423142


练习3.1

练习 3.1: 如果f函数返回的是无限制的float64值,那么SVG文件可能输出无效的多边形元素(虽然许多SVG渲染器会妥善处理这类问题)。修改程序跳过无效的多边形

这题需要在调用 f() 后加个处理, 标记返回为 NAN 以及 +-INF

// Surface computes an SVG rendering of a 3-D surface function.
package main

import (
	"fmt"
	"math"
)

const (
	width, height = 600, 320            // canvas size in pixels
	cells         = 100                 // number of grid cells
	xyrange       = 30.0                // axis ranges (-xyrange..+xyrange)
	xyscale       = width / 2 / xyrange // pixels per x or y unit
	zscale        = height * 0.4        // pixels per z unit
	angle         = 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='http://www.w3.org/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, validA := corner(i+1, j)
			bx, by, validB := corner(i, j)
			cx, cy, validC := corner(i, j+1)
			dx, dy, validD := corner(i+1, j+1)
			if validA && validB && validC && validD {
				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, bool) {
	// 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)

	// If z is infinite or NaN, return invalid.
	if math.IsInf(z, 0) || math.IsNaN(z) {
		return 0, 0, false
	}

	// Project (x,y,z) isometrically onto 2-D SVG canvas (sx,sy).
	sx := width/2 + (x-y)*cos30*xyscale
	sy := height/2 + (x+y)*sin30*xyscale - z*zscale
	return sx, sy, true
}

func f(x, y float64) float64 {
	r := math.Hypot(x, y) // distance from (0,0)
	return math.Sin(r) / r
}

image-20240416140417471

image-20240416140609025

image-20240416140651581


练习3.2

练习 3.2: 试验math包中其他函数的渲染图形。你是否能输出一个egg box、moguls或a saddle图案?

绘图本身不是我们学习这章的目的, 这题的目的主要在于让我们多认识几个 math 包的函数, 例如

// Surface computes an SVG rendering of a 3-D surface function.
package main

import (
	"fmt"
	"math"
	"os"
)

const (
	width, height = 600, 320            // canvas size in pixels
	cells         = 100                 // number of grid cells
	xyrange       = 30.0                // axis ranges (-xyrange..+xyrange)
	xyscale       = width / 2 / xyrange // pixels per x or y unit
	zscale        = height * 0.4        // pixels per z unit
	angle         = math.Pi / 6         // angle of x, y axes (=30°)
)

var sin30, cos30 = math.Sin(angle), math.Cos(angle) // sin(30°), cos(30°)

func main() {
	draw("eggbox.svg", corner)
	draw("moguls.svg", corner_moguls)
	draw("saddle.svg", corner_saddle)
}

func draw(out_path string, function func(i, j int) (float64, float64, bool)) {
	// 输出文件
	f, err := os.Create(out_path)
	if err != nil {
		fmt.Fprintf(os.Stderr, "create file: %v\n", err)
		return
	}
	defer f.Close()

	fmt.Printf("<svg xmlns='http://www.w3.org/2000/svg' "+
		"style='stroke: grey; fill: white; stroke-width: 0.7' "+
		"width='%d' height='%d'>", width, height)
	fmt.Fprintf(f, "<svg xmlns='http://www.w3.org/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, validA := function(i+1, j)
			bx, by, validB := function(i, j)
			cx, cy, validC := function(i, j+1)
			dx, dy, validD := function(i+1, j+1)
			if validA && validB && validC && validD {
				fmt.Printf("<polygon points='%g,%g %g,%g %g,%g %g,%g'/>\n",
					ax, ay, bx, by, cx, cy, dx, dy)
				fmt.Fprintf(f, "<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, bool) {
	// 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)
	z := eggBox(x, y)

	// If z is infinite or NaN, return invalid.
	if math.IsInf(z, 0) || math.IsNaN(z) {
		return 0, 0, false
	}

	// Project (x,y,z) isometrically onto 2-D SVG canvas (sx,sy).
	sx := width/2 + (x-y)*cos30*xyscale
	sy := height/2 + (x+y)*sin30*xyscale - z*zscale
	return sx, sy, true
}

func corner_moguls(i, j int) (float64, float64, bool) {
	// 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 := moguls(x, y)

	// If z is infinite or NaN, return invalid.
	if math.IsInf(z, 0) || math.IsNaN(z) {
		return 0, 0, false
	}

	// Project (x,y,z) isometrically onto 2-D SVG canvas (sx,sy).
	sx := width/2 + (x-y)*cos30*xyscale
	sy := height/2 + (x+y)*sin30*xyscale - z*zscale
	return sx, sy, true
}

func f(x, y float64) float64 {
	r := math.Hypot(x, y) // distance from (0,0)
	return math.Sin(r) / r
}

func eggBox(x, y float64) float64 {
	return (math.Sin(x) + math.Sin(y)) / 10.0
}

func moguls(x, y float64) float64 {
	return (math.Sin(x) * math.Sin(y)) / 10.0
}

func corner_saddle(i, j int) (float64, float64, bool) {
	// 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 := saddle(x, y)

	// If z is infinite or NaN, return invalid.
	if math.IsInf(z, 0) || math.IsNaN(z) {
		return 0, 0, false
	}

	// Project (x,y,z) isometrically onto 2-D SVG canvas (sx,sy).
	sx := width/2 + (x-y)*cos30*xyscale
	sy := height/2 + (x+y)*sin30*xyscale - z*zscale
	return sx, sy, true
}

func saddle(x, y float64) float64 {
	return (math.Pow(x, 2) - math.Pow(y, 2)) / 25.0
}

image-20240416145408266

image-20240416145419222

image-20240416145426987


练习3.3

练习 3.3: 根据高度给每个多边形上色,那样峰值部将是红色(#ff0000),谷部将是蓝色(#0000ff)。

这题需要在 main 函数中加一次遍历找到 z 的最大值和最小值,然后根据偏差上色

// Surface computes an SVG rendering of a 3-D surface function.
package main

import (
	"fmt"
	"math"
	"os"
)

const (
	width, height = 600, 320            // canvas size in pixels
	cells         = 100                 // number of grid cells
	xyrange       = 30.0                // axis ranges (-xyrange..+xyrange)
	xyscale       = width / 2 / xyrange // pixels per x or y unit
	zscale        = height * 0.4        // pixels per z unit
	angle         = math.Pi / 6         // angle of x, y axes (=30°)
)

var sin30, cos30 = math.Sin(angle), math.Cos(angle) // sin(30°), cos(30°)

func main() {
	// 输出文件
	f, err := os.Create("output.svg")
	if err != nil {
		fmt.Fprintf(os.Stderr, "create file: %v\n", err)
		return
	}
	defer f.Close()
	fmt.Printf("<svg xmlns='http://www.w3.org/2000/svg' "+
		"style='stroke: grey; fill: white; stroke-width: 0.7' "+
		"width='%d' height='%d'>", width, height)
	fmt.Fprintf(f, "<svg xmlns='http://www.w3.org/2000/svg' "+
		"style='stroke: grey; fill: white; stroke-width: 0.7' "+
		"width='%d' height='%d'>", width, height)

	minZ, maxZ := math.Inf(1), math.Inf(-1)
	for i := 0; i < cells; i++ {
		for j := 0; j < cells; j++ {
			_, _, z1, valid1 := corner(i+1, j)
			_, _, z2, valid2 := corner(i, j)
			_, _, z3, valid3 := corner(i, j+1)
			_, _, z4, valid4 := corner(i+1, j+1)
			if valid1 && valid2 && valid3 && valid4 {
				z := (z1 + z2 + z3 + z4) / 4
				if z < minZ {
					minZ = z
				}
				if z > maxZ {
					maxZ = z
				}
			}
		}
	}

	for i := 0; i < cells; i++ {
		for j := 0; j < cells; j++ {
			ax, ay, z1, valid1 := corner(i+1, j)
			bx, by, z2, valid2 := corner(i, j)
			cx, cy, z3, valid3 := corner(i, j+1)
			dx, dy, z4, valid4 := corner(i+1, j+1)
			if valid1 && valid2 && valid3 && valid4 {
				z := (z1 + z2 + z3 + z4) / 4
				color := getColor(z, minZ, maxZ)
				fmt.Fprintf(f, "<polygon points='%g,%g %g,%g %g,%g %g,%g' style='fill: #%06x'/>\n",
					ax, ay, bx, by, cx, cy, dx, dy, color)
			}
		}
	}
	fmt.Println("</svg>")
}

func corner(i, j int) (float64, float64, float64, bool) {
	// 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)

	// If z is infinite or NaN, return invalid.
	if math.IsInf(z, 0) || math.IsNaN(z) {
		return 0, 0, 0, false
	}

	// Project (x,y,z) isometrically onto 2-D SVG canvas (sx,sy).
	sx := width/2 + (x-y)*cos30*xyscale
	sy := height/2 + (x+y)*sin30*xyscale - z*zscale
	return sx, sy, z, true
}

func getColor(z, min, max float64) int {
	ratio := (z - min) / (max - min)
	r := int(255 * ratio)
	b := int(255 * (1 - ratio))
	return r<<16 | b
}

func f(x, y float64) float64 {
	r := math.Hypot(x, y) // distance from (0,0)
	return math.Sin(r) / r
}

image-20240416145455279

image-20240416145524363

image-20240416145539183

image-20240416145552706


练习3.4

练习 3.4: 参考1.7节Lissajous例子的函数,构造一个web服务器,用于计算函数曲面然后返回SVG数据给客户端。服务器必须设置Content-Type头部:

w.Header().Set("Content-Type", "image/svg+xml")

(这一步在Lissajous例子中不是必须的,因为服务器使用标准的PNG图像格式,可以根据前面的512个字节自动输出对应的头部。)允许客户端通过HTTP请求参数设置高度、宽度和颜色等参数。

package main

import (
	"fmt"
	"log"
	"math"
	"net/http"
)

const (
	width, height = 600, 320            // canvas size in pixels
	cells         = 100                 // number of grid cells
	xyrange       = 30.0                // axis ranges (-xyrange..+xyrange)
	xyscale       = width / 2 / xyrange // pixels per x or y unit
	zscale        = height * 0.4        // pixels per z unit
	angle         = math.Pi / 6         // angle of x, y axes (=30°)
)

var sin30, cos30 = math.Sin(angle), math.Cos(angle) // sin(30°), cos(30°)

func generateSurface(w http.ResponseWriter) {
	fmt.Fprintf(w, "<svg xmlns='http://www.w3.org/2000/svg' "+
		"style='stroke: grey; fill: white; stroke-width: 0.7' "+
		"width='%d' height='%d'>", width, height)

	minZ, maxZ := math.Inf(1), math.Inf(-1)
	for i := 0; i < cells; i++ {
		for j := 0; j < cells; j++ {
			_, _, z1, valid1 := corner(i+1, j)
			_, _, z2, valid2 := corner(i, j)
			_, _, z3, valid3 := corner(i, j+1)
			_, _, z4, valid4 := corner(i+1, j+1)
			if valid1 && valid2 && valid3 && valid4 {
				z := (z1 + z2 + z3 + z4) / 4
				if z < minZ {
					minZ = z
				}
				if z > maxZ {
					maxZ = z
				}
			}
		}
	}

	for i := 0; i < cells; i++ {
		for j := 0; j < cells; j++ {
			ax, ay, z1, valid1 := corner(i+1, j)
			bx, by, z2, valid2 := corner(i, j)
			cx, cy, z3, valid3 := corner(i, j+1)
			dx, dy, z4, valid4 := corner(i+1, j+1)
			if valid1 && valid2 && valid3 && valid4 {
				z := (z1 + z2 + z3 + z4) / 4
				color := getColor(z, minZ, maxZ)
				fmt.Fprintf(w, "<polygon points='%g,%g %g,%g %g,%g %g,%g' style='fill: #%06x'/>\n",
					ax, ay, bx, by, cx, cy, dx, dy, color)
			}
		}
	}
	fmt.Fprintf(w, "</svg>")
}

func corner(i, j int) (float64, float64, float64, bool) {
	// 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)

	// If z is infinite or NaN, return invalid.
	if math.IsInf(z, 0) || math.IsNaN(z) {
		return 0, 0, 0, false
	}

	// Project (x,y,z) isometrically onto 2-D SVG canvas (sx,sy).
	sx := width/2 + (x-y)*cos30*xyscale
	sy := height/2 + (x+y)*sin30*xyscale - z*zscale
	return sx, sy, z, true
}

func getColor(z, min, max float64) int {
	ratio := (z - min) / (max - min)
	r := int(255 * ratio)
	b := int(255 * (1 - ratio))
	return r<<16 | b
}

func f(x, y float64) float64 {
	r := math.Hypot(x, y) // distance from (0,0)
	return math.Sin(r) / r
}

func handler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "image/svg+xml")
	generateSurface(w)
}

func main() {
	http.HandleFunc("/", handler)
	log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

image-20240416151121425


CH3.3.复数

复数 - Go语言圣经 (gopl-zh.github.io)open in new window

Go语言提供了两种精度的复数类型:complex64complex128,分别对应float32和float64两种浮点数精度。

内置的complex函数用于构建复数,内建的real和imag函数分别返回复数的实部和虚部:

var x complex128 = complex(1, 2) // 1+2i
var y complex128 = complex(3, 4) // 3+4i
fmt.Println(x*y)                 // "(-5+10i)"
fmt.Println(real(x*y))           // "-5"
fmt.Println(imag(x*y))           // "10"

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

fmt.Println(1i * 1i) // "(-1+0i)", i^2 = -1

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

x := 1 + 2i
y := 3 + 4i

复数也可以用==和!=进行相等比较。只有两个复数的实部和虚部都相等的时候它们才是相等的

译注:浮点数的相等比较是危险的,需要特别小心处理精度问题


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

fmt.Println(cmplx.Sqrt(-1)) // "(0+1i)"

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

// Mandelbrot emits a PNG image of the Mandelbrot fractal.
package main

import (
	"image"
	"image/color"
	"image/png"
	"math/cmplx"
	"os"
)

func main() {
	// 定义图片输出路径
	f, err := os.Create("mandelbrot.png")
	if err != nil {
		panic(err)
	}

	const (
		xmin, ymin, xmax, ymax = -2, -2, +2, +2
		width, height          = 1024, 1024
	)

	img := image.NewRGBA(image.Rect(0, 0, width, height))
	for py := 0; py < height; py++ {
		y := float64(py)/height*(ymax-ymin) + ymin
		for px := 0; px < width; px++ {
			x := float64(px)/width*(xmax-xmin) + xmin
			z := complex(x, y)
			// Image point (px, py) represents complex value z.
			img.Set(px, py, mandelbrot(z))
		}
	}
	png.Encode(f, img)
}

func mandelbrot(z complex128) color.Color {
	const iterations = 200
	const contrast = 15

	var v complex128
	for n := uint8(0); n < iterations; n++ {
		v = v*v + z
		if cmplx.Abs(v) > 2 {
			return color.Gray{255 - contrast*n}
		}
	}
	return color.Black
}

image-20240416153659556

用于遍历 1024x1024 图像每个点的两个嵌套的循环对应 -2 到 +2 区间的复数平面。

程序反复测试每个点对应复数值平方值加一个增量值对应的点是否超出半径为2的圆。

最终程序将生成的PNG格式分形图像输出到标准输出,如图3.3所示。

img


练习3.5

练习 3.5: 实现一个彩色的Mandelbrot图像,使用image.NewRGBA创建图像,使用color.RGBA或color.YCbCr生成颜色。

这一题偏离了本章主题, 实际上只需要修改 mandelbrot函数 return 的颜色值

可以是

// Mandelbrot emits a PNG image of the Mandelbrot fractal.
package main

import (
	"image"
	"image/color"
	"image/png"
	"math/cmplx"
	"os"
)

func main() {
	f, err := os.Create("mandelbrot.png")
	if err != nil {
		panic(err)
	}

	const (
		xmin, ymin, xmax, ymax = -2, -2, +2, +2
		width, height          = 1024, 1024
	)

	img := image.NewRGBA(image.Rect(0, 0, width, height))
	for py := 0; py < height; py++ {
		y := float64(py)/height*(ymax-ymin) + ymin
		for px := 0; px < width; px++ {
			x := float64(px)/width*(xmax-xmin) + xmin
			z := complex(x, y)
			img.Set(px, py, mandelbrot(z))
		}
	}
	png.Encode(f, img)
}

func mandelbrot(z complex128) color.Color {
	const iterations = 200
	const contrast = 15

	var v complex128
	for n := uint8(0); n < iterations; n++ {
		v = v*v + z
		if cmplx.Abs(v) > 2 {
			return color.RGBA{
				R: uint8(contrast * n % 255),
				G: uint8(255 - contrast*n%255),
				B: uint8((contrast * n / 2) % 255),
				A: 255,
			}
		}
	}
	return color.RGBA{0, 0, 0, 255}
}

image-20240416155151443


练习3.6

练习 3.6: 升采样技术可以降低每个像素对计算颜色值和平均值的影响。简单的方法是将每个像素分成四个子像素,实现它。

偏离本章主题, 不写了(


练习3.7

练习 3.7: 另一个生成分形图像的方式是使用牛顿法来求解一个复数方程,例如z41=0z^4-1=0。每个起点到四个根的迭代次数对应阴影的灰度。方程根对应的点用颜色表示

偏离主题, 不写了(


练习3.8

练习 3.8: 通过提高精度来生成更多级别的分形。使用四种不同精度类型的数字实现相同的分形:complex64、complex128、big.Float和big.Rat。后面两种类型在math/big包声明。Float是有指定限精度的浮点数;Rat是无限精度的有理数。)

它们间的性能和内存使用对比如何?当渲染图可见时缩放的级别是多少?

不想画图了(


练习3.9

练习 3.9: 编写一个web服务器,用于给客户端生成分形的图像。运行客户端通过HTTP参数指定x、y和zoom参数。

和 1.7 没有本质区别, 不写了(


CH3.4.布尔型

一个布尔类型的值只有两种:true和false。

if和for语句的条件部分都是布尔类型的值,并且==和<等比较操作也会产生布尔型的值。

一元操作符!对应逻辑非操作,因此!true的值为false,更罗嗦的说法是( !true==false)==true,虽然表达方式不一样,不过我们一般会采用简洁的布尔表达式,就像用x来表示x==true

布尔值可以和&&(AND)和||(OR)操作符结合,并且有短路行为:如果运算符左边值已经可以确定整个布尔表达式的值,那么运算符右边的值将不再被求值,因此下面的表达式总是安全的:

s != "" && s[0] == 'x'

其中s[0]操作如果应用于空字符串将会导致panic异常, 但如果是空字符串的话就会在 s!="" 短路掉, 因此不会运算 s[0] 也就不会异常


因为&&的优先级比||高(助记:&&对应逻辑乘法,||对应逻辑加法,乘法比加法优先级要高),下面形式的布尔表达式是不需要加小括弧的:

if 'a' <= c && c <= 'z' ||
    'A' <= c && c <= 'Z' ||
    '0' <= c && c <= '9' {
    // ...ASCII letter or digit...
}


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

i := 0
if b {
    i = 1
}

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

// btoi returns 1 if b is true and 0 if false.
func btoi(b bool) int {
    if b {
        return 1
    }
    return 0
}

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

// itob reports whether i is non-zero.
func itob(i int) bool { return i != 0 }

CH3.5.字符串

字符串 - Go语言圣经 (gopl-zh.github.io)open in new window

一个字符串是一个不可改变的字节序列。

字符串可以包含任意的数据,包括byte值0,但是通常是用来包含人类可读的文本。

文本字符串通常被解释为采用UTF8编码的Unicode码点(rune)序列,我们稍后会详细讨论这个问题。

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

s := "hello, world"
fmt.Println(len(s))     // "12"
fmt.Println(s[0], s[7]) // "104 119" ('h' and 'w')

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

c := s[len(s)] // panic: index out of range

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

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

fmt.Println(s[0:5]) // "hello"

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

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

fmt.Println(s[:5]) // "hello"
fmt.Println(s[7:]) // "world"
fmt.Println(s[:])  // "hello, world"

字符串可以用 ==< 进行比较;

比较通过逐个字节比较完成的,因此比较的结果是字符串自然编码的顺序


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

s := "left foot"
t := s
s += ", right foot"

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

fmt.Println(s) // "left foot, right foot"
fmt.Println(t) // "left foot"

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

s[0] = 'L' // compile error: cannot assign to s[0]

不变性意味着如果两个字符串共享相同的底层数据的话也是安全的,这使得复制任何长度的字符串代价是低廉的。

同样,一个字符串 s 和对应的子字符串切片 s[7:] 的操作也可以安全地共享相同的内存,因此字符串切片操作代价也是低廉的。

在这两种情况下都没有必要分配新的内存。

图3.4演示了一个字符串和两个子串共享相同的底层数据。

img

这节内容和 python 中的字符串基本一致


CH3.5.1.字符串面值

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

"Hello, world"

img

因为Go语言源文件总是用UTF8编码,并且Go语言的文本字符串也以UTF8编码的方式处理,因此我们可以将Unicode码点也写到字符串面值中。

在一个双引号包含的字符串面值中,可以用以反斜杠\开头的转义序列插入任意的数据。下面的换行、回车和制表符等是常见的ASCII控制代码的转义方式:

\a      响铃
\b      退格
\f      换页
\n      换行
\r      回车
\t      制表符
\v      垂直制表符
\'      单引号(只用在 '\'' 形式的rune符号面值中)
\"      双引号(只用在 "..." 形式的字符串面值中)
\\      反斜杠

可以通过十六进制或八进制转义在字符串面值中包含任意的字节。一个十六进制的转义形式是\xhh,其中两个h表示十六进制数字(大写或小写都可以)。

一个八进制转义形式是\ooo,包含三个八进制的o数字(0到7),但是不能超过\377(译注:对应一个字节的范围,十进制为255)。

每一个单一的字节表达一个特定的值。

稍后我们将看到如何将一个Unicode码点写到字符串面值中。


一个原生的字符串面值形式是...,使用反引号代替双引号。

在原生的字符串面值中,没有转义操作;全部的内容都是字面的意思,包含退格和换行

因此一个程序中的原生字符串面值可能跨越多行(译注:在原生字符串面值内部是无法直接写反引号字符的,可以用八进制或十六进制转义或+"`"连接字符串常量完成)。

唯一的特殊处理是会删除回车以保证在所有平台上的值都是一样的,包括那些把回车也放入文本文件的系统(译注:Windows系统会把回车和换行一起放入文本文件中)。

原生字符串面值用于编写正则表达式会很方便,因为正则表达式往往会包含很多反斜杠。原生字符串面值同时被广泛应用于HTML模板、JSON面值、命令行提示信息以及那些需要扩展到多行的场景。

const GoUsage = `Go is a tool for managing Go source code.

Usage:
    go command [arguments]
...`


CH3.5.2.Unicode

通识

在很久以前,世界还是比较简单的,起码计算机世界就只有一个ASCII字符集:美国信息交换标准代码。ASCII,更准确地说是美国的ASCII,使用7bit来表示128个字符:包含英文字母的大小写、数字、各种标点符号和设备控制符。

对于早期的计算机程序来说,这些就足够了,但是这也导致了世界上很多其他地区的用户无法直接使用自己的符号系统。随着互联网的发展,混合多种语言的数据变得很常见(译注:比如本身的英文原文或中文翻译都包含了ASCII、中文、日文等多种语言字符)。如何有效处理这些包含了各种语言的丰富多样的文本数据呢?

答案就是使用Unicode( http://unicode.org ),它收集了这个世界上所有的符号系统,包括重音符号和其它变音符号,制表符和回车符,还有很多神秘的符号,每个符号都分配一个唯一的Unicode码点,Unicode码点对应Go语言中的 rune 整数类型(译注:runeint32 等价类型)。

在第八版本的Unicode标准里收集了超过120,000个字符,涵盖超过100多种语言。这些在计算机程序和数据中是如何体现的呢?通用的表示一个Unicode码点的数据类型是 int32,也就是Go语言中rune对应的类型;它的同义词rune 符文正是这个意思。

我们可以将一个符文序列表示为一个int32序列。这种编码方式叫 UTF-32UCS-4,每个Unicode码点都使用同样大小的 32bit 来表示。这种方式比较简单统一,但是它会浪费很多存储空间,因为大多数计算机可读的文本是ASCII字符,本来每个ASCII字符只需要8bit或1字节就能表示。

而且即使是常用的字符也远少于65,536个,也就是说用 16bit 编码方式就能表达常用字符。但是,还有其它更好的编码方法吗?


CH3.5.3.UTF-8

通识

UTF8是一个将Unicode码点编码为字节序列的变长编码。UTF8编码是由Go语言之父Ken Thompson和Rob Pike共同发明的,现在已经是Unicode的标准。

UTF8编码使用1到4个字节来表示每个Unicode码点,ASCII部分字符只使用1个字节,常用字符部分使用2或3个字节表示。

每个符号编码后第一个字节的高端bit位用于表示编码总共有多少个字节。如果第一个字节的高端bit为0,则表示对应7bit的ASCII字符,ASCII字符每个字符依然是一个字节,和传统的ASCII编码兼容。如果第一个字节的高端bit是110,则说明需要2个字节;后续的每个高端bit都以10开头。更大的Unicode码点也是采用类似的策略处理。

0xxxxxxx                             runes 0-127    (ASCII)
110xxxxx 10xxxxxx                    128-2047       (values <128 unused)
1110xxxx 10xxxxxx 10xxxxxx           2048-65535     (values <2048 unused)
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx  65536-0x10ffff (other values unused)

变长的编码无法直接通过索引来访问第n个字符,但是UTF8编码获得了很多额外的优点。首先UTF8编码比较紧凑,完全兼容ASCII码,并且可以自动同步:它可以通过向前回朔最多3个字节就能确定当前字符编码的开始字节的位置。它也是一个前缀编码,所以当从左向右解码时不会有任何歧义也并不需要向前查看(译注:像GBK之类的编码,如果不知道起点位置则可能会出现歧义)。

没有任何字符的编码是其它字符编码的子串,或是其它编码序列的字串,因此搜索一个字符时只要搜索它的字节编码序列即可,不用担心前后的上下文会对搜索结果产生干扰。

同时UTF8编码的顺序和Unicode码点的顺序一致,因此可以直接排序UTF8编码序列。同时因为没有嵌入的 NUL(0) 字节,可以很好地兼容那些使用 NUL 作为字符串结尾的编程语言。


Go语言的源文件采用UTF8编码,并且Go语言处理UTF8编码的文本也很出色。

unicode包提供了诸多处理rune字符相关功能的函数(比如区分字母和数字,或者是字母的大写和小写转换等),unicode/utf8 包则提供了用于rune字符序列的UTF8编码和解码的功能。


练习3.10

练习 3.10: 编写一个非递归版本的comma函数,使用bytes.Buffer代替字符串链接操作。


练习 3.11

练习 3.11: 完善comma函数,以支持浮点数处理和一个可选的正负号的处理。