Go程序设计语言 学习笔记 第三章 基本数据

时间:2024-03-21 10:03:47

Go的数据类型分四大类:基础类型(basic type)、聚合类型(aggregate type)、引用类型(reference type)、接口类型(interface type)。本章的主题是基础类型,包括数字(number)、字符串(string)、布尔型(boolean)。聚合类型——数组(array,4.1)和结构体(struct,4.4)——是通过各种简单类型得到的更复杂的数据类型。引用是一大分类,其中包括指针(pointer、2.3.2)、slice(4.2)、map(4.3)、函数(function,ch5)、通道(channel,ch8),它们的共同点是全都间接指向程序变量或状态,于是操作所引用数据的效果就会遍及该数据的全部引用。接口类型将在ch7讨论。

3.1 整数

Go的数值类型包括几种不同大小的整数、浮点数、复数。各种数值类型分别有自己的大小,对正负号支持也各异。我们从整数开始。

Go同时具备有符号和无符号整数。有符号整数分四种大小:8位、16位、32位、64位,用int8、int16、int32、int64表示,对应的无符号整数是uint8、uint16、uint32、uint64。

此外还有两种类型int和uint。在特定平台上,其大小与原生的有符号整数\无符号整数相同,或等于该平台上的运算效率最高的值。int是目前使用最广泛的数值类型。这两种类型大小相等,都是32位或64位,但不能认定它们一定是32位,或一定是64位;即使在同样的硬件平台上,不同编译器可能选用不同的大小。

rune类型是int32类型的同义词,常用于指明一个值是Unicode码点(code point,即Unicode里的一个字符的值)。这两个名称可互换使用。同样,byte类型是uint8类型的同义词,强调一个值是原始数据(raw data)。

最后,还有一种无符号整数uintptr,其大小并不明确,但足以完整存放指针。uintptr类型仅用于底层编程,例如Go程序与C程序库或操作系统的接口界面。

int、uint、uintptr都有别于其大小明确的相似类型,即int和int32是不同类型,尽管int天然大小就是32位,并且int值若要当作int32使用,必须显式转换;反之亦然。

有符号整数以补码表示,保留最高位为符号位,n位数字的取值范围是-2 n − 1 ^{n-1} n12$^{n-1}$-1。无符号整数由全部位构成其非负值,范围是02 n ^n n-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。

不论是有符号数还是无符号数,若表示算术运算结果所需的位超出该类型的范围,就称为溢出。溢出的高位部分会被无提示地丢弃。假如原本的计算结果是有符号类型,且最左侧位是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也具备下列位运算符,前四个对操作数逐位独立进行,不涉及算术进位或正负号:
在这里插入图片描述
^作为二元运算符,表示按位异或;作为一元运算符,表示操作数逐位取反。运算符&^是按位清除:表达式z=x&^y中,若y的某位是1,则z的对应位是0;否则,它就等于x的对应位。

以下代码说明如何将uint8作为位集(bitset)处理,其内含8个独立的位,高效且紧凑。Printf用谓词%b以二进制形式输出数值,副词08在这个输出结果前补0,补够8位:

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

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

fmt.Printf("%08b\n", x&y) // "00000010",并集{1}
fmt.Printf("%08b\n", x|y) // "00100110",交集{1,2,5}
fmt.Printf("%08b\n", x^y) // "00100100",对称差{2,5}
fmt.Printf("%08b\n", x&^y) // "00100000",差集{5}

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

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

在移位操作x<<n和x>>n中,操作数n决定位移量,且n必须为无符号型;操作数x可以是有符号型也可以是无符号型。

左移以0填补右边空位。无符号整数右移以0填补左边空位;有符号整数右移以符号位的值填补空位。因此,如果将整数以位模式处理,须使用无符号整型。

尽管有些时候某值不可能为负(如数组下标),直观上应该选无符号数,但还是会选有符号整型,如下例,len函数返回有符号整数:

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变为uint型的最大值,而非-1,导致medals[i]访问越界元素,超出slice范围,引发运行失败或宕机(5.9)。

因此,无符号整数往往只用于位运算符和特定算术运算符,如实现bitset时,解析二进制格式的文件,或散列和加密。一般而言,无符号整数极少用于标识非负的值。

