Go语言圣经 —— 基础数据类型
前言
本以为会像原来学习的编程语言一样,老一套地介绍一下数据类型,数据范围,运算符等内容。但是《Go语言圣经》令我对此大为改观,觉得惊艳的原因主要是:
- 第一次知道\(golang\)原生支持复数(
复数的声明、涉及到的相关运算),前面学习量子计算的时候也算是深入学习了一下复数这一块的内容,我觉得这很有趣。虽说其他编程语言可以自己定义设计复数,\(sagemath\)等偏数学的库也支持复数,但是当复数作为一个基础数据类型出现的时候,\(golang\)好感\(+1\)。 - 这本书中的实例很多,特别是有关图像的实例,前期一直想学一下怎么用编程语言绘制出好看的函数图形、概率分布图像以及\(Lattice\)图,以前都是让\(AI\)去编,然后再回头拷打,学习数据类型的过程中,体验了一下自己码绘图代码的快感,也趁着机会学习了一下这方面的几何知识。
其中整型与其他变成语言基本一致,大概内容就是数据类型的表示范围、溢出、类型转换的概念,不过Go语言的Printf的相关参数还是值得再看一下的,也可以回顾参考第一章 入门。最值得看的就是浮点数、复数、字符串这一块。常量的话,感觉可以过一遍,知晓\(golang\)中的常量与其他编程语言不同的地方。
- Go语言将数据类型分为四类:基础类型、复合类型、引用类型和接口类型。本章介绍基础数据类型,包括数字、字符串和布尔型。
- 符合数据类型——数组和结构体是通过组合简单数据类型,来表达更加复杂的数据结构。引用数据类型包括指针、切片、字典、函数、通道,虽然数据种类很多,但它们都是对程序中一个变量或状态的间接引用,这意味着对任一引用类型数据的修改都会影响所有该引用的拷贝。
3.1 整型
Go语言的数值类型包括几种不同大小的整型数、浮点数和复数。每种数值类型都决定了能表示的大小范围和是否支持\(±\)符号。
Go语言同时提供了有符号和无符号类型的整数运算。这里有int8、int16、int32和int64四种不同大小的有符号整数类型,分别对应8、16、32、64\(bit\)大小的有符号整型数,与之对应的是uint8、uint16、uint32、uint64四种无符号整数类型。
这里还有两种一般对应特定\(CPU\)平台机器字大小的有符号和无符号整数int和uint;其中int是最广泛的数据类型。这两种类型有着相同的大小,32或64\(bit\),但是不能对此做任何的假设,因为不同的编译器即使在相同的硬件平台上也可能产生不同的大小。
\(Unicode\)字符rune类型是和int32等价的类型,通常用于表示一个\(Unicode\)码点。这两个名称可以互换使用。同样byte也是uint8类型的等价类型,byte一般用于强调数值是一个原始的数据而不是一个小的整数。
最后,还有一种无符号的郑虎类型uintptr,没有指定具体的bit大小,但是足以容纳指针。uintptr类型只有在底层编程时才需要,特别是Go语言和C语言函数库或操作系统接口相交互的场景。
不管它们的具体大小,int、uint、uintptr是不同的兄弟类型。其中int和int32也是不同的类型,即使int的大小有时也是32\(bit\),在需要将int当作int32类型的地方需要一个显示的类型转换操作,反之亦然。
其中有符号整数采用\(2\)的补码形式表示,也就是最高\(bit\)位用作表符号位,一个\(n-bit\)的有符号整数的值域是\([2^{n-1},2^{n-1}-1]\)。无符号整数的所有\(bit\)位都用来表示非负数,值域是\([0,2^n-1]\)。
补码(
有点记不清补码的意义了,这里补充回顾一下)计算机只有加法器,没有减法器。所以\(A-B\)要变成\(A+(-B)\)来计算。如果我们采用原码来表示负数,即
+1=0000 0001、-1=1000 0001,那么直接按照原码相加的话会有0000 0001 + 1000 0001 = 1000 0010 = -2这样的错误现象。与此同时,+0 = 0000 0000和-0 = 1000 0000都是0的表示,这不仅浪费了一个编码,而且让计算机判断“是否为0”变得复杂。为了解决“负数加法”和"
0有两个编码的问题,设计了补码。补码的核心思想就是模运算,假设我们有3位二进制(范围0~7),这就相当于是一个模8的环,现在数值为2,如果想让数值变为5,有两种操作:
+3(2+3=5)-5(2-5=-3 --> mod8 --> 5)
在uint8中,一个负数的补码=负数+\(2^8\),举个例子,求1的补码,-1+256=255(1111 1111),所以1111 1111就是-1的补码。此时我们就有了1+(-1)=0000 0001 + 1111 1111 = 1 0000 0000,由于溢出,这个结果就是0000 0000 = 0。而我们在第一次学习计算机导论等内容的时候都会遇到一种求补码的方式,整数的补码就是原码,负数的补码是原码取反加
1,例如int8下的-5就是这样子存储的,先求+5的原码0000 0101,取反得到1111 1010,最后加1得到1111 1011,这就是-5的补码。
算数运算符\(+\)、\(-\)、\(*\)、\(/\)可以适用于整数、浮点数和复数,但是取模运算符\(\%\)仅用于整数间的运算。对于不同编程语言,\(\%\)取模运算的行为可能并不相同。在Go语言中,\(\%\)运算符的符号和被取模数的符号总是一致的,因此-5%3和-5%-3的结果都是-2。除法运算符\(/\)的行为则依赖于操作数是否全为整数,比如5.0/4.0的结果是1.25,但是5/4的结果是1,因为整数除法会向0方向截断余数。
如果一个算术运算的结果,不管是有符号或者是无符号的,如果需要更多的\(bit\)位才能正确表示的话,就说明计算结果一处了,超出的高位\(bit\)位部分将被丢弃。如果原始的数值是有符号类型,而且最左边的\(bit\)是1的话,那么最终结果可能是负的,例如:
package main
import "fmt"
func main() {
var u uint8 = 255
fmt.Println(u, u + 1, u * u) // 255 0 1
// (uint8)255 -> 11111111
// 1[00000000] --> u + 1 = 0
// 255 * 255 = 0b11111110[00000001] = 1
var i int8 = 127
fmt.Println(i, i + 1, i * i) // 127 -128 1
// (int8)255 -> 01111111
// 10000000
}
两个相同的整数类型可以使用下面的二元比较运算发比较,比较表达式的结果是布尔类型:
== equal to
!= not equal to
< less than
<= less than or equal to
> greater
>= greater than or equal to
事实上,布尔型、数字类型和字符串等基本类型都是可比较的,也就是说两个相同类型的值可以用==或!=进行比较。此外,整数、浮点数和字符串可以根据比较结果排序。许多其它类型的值可能是不能比较的,因此也就可能是不可排序的。
Go语言还提供了以下的\(bit\)位操作运算符,前面\(4\)个操作运算符并不区分是有符号还是无符号数:
& 位运算 AND
| 位运算 OR
^ 位运算 XOR
&^ 位清空 (AND NOT)
<< 左移
>> 右移
位操作运算符^作为二元运算符时时按位异或(\(XOR\)),当用作一个一元运算符时表示按位取反。其中&^的操作解释为a &^ b = a & (^b),即先将b按位取反,此后与a进行“与”操作。直观理解就是将a中在 b 为 1 的对应位上全部清零(设置为0),其余位保持不变。
下面的代码演示了如何使用位操作解释uint8类型值得8个独立的\(bit\)位。它使用了Pintf函数的%b参数打印二进制格式的数字:其中%08b表示打印至少8个字符宽度,不足的前缀部分用0填充:
package main
import "fmt"
func main() {
var x uint8 = 1 << 1 | 1 << 5 // 0000 0010 | 0010 0000 = 0010 0010 = 34
var y uint8 = 1 << 1 | 1 << 2 // 0000 0010 | 0000 0100 = 0000 0110 = 6
fmt.Printf("%08b\n", x) // 00100010
fmt.Printf("%08b\n", y) // 00000110
fmt.Printf("%08b\n", x & y) // 00000010
fmt.Printf("%08b\n", x | y) // 00100110
fmt.Printf("%08b\n", x ^ y) // 00100100
fmt.Printf("%08b\n", x &^ y) // 00100000
for i := uint(0); i < 8; i++ {
if x & (1 << i) != 0 {
fmt.Println(i) // set{1, 5}
}
}
fmt.Printf("%08b\n", x << 1) // 01000100
fmt.Printf("%08b\n", x >> 1) // 00010001
}
在x << n和x >> n移位操作中,决定了移位操作\(bit\)数部分的i必须是无符号数;被操作的数x可以是有符号或无符号整数。算术上,一个x << n左移运算等价于乘以\(2^n\),一个x >> n右移运算等价于除以\(2^n\)。
尽管Go语言提供了无符号数和运算,即使数值本身不可能出现负值,一般还是倾向于使用有符号的int类型,就像数组的长度一样,虽然使用uint无符号类型似乎是一个更合理的选择。事实上,内置的len函数返回的就是一个有符号int:
medals := []string{"gold", "silver", "bronze"}
for i := len(medals) 1; i >= 0; i-- {
fmt.Println(medals[i]) // "bronze" "silver" "bronze"
}
另一个选择对于上面的例子则是失败的,如果len函数返回一个无符号数,那么i也将是无符号的uint类型,此时条件i >= 0永远为真。在三次迭代之后,也就是i == 0是,i--语句将不会产生-1,而是变成了一个uint类型的最大值,然后medals[i]表达式将发生运行时\(panic\)异常,也就是常说的“数组越界”。
一把来说,需要一个现实的转换将一个值从一种类型转化为另一种类型,并且算数和逻辑运算的二元操作中必须是相同的类型。虽然着偶尔会产生很长的表达式,但是它消除了所有与类型相关的问题,而且使程序很容易理解。
var apples int32 = 1
var oranges int16 = 2
var compote int = apples + oranges // compile error
// invalid operation: apples + oranges (mimatched 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.Printf(f, i) // 3.141 3
f = 1.99
fmt.Println(int(f)) // 1
任何大小的整数字面值都可以用以0开头的八进制格式书写,例如0666;或用以0x或0X开头的十六进制格式书写。十六进制中的字母不区分大小写。如今八进制数据通常用于\(POSIX\)操作系统上的文件访问权限标志,十六进制数字则更强调数字值得\(bit\)位模式。
当使用fmt包打印一个数值时,我们可以用%d、%o、%x参数控制输出得进制格式:
package main
import "fmt"
func main() {
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) // 3735928559 deadbeef 0xdeadbeef 0XDEADBEEF
}
这里有两个fmt的使用技巧。
- 通常
Printf格式化字符串包含多个%参数时将会包含对应数量的额外操作数,但是%之后的[1]副词告诉Printf函数再次使用第一个操作数。 %后的#副词告诉Printf函数在用%b、%o、%x、%X输出时生成0、0x、0X前缀。
字符面值通过一对单引号直接包含字符。最简单的例子是\(ASCII\)中类似'a'写法的字符面值,但是我们也可以通过转义的数值来表示任意的\(Unicode\)码点对应的字符。
字符使用%c参数打印,或者使用%q参数打印带单引号的字符:
package main
import "fmt"
func main() {
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'
}
3.2 浮点数
Go语言提供了两种精度的浮点数,float32和float64。它们的算术规范由\(IEEE754\)浮点数国际标准定义,该浮点数规范被所有现代\(CPU\)支持。
这些浮点数类型的取值范围可以从很小到很大。浮点数的范围极限值可以在math包找到。查改良math.MacFloat32表示float32能表示的最大数值,大约是3.4e38;对应的math.MaxFloat64常量大约是1.8e308。它们分别能表示的最小是近似为1.4e-45和4.9e-324。
一个float32类型的浮点数可以提供大约6个十进制数的精度,而float64则可以提供约15个十进制数的精度;通常应该优先使用float64类型,因为float32类型的累计计算误差很容易扩散,并且float32能精确表示的正整数并不是很大(因为float32的有效\(bit\)位只有23个,其他的\(bit\)位用于指数和符号;当整数大于23\(bit\)能表达的范围时,float32的表示将出现误差):
var f float32 = 16777216 // 1 << 24
fmt.Println(f == f + 1) // true
浮点数的字面值可以直接写小数形式:
const e = 2.71828 // (approximately)
小数点前面或后面的数字可能被省略。很小或很大的数最好用科学计数法书写,通过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)))
}
上面代码打印e的幂,打印精度是小数点后三个小数精度和8个字符宽度。
x = 0 e^x = 1.000
x = 1 e^x = 2.718
x = 2 e^x = 7.389
x = 3 e^x = 20.086
x = 4 e^x = 54.598
x = 5 e^x = 148.413
x = 6 e^x = 403.429
x = 7 e^x = 1096.633
math包中除了提供大量常用的数学函数外,还提供了\(IEEE754\)浮点数标准中定义的特殊值的创建和测试;正无穷大和负无穷大,分别用于表示太大溢出的数字和除零的结果;还有\(NaN\)非数,一般用于表示无效的除法操作结果\(0\over 0\)或\(\sqrt{-1}\)。
var z float64
fmt.Println(z, -z, 1/z, -1/z, z/z) // 0 -0 +Inf -Inf NaN
函数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
fmt.Println(math.IsNaN(nan)) // true
如果一个函数返回的浮点数结果可能失败,最好的做法是用单独的标志报告失败,像这样:
func compute() (value float64, ok bool) {
// ...
if failed {
return 0, false
}
return result, true
}
接下来的程序演示了通过浮点数计算生成的图形。它是带有两个参数的\(z=f(x,y)\)函数的三维形式,使用了可缩放矢量图形(\(SVG\))格式输出,\(SVG\)是一个用于适量先绘制的\(XML\)标准。下面的这幅图显示了\({sin(r)\over r}\)函数的输出图像,其中\(r=\sqrt{x^2+y^2}\):
// 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 range (-xyrange..+xyrange)
xyscale = width / 2 / xyrange // pixels per x or y uint
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) isomterically onto 2-D SAV 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
}
等角投影(\(Isometric\ Projection\))
我们要把一个\(z=f(x,y)\)的\(3-D\)坐标\((x,y,z)\)画在\(2-D\)的显示器屏幕\((sx,sy)\)上,这其中用到了等角投影技术。
想象正从一个特定的斜上方俯视这个\(3-D\)网格。
- 屏幕的\(x\)轴(水平向右)和\(y\)轴(垂直向下)是\(2-D\)画布的绝对方向;
- \(3-D\)空间里的\(x\)轴被我们设定为向屏幕右下方延伸,与屏幕水平线夹角为\(30°\);
- \(3-D\)空间里的\(y\)轴被我们设定为向屏幕左下方延伸,与屏幕水平线夹角为\(30°\);
- \(3-D\)空间里的\(z\)轴垂直向上,对应屏幕的正上方。
基于这个设定,数学推导如下:
- 屏幕的水平坐标\(sx\):
- 当\(3-D\)的\(x\)增加时,\(sx\)向右移动(正向):距离为\(x×cos(30°)\);
- 当\(3-D\)的\(y\)增加时,\(sx\)向左移动(负向):距离为\(y×cos(30°)\);
- 合并起来,并加上画布中心点的偏移:\(sx={width\over2}+(x-y)×cos(30°)×xyscale\)
- 屏幕的垂直坐标\(sy\):
- 当\(3-D\)的\(x\)和\(y\)增加时,它们它们都向屏幕下方走(正向):距离分别为\(x×sin(30°)\)和\(y×sin(30°)\);
- 当\(3-D\)的\(z\)(高度)增加时,它向屏幕上方走(负向,因为网页/SVG的\(sy=0\)在最顶端):距离为\(z×zscale\);
- 合并起来,并加上画布中心点的偏移:\(sy={hight\over 2}+(x+y)×sin(30°)×xyscale-z×zscale\)
这就是corner函数最难懂的两行代码的数学来源。