通常,将某种类型的值转换成另一种,需要显式转换。对于算术和逻辑(不含移位)的二元运算符,其操作数的类型必须相同。这有时导致表达式相对冗长,但一整类错误得以避免。

考虑以下语句:

var apples int32 = 1
var oranges int16 = 2
var compote int = apples + oranges // 编译错误

编译这三个声明将产生错误消息:
在这里插入图片描述
类型不匹配问题有几种方法改正,最直接地,将全部操作数转换成同一类型:

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

对于每种类型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"

浮点数转成整型,会舍弃小数,向0取整。如果转换的操作数的值超出了目标类型的取值范围,就应避免这种转换,因为其行为依赖于实现:

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

不论大小和有无符号,整数都能写成十进制、八进制(以0开头,如0666)、十六进制(以0x开头,大小写皆可)数。八进制似乎仅有一种用途——POSIX文件系统的权限。十六进制广泛用于强调其位模式,而非数值大小。

如果使用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) // "3735928559 deadbeef 0xdeadbeef 0xDEADBEEF"

注意fmt的两个技巧。通常Pringf的格式化字符串含有多个%谓词,这要求提供多个操作数,而%后的副词[1]告知Printf重复使用第一个操作数。其次,%o、%x、%X之前的副词#告知Printf输出相应的前缀0、0x、0X。

源码中,文字符号(rune literal)的形式是字符写在一对单引号内。用%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'"

3.2 浮点数

Go具有两种大小的浮点数float32和float64。其算术特性遵循IEEE 754标准,所有新式CPU都支持该标准。

这两个类型的值可从极细微到超宏大。math包给出了浮点值的极限。常量math.MaxFloat32是float32的最大值,大约为3.4e38,而math.MaxFloat64则大约为1.8e308。相应地,最小的正浮点值大约为1.4e-45和4.9e-324。

十进制下,float32的有效数字大约是6位,float64的有效数字大约是15位。绝大多数情况下,应优先选用float64,因为除非格外小心,否则float的运算会迅速累积误差。另外,float32能精确表示的正整数范围有限:

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

小数点前的数字0可以省略(.707),后面的0也可省去(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)))
}

上面的代码按8个字符的宽度输出自然对数e的各个幂方,结果保留三位小数:
在这里插入图片描述
除了大量常见的数学函数外,math包还有函数用于创建和判断IEEE 754标准定义的特殊值:正无穷大和负无穷大,它表示超出最大许可值的数及除以零的商;以及NaN(Not a Number),它表示在数学上无意义的运算结果(0/0或Sqrt(-1))。

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

math.IsNaN函数判断其参数是否是非数值,math.NaN函数则返回非数值(NaN)。在数字运算中,我们倾向于将NaN当作信号值(sentinel value),但直接判断具体的计算结果是否为NaN可能导致潜在错误,因为与NaN的比较总为false(除了!=,它总是与==相反):

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

一个函数的返回值是浮点型且它有可能出错,那么最好单独报错:

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

下一个程序以浮点绘图运算为例。它根据传入两个参数的函数z=f(x,y),绘出三维的网状曲面,绘制过程运用了可缩放矢量图形(Scalable Vector Graphics,SVG),绘制线条的一种标准XML格式。图3-1是函数sin®/r的图形输出样例,其中r为sqrt(xx+yy)。
在这里插入图片描述

// surface函数根据一个三维曲面函数计算并生成SVG
package main

import (
	"fmt"
	"math"
)

const (
	width, height = 600, 320            // 以像素表示的画布大小
	cells         = 100                 // 网格单元的个数
	xyrange       = 30.0                // 坐标轴的范围(-xyrange..+xyrange)
	xyscale       = width / 2 / xyrange // x或y轴上每个单位长度的像素
	zscale        = height * 0.4        // z轴上每个单位长度的像素
	angle         = math.Pi / 6         // x、y轴的角度(=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' "+
		"stype='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) {
	// 求出网格单元(i,j)的在3D坐标系中的(x,y)坐标
	x := xyrange * (float64(i)/cells - 0.5)
	y := xyrange * (float64(j)/cells - 0.5)

	// 计算曲面高度
	z := f(x, y)

	// 将3D坐标(x,y,z)等角投射到二维SVG绘图平面上,坐标是(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) // 到(0,0)的距离
	return math.Sin(r) / r
}