整个程序的本质时一条从“网格索引”到屏幕像素的流水线,分为三步:
- 二维网格索引\((i,j)\)映射到\(3-D\)数学坐标\((x,y)\):
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)
// ...
}
这里的cells = 100,意味着网格是100 × 100的格子。i和j的范围是0到100。
float64(i) / cells:把[0, 100]映射成0.0到1.0的比例。-0.5:把比例偏移为[-0.5, 0.5],这样中心点就变成了(0, 0)。* xyrange:由于xyrange是30.0,所以最终生成的数学坐标\((x, y)\)落在[-15.0, 15.0]之间。
- 计算高度\(z\),完成\(3-D\)建模
z := f(x, y)
// ...
func f(x, y float64) float64 {
r := math.Hypot(x, y) // 即数学里的 r = sqrt(x^2 + y^2)
return math.Sin(r) / r
}
通过\(f(x,y)\)算出高度\(z\)。由于这里的函数是\({sin(r)\over r}\)(也被称为\(Sinc\)函数),它会在原点附近产生一个高耸的波峰,四周形成水波纹般的起伏。
3. 执行等角投影,输出\(SVG\)
// Project (x, y, z) isomterically 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
这里使用了我们上面介绍的几何原理。xyscale和zcale相当于摄像机的缩放镜头,用来确保算出来的像素缩表能够合适地塞进600 × 320的画布里。
画家算法(\(Painter's\ Algorithm\))
在main函数的双重循环中:循环从0开始是因为在这套投影下,i和j越小,说明这个网格在\(3-D\)空间中离观察者越远。先画远处的网格,再画近处的网格,近处的网格就会自然地遮挡住远处的网格,这再计算机图形学中叫做“画家算法”。
for i := 0; i < cells; i++ {
for j := 0; j < cells; j++ {
// 计算当前网格ABCD四个角的屏幕坐标...
fmt.Printf("<polygon points='%g,%g %g,%g %g,%g %g,%g' />\n", ...)
3.3 复数
Go语言提供了两种精度的复数类型:complex64和complex128,分别对应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 * i = -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() {
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(os.Stdout, img) // NOTE: ignoring errors
}
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
}
\(Mandelbrot\)集合(曼德勃罗集)
数学中最著名的分形(\(Fractal\))图像之一。它最神奇的地方在于,无论你放大多少倍,都能在边缘看到极其复杂且无限重复的相似结构。
在数学上,\(Mandelbrot\)集合基于一个非常简单的迭代公式:\[Z_{n+1}=Z_n^2+C \]\(C\)是复平面上的一个坐标点(也就是我们要测试的像素点),\(Z\)的初始值为\(0\)(即\(Z_0=0\))。规则如下,我们将一个点\(C\)代入公式,算出了一个新的\(Z_1\),然后把\(Z_1\)再代入公式算出\(Z_2\),如此反复迭代。
- 如果随着迭代次数的增加,\(\Vert Z\Vert\)无限膨胀,飞向无穷大,那么这个点\(C\)就不属于\(Mandelbrot\)集合。
- 如果无论迭代多少次,\(\Vert Z\Vert\)始终在一个有限范围内跳动,那么这个点\(C\)就属于\(Mandelbrot\)集合。(在图像中通常被涂成纯黑色)
- 构建画布和坐标映射
const (
xmin, ymin, xmax, ymax = -2, -2, +2, +2
width, height = 1024, 1024
)
- 复平面是无限大的,但\(Mandelbrot\)集合得主体恰好落在了一个
x和y都在[-2, 2]得正方形区域内。(与逃逸条件为\(|Z_n|\leq 2\)有关) width与height的值说明了我们要生成一张1024×1024像素的图片。
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)
// ...
- 像素坐标
(px, py)的范围是[0, 1024],但是我们需要把它等比例映射到数学平面的[-2.0, 2.0]之间。float64(px) / width算出当前像素点在整行中的百分比(0.0到1.0)- 乘以
(xmax - xmin)也就是4,把百分比拉伸到宽度为4的范围。 - 加上
xmin也就是-2.0,就把范围平移到了[-2, 2],最后z := complex(x, y)把计算出来的二维坐标组装成了复数,这就是公式中的\(C\)。
mandelbrot函数
这个函数接收复数z(代表像素点\(C\)),并返回该像素点应该涂什么颜色。
func mandelbrot(z complex128) color.Color {
const iterations = 200
const contrast = 15
var v complex128 // 初始值为 0+0i
// ...
}
- 代码中的
z就是公式中的常数\(C\),代码总的v就是公式中不断迭代的\(Z\),Go语言会默认其为0+0i,这正式\(Z_0\)
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
}
v = v * v + z就是公式中的\(Z_{n+1}=Z_n^2+C\)的代码实现;complx.Abs(v) > 2:数学家已经证明,只要\(|Z|\)超过了\(2\),它就注定会飞向无穷大,这就是“逃逸条件”。- 上色逻辑(逃逸):如果在
200此循环内逃逸了,说明它不在集合内。代码根据它“逃跑”的快慢(迭代次数\(n\)),用255 - contrast * n计算出一个灰度值返回,逃跑得越快,颜色越浅。 - 上色逻辑(属于集合):如果
200此=次循环都跑完了,绝对值还是没有超过2,我们就认为它属于\(Mandelbrot\)集合,函数返回color.Black以作标记。

3.4 布尔型
一个布尔类型的值只有两种:true和false。if和for语句的条件部分都是布尔类型的值,并且==和<等比较操作也会产生布尔型的值。一元操作符!对应逻辑非操作,因此!true的值为false。
布尔值可以和&&(AND)和||(OR)操作符结合,并且可能会有短路行为:如果运算符左边值已经可以确定整个布尔表达式的值,那么运算符右边的值将不再被求值,因此下面的表达式总是安全的:
s != "" && s[0] == 'x'
其中s[0]操作可能会导致\(panic\)异常(数组越界)。
因为&&的优先级比||高(&&对应逻辑乘法,||对应逻辑加法,乘法比加法优先级要高),下面的布尔表达式是不需要加小括号的:
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 return 1 if b is true and 0 if false.
func btoi(b bool) int {
if b {
return 1
}
return 0
}
// itob
func itob(i int) bool {
return i != 0
}

浙公网安备 33010602011771号