运行它:
在这里插入图片描述
注意,corner函数返回两个值,构成网格单元其中一角的坐标。

理解这段程序只需基本的几何知识,也可略过,因为本例旨在说明浮点运算。这段程序本质上是三套不同坐标系的相互映射,见图3-2。首先是个包含100×100个单元的二维网格,每个网格单元用整数(i,j)标记,从最远处靠后的角落(0,0)开始。我们从后向前绘制,因而后方的多边形可能被前方的遮住。
在这里插入图片描述
第二个坐标系内,网格由三维浮点数(x,y,z)决定,其中x和y由i和j的线性函数决定,经过坐标转换,原点处于*,并且坐标系按照xyrange进行缩放。高度值z由曲面函数f(x,y)决定。

第三个坐标系是二维成像绘图平面(image canvas),原点在左上角。这个平面中点的坐标记作(sx,sy)。我们用等角投影(isometric projection)将三维坐标点(x,y,z)映射到二维绘图平面上。若一个点的x值越大,y值越小,则其在绘图平面上看起来就越接近右方。而若一个点的x值或y值越大,则其在绘图平面上看起来越接近下方。纵向(x)与横向(y)的缩放系数是由30°角的正弦值和余弦值推导而得。z方向的缩放系数为0.4,是个随意选定的参数值。

练习3.4:构建一个Web服务器,计算并生成曲面,同时将SVG数据写入客户端,允许客户端通过HTTP请求参数的形式指定各种值,如高度、宽度、颜色。服务器必须如下设置Content-Type报头:

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

在Lissajous示例中,这一步并不强求,因为该服务器使用标准的启发式规则,根据响应内容最前面的512字节来识别常见的格式(如PNG),并生成正确的HTTP报头。

代码如下:

// surface函数根据一个三维曲面函数计算并生成SVG
package main

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

var cells int

const (
	width, height = 600, 320 // 以像素表示的画布大小
	// cells         = 100                 // 网格单元的个数
	xyrange = 30.0                // 坐标轴的范围(-xyrange..+xyrange)
	xyscale = width / 2 / xyrange // x或y轴上每个单位长度的像素
	zscale  = height * 0.4        // z轴上每个单位长度的像素
	angle   = math.Pi / 6         // x、y轴的角度(=30°)
)

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

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

func surface(w http.ResponseWriter, r *http.Request) {
	if err := r.ParseForm(); err != nil {
		log.Print(err)
		return
	}

	form := make(map[string]string)
	for k, v := range r.Form {
		form[k] = v[0]
	}

	cellsStr, ok := form["cells"]
	if ok {
		var err error
		if cells, err = strconv.Atoi(cellsStr); err != nil {
			log.Print(err)
			return
		}
	} else {
		cells = 100
	}

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

	fmt.Fprintf(w, "<svg xmlns='http://www.w3.org/2000/svg' "+
		"stype='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.Fprintf(w, "<polygon points='%g,%g %g,%g %g,%g %g,%g'/>\n",
				ax, ay, bx, by, cx, cy, dx, dy)
		}
	}
	fmt.Fprintf(w, "</svg>")
}

func corner(i, j int) (float64, float64) {
	// 求出网格单元(i,j)的顶点坐标(x,y)
	x := xyrange * (float64(i)/float64(cells) - 0.5)
	y := xyrange * (float64(j)/float64(cells) - 0.5)

	// 计算曲面高度
	z := f(x, y)

	// 将(x,y,z)等角投射到二维SVG绘图平面上,坐标是(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) // 到(0,0)的距离
	return math.Sin(r) / r
}

运行它,并打开浏览器访问:
在这里插入图片描述
然后指定cells为800:
在这里插入图片描述
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,它就变成一个虚数:

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

复数常量可以和其他常量相加,前面x和y的声明可以简写为:

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

// x和y是复数
x = x - 2i // 此时x为1+0i

可以用==或!=判断复数是否等值。若两个复数的实部和虚部都相等,则它们相等。math/cmplx包提供了复数运算所需的库函数,例如复数的平方根函数和复数的幂函数。

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

以下程序用complex128运算生成一个Mandelbrot集:

// mandelbrot函数生成一个PNG格式的Mandelbrot分形图
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)
			// 点(px,py)表示