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

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)表示复数z
			img.Set(px, py, mandelbrot(z))
		}
	}
	png.Encode(os.Stdout, 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
}

两个嵌套循环在1024×1024的灰度图上逐行扫描每个点,这个图表示复平面-2~+2的区域,每个点都对应一个复数。该程序针对各个点反复迭代计算其平方与自身的和,判断其最终能否“超出”半径为2的圆。若然,就根据超出圆边界所需的迭代次数设定该点的灰度。否则,该点属于Mandelbrot集,颜色留黑。最后程序将标准输出的数据写入PNG图,得到一个标志性的分形:
在这里插入图片描述
练习3.9:编写一个Web服务器,它生成分形并将图像数据写入客户端。要让客户通过HTTP请求的参数指定x、y值和放大系数:

// mandelbrot函数生成一个PNG格式的Mandelbrot分形图
package main

import (
	"fmt"
	"image"
	"image/color"
	"image/png"
	"math/cmplx"
	"net/http"
	"strconv"
)

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

func handler(w http.ResponseWriter, r *http.Request) {
	if err := r.ParseForm(); err != nil {
		fmt.Println(err)
		return
	}

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

	xmin := float64(-2)
	xminStr, ok := form["xmin"]
	var err error = nil
	if ok {
		if xmin, err = strconv.ParseFloat(xminStr, 64); err != nil {
			fmt.Println(err)
			return
		}
	}

	ymin := float64(-2)
	yminStr, ok := form["ymin"]
	if ok {
		if xmin, err = strconv.ParseFloat(yminStr, 64); err != nil {
			fmt.Println(err)
			return
		}
	}

	xmax := float64(2)
	xmaxStr, ok := form["xmax"]
	if ok {
		if xmax, err = strconv.ParseFloat(xmaxStr, 64); err != nil {
			fmt.Println(err)
			return
		}
	}

	ymax := float64(2)
	ymaxStr, ok := form["ymax"]
	if ok {
		if ymax, err = strconv.ParseFloat(ymaxStr, 64); err != nil {
			fmt.Println(err)
			return
		}
	}

	const (
		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)表示复数z
			img.Set(px, py, mandelbrot(z))
		}
	}
	png.Encode(w, 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
}

运行服务器并从浏览器访问:
在这里插入图片描述
3.4 布尔值

bool型的值或布尔值(boolean)只有两种可能:真(true)和假(false)。一元操作符(!)表示逻辑取反。考虑到代码风格,布尔表达式x==true相对冗长,我们总是简写为x。

布尔值可以由运算符&&(AND)和||(OR)组合运算,这可能引起短路行为:如果左边的操作数已经能确定总体结果,则右边操作数不会计算在内,所以下面的表达式是安全的:

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

如果s是空字符串,s[0]会触发宕机异常。

&&较||优先级更高(助记窍门:&&表示逻辑乘法,||表示逻辑加法),所以下面条件不用加圆括号:

if 'a' <= c && c <= 'z' ||
    'A' <= c && c <= 'Z' ||
    '0' <= c && c <= '9' {
    // ...ASCII字母或数字
}

布尔值无法隐式转换成数值(如0或1),反之也不行。以下状况有必要使用if:

i := 0
if b {
    i = 1
}

假如转换操作常常用到,就值得专门为此写个函数:

// 如果b为真,btoi返回1;如果b为假,则返回0
func btoi(b bool) int {
    if b {
        return 1
    }
    return 0
}

反向转换操作过于简单,无须专门撰写函数,但为了与btoi对应,这里还是给出其代码:

// itob报告i是否为非零值
func itob(i int) bool { return i != 0 }

3.5 字符串

字符串时不可变的字节序列,它可以包含任意数据,包括0值字节,但主要是人类可读的文本。习惯上,文本字符串被解读成按UTF-8编码的Unicode码点(文字符号)序列。

内置len函数返回字符串字节数(并非文字符号的数目),下标访问操作s[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')

试图访问许可范围外的字节会触发宕机异常:

c := s[len(s)] // 宕机:下标越界

字符串的第i个字节不一定就是第i个字符,因为非ASCII字符的UTF-8码点需要两个或多个字节。

子串生成操作s[i:j]产生一个新字符串,内容取自原字符串的字节,下标从i(含边界值)开始,直到j(不含边界值)。结果的大小是j-i个字节。

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

上例中,若下标j的值小于i,将触发宕机异常。

操作数i与j的默认值分别是0(字符串起始位置)和len(s)(字符串终止位置),若省略i或j,或两者,则取默认值。

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

加号(+)运算符连接两个字符串而生成一个新字符串:

fmt.Println("goodbye" + s[5:]) // "goodbye, world"

字符串可通过比较运算符做比较,按字节进行比较,结果服从本身的字典排序。

可以将新值赋予字符串变量,但字符串值无法改变:字符串值本身所包含的字节序列永不可变。要在一个字符串后面添加另一个字符串,可以这样编写代码:

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

这并不改变s原有的字符串值,只是将+=语句生成的新字符串赋予s。同时,t仍然持有旧的字符串值。

因为字符串不可改变,所以字符串内部的数据不允许修改:

s[0] = 'L' // 编译错误:s[0]无法赋值

不可变意味着两个字符串能安全地共用同一段底层内存,使得复制任何长度字符串的开销都低廉。类似地,字符串s及其子串(如s[7:])可以安全地共用数据,因此子串生成操作的开销低廉。这两种情况下都没有分配新内存。
在这里插入图片描述
3.5.1 字符串字面量

字符串的值可以直接写成字符串字面量(string literal),形式上是带双引号的字节序列:
在这里插入图片描述
Go的源文件总是按UTF-8编码,且习惯上Go的字符串会按UTF-8解读,所以源码中我们可以将Unicode码点写入字符串字面量。

带双引号的字符串字面量中,转义序列以反斜杠(\)开始,可以将任意字节插入字符串中。以下是一组转义符:
在这里插入图片描述
源码中的字符串也可以包含十六进制或八进制的任意字节。十六进制的转义字符写成\xhh的形式,h是十六进制数字(大小写皆可),且必须是两位。八进制的转义字符写成\ooo,必须使用三位八进制数字(0~7),且不能超过\377.这两者都表示单个字节,内容是给定值。

原生的字符串字面量的书写形式是用反引号`替代双引号。原生的字符串字面量内,转义序列不起作用;实质内容与字面写法严格一致,包括反斜杠和换行符,因此,在程序源码中,原生的字符串字面量可以展开多行。唯一的特殊处理是回车符会被删除(换行符会保留),使得同一字符串在所有平台上的值都相同。

正则表达式往往含有大量反斜杠,可以方便地写成原生的字符串字面量。原生的字面量也适用于HTML模板、JSON字面量、命令提示信息,以及需要多行文本表达的场景。
在这里插入图片描述
3.5.2 Unicode

以前软件只需处理一个字符集:ASCII(美国信息交换标准码)。ASCII(准确地说是US-ASCII)码用7位表示128个“字符”:大小写英文字母、数字、各种标点和设备控制符。这对早期的计算机行业已经够了,但使用其他语言的人无法在计算机上使用自己的文书体系。随着互联网兴起,包含各种语言的数据屡见不鲜。

使用Unicode(unicode.org)可以囊括世界上所有文书体系的全部字符,还有重音符和其他变音符,控制码(如制表符和回车符),以及许多特有文字,对它们各自赋予一个叫Unicode码点的标准数字。在Go的术语中,这些字符记号称为文字符号(rune)。

Unicode第8版定义了超过一百种语言文字的12万个字符的码点。天然适合保存单个文字符号的数据类型是int32,为Go所采用;正因如此,rune类型作为int32类型的别名。

我们可以将文字符号的序列表示成int32值序列,这种表示方式称作UTF-32或UCS-4,每个Unicode码点的编码长度相同,都是32位。这种编码简单划一,但因为大多数面向计算机的可读文本是ASCII码,每个字符只需8位,也就是1字节,导致了不必要的存储空间消耗。而使用广泛的字符数目也少于65556个,字符用16位就能容纳,可以做出改进。

3.5.3 UTF-8

UTF-8以字节为单位对Unicode码点作变长编码。UTF-8是现行的一种Unicode标准,由Go的两位创建者Ken Thompson和Rob Pike发明。每个文字符号用1~4个字节表示,ASCII字符的编码仅占1个字节,而其他常用的文书字符的编码只是2或3个字节。一个文字符号编码的首字节的高位指明了后面还有多少字节。若最高位为0,则标示着它是7位的ASCII码,其文字符号的编码仅占1字节,这样就与传统的ASCII码一致。若最高几位是110,则文字符号的编码占用2个字节,第二个字节以10开始。更长的编码依此类推:
在这里插入图片描述
变长编码的字符串无法按下表直接访问第n个字符,然而有失有得,UTF-8换来许多有用的特性。UTF-8编码紧凑,兼容ASCII,并且自同步:最多追溯3字节,就能定位一个字符的起始位置。UTF-8还是前缀编码,因此它能从左向右解码而不产生歧义,也无须超前预读。于是查找文字符号仅需搜索它自身的字节,不必考虑前文内容。文字符号的字典顺序与Unicode码点顺序一致(Unicode设计如此),因此按UTF-8编码排序自然就是对文字符号排序。UTF-8编码本身不会嵌入NUL字节(0值),这便于某些程序语言用NUL标记字符串结尾。

Go的源文件总是以UTF-8编码,同时,需要用Go程序操作的文本字符串也优先采用UTF-8编码。unicode包具备针对单个文字符号的函数(例如区分字母和数字,转换大小写),而unicode/utf8包则提供了按UTF-8编码和解码文字符号的函数。

许多Unicode字符难以直接从键盘输入;有的看起来十分相似几乎无法分辨;有些甚至不可见。Go中,字符串字面量的转义让我们可以用码点来指明Unicode字符。有两种形式,\uhhhh表示16位码点值,\Uhhhhhhhh表示32位码点值,其中每个h代表一个十六进制数字;32位形式的码点值几乎不需要用到。下面几个字符串都表示长度为6字节的相同串:

"世界"
"\xe4\xb8\x96\xe7\x95\x8c"
"\u4e16\u754c"
"\U00004e16\U0000754"

Unicode转义符也能用于文字符号。下列字符是等价的:

'世' '\u4e16' '\U00004e16'

码点值小于256的文字符号可以写成单个十六进制数转义的形式,如’A’写成’\x41’;更高的码点值必须用\u或\U转义,因此’\xe4\xb8\x96’不是合法的文字符号,虽然这三个字节构成某个有效的UTF-8编码码点。

由于UTF-8的优良特性,许多字符串操作都无需解码。我们可以直接判断某个字符串是否为另一个的前缀:

func HasPrefix(s, prefix string) bool {
    return len(s) >= len(prefix) && s[:len(prefix)] == prefix
}

或者它是否为另一个字符串的后缀:

func HasSuffix(s, suffix string) bool {
    return len(s) >= len(preffix) && s[len(s)-len(suffix):] == suffix
}

或者它是否为另一个的子字符串:

func Contains(s, substr string) bool {
    for i := 0; i < len(s); i++ {
        if HasPrefix(s[i:], substr) {
            return true
        }
    }
	return false
}

按UTF-8编码的文本逻辑同样也适用原生字节序列。上面的函数取自strings包,其实Contains函数的具体实现使用了散列方法让搜索更高效。

下例的字符串包含两个东亚字符:

import "unicode/utf8"

s := "Hello, 世界"
fmt.println(len(s)) // "13"
fmt.println(utf8.RuneCountInString(s)) // "9"

该字符串含有13个字节,而按UTF-8解读,本质是9个码点或文字符号的编码。

我们可以用unicode/utf8包的UTF-8解码器来每次获取一个文字符号:

for i := 0; i < len(s); {
    r, size := utf8.DecodeRuneInString(s[i:])
    fmt.Printf("%d\t%c\t", i, r)
    i += size
}

每次DecodeRuneInString的调用都返回r(文字符号本身)和一个值(表示r的UTF-8编码所占字节数)。Go的range循环也适用于字符串,它按UTF-8解码,可替代以上代码,如下图:
在这里插入图片描述
上图中,'世’以整数输出是19990,该数字转换为十六进制为4e16,而不是内存中存的E4B896,因为E4B896是4e16的UTF-8编码后的结果

我们可用简单的range循环统计字符串中文字符号数目:

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

与其他形式的range循环一样,可以忽略没用的变量:

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

或者,直戳了当地调用utf8.RuneCountInString(s)。

range循环或utf8.DecodeRuneInString都能解读UTF-8编码的字符串,但如果字符串中含任意二进制数,即UTF-8数据出错时,每次读入一个不合理的字节,它们都会把它替换为’\uFFFD’,其输出通常是个黑色六角形或类似钻石的形状,里面有个白色问号。

在程序内部使用文字字符类型可能更加方便,因为它们大小一致,便于在数组和slice中用下标访问。

当[]rune转换作用于UTF-8编码的字符串时,返回该字符串的Unicode码点序列:
在这里插入图片描述
如上图,5个日文字符输出了5个Unicode码点。

上面第一个Printf里的谓词%x(%和x中间有空格)以十六进制数形式输出,并在两个数位间插入空格。

如果把文字符号类型的slice转换成一个字符串,它会输出各个文字符号的UTF-8编码拼接结果:
在这里插入图片描述
若将一个整数转换成字符串,其值按文字符号类型解读,且产生代表该文字符号值的UTF-8码:
在这里插入图片描述
注意,上图中的0x4eac指的是Unicode码点值,如果将该4字节码点值用UTF-8编码,结果会是3字节的E4BAAC。

如果文字符号值非法,将被专门的替换字符取代(见前面的’\uFFFD’):
在这里插入图片描述
在这里插入图片描述
3.5.4 字符串和字节slice

4个标准包对字符串操作特别重要:bytes、strings、strconv、unicode。

strings包提供了许多函数,用于搜索、替换、比较、修整、切分、连接字符串。

bytes包也有类型函数,用于操作字节slice([]byte类型,其某些属性和字符串相同)。由于字符串不可变,因此按增量方式构建字符串会导致多次内存分配和复制。此时使用bytes.Buffer类型会更高效。

strconv包具备的函数,主要用于转换布尔值、整数、浮点数到字符串形式,执行相反转换。另外还有为字符串添加/、去除引号的函数。

unicode包备有判别文字符号值特性的函数,如IsDigit、IsLetter、IsUpper、IsLower。每个函数以单个文字符号值作为参数,并返回布尔值。若文字符号值是英文字母,转换函数(如ToUpper、ToLower)将其转换成指定的大小写。上面所有函数都遵循Unicode标准对字母数字等的分类原则。strings也有类似函数,函数名也是ToUpper和ToLower,它们对原字符串的每个字符做指定变换,生成并返回一个新字符串。

我们将实现一个basename函数,它模仿UNIX shell中的同名实用程序,效果如下图。只要s的前缀各部分按斜杠分割,该版本的basename(s)就能将其移除,文件类型的后缀也被移除:
在这里插入图片描述
初版的basename独自完成全部工作,不依赖任何库:

// basename移除路径部分和.后缀
// e.g., a => a, a.go => a, a/b/c.go => c, a/b.c.go => b.c
func basename(s string) string {
	// 将最后一个'/'和之前的部分全都舍弃
	for i := len(s) - 1; i >= 0; i-- {
		if s[i] == '/' {
			s = s[i+1:]
			break
		}
	}
	// 保留最后一个'.'之前的所有内容
	for i := len(s) - 1; i >= 0; i-- {
		if s[i] == '.' {
			s = s[:i]
			break
		}
	}
	return s
}

简化版利用库函数string.LastIndex:

func basename2(s string) string {
	slash := strings.LastIndex(s, "/") // 如果没找到"/",则slash取值-1
	s = s[slash+1:]
	if dot := strings.LastIndex(s, "."); dot >= 0 {
		s = s[:dot]
	}
	return s
}

path包和path/filepath包提供了一组更加普适的函数,用来操作文件路径等具有层次结构的名字。path包处理以斜杠’/'分段的路径字符串,不分平台。它不适合用于处理文件名,却适合处理其他领域,像URL地址的路径部分。相反地,path/filepath包根据宿主平台(host platform)的规则处理文件名,例如POSIX系统使用/foo/bar,而Microsoft Windows系统使用c:\foo\bar。

看另一个例子,它涉及子字符串操作。任务是接受一个表示整数的字符串,如"12345",从右侧开始每3位数字后面就插入一个逗号,形如"12,345"。这个版本仅对整数有效。对浮点数的处理方式留作练习:

// 函数向表示十进制非负整数的字符串中插入逗号
func comma(s string) string {
	n := len(s)
	if n <= 3 {
		return s
	}
	return comma(s[:n-3]) + "," + s[n-3:]
}

字符串创建后就无法改变。相反地,字节slice的元素允许随意修改。

字符串可以和字节slice相互转换:

s := "abc"
b := []byte(s)
s2 := string(b)

概念上,[]byte(s)转换操作会分配新的字节数组,拷贝填入s含有的字节,并生成一个slice引用,指向整个数组。具备优化功能的编译器在某些情况下可能会避免分配内存和复制内容,但一般而言,复制有必要确保s的字节维持不变(即使b的字节在转换后发生改变)。反之,用string(b)将字节slice转换成字符串也会产生一份副本,保证s2也不可变。

为了避免转换和不必要的内存分配,bytes包和strings包有很多对应的实用函数(utility function)它们两两对应。如strings包具备下面6个函数:
在这里插入图片描述
bytes包里面的对应函数为:
在这里插入图片描述
唯一的不同是,操作对象由字符串变为字节slice。

bytes包为高效处理字节slice提供了Buffer类型。Buffer起初为空,其大小随着各类型数据的写入而增长,如string、byte、[]byte。如下例所示,bytes.Buffer变量无须初始化,原因是零值本来就有效:

package main

import (
	"bytes"
	"fmt"
)

// intsToString与fmt.Sprint(values)类似,但插入了逗号
func intsToString(values []int) string {
	var buf bytes.Buffer
	buf.WriteByte('[')
	for i, v := range values {
		if i > 0 {
			buf.WriteString(", ")
		}
		fmt.Fprintf(&buf, "%d", v)
	}
	buf.WriteByte(']')
	return buf.String()
}

func main() {
	fmt.Println(intsToString([]int{1, 2, 3})) // [1, 2, 3]
}

若要在bytes.Buffer变量后面添加任意文字符号的UTF-8编码,最好使用bytes.Buffer的WriteRune方法,而追加ASCII字符,如’[‘和’]',则使用WriteByte亦可。

bytes.Buffer类型用途极广,在第7章讨论接口的时候,假若I/O函数需要一个字节接收器(io.Writer)或字节发生器(io.Reader),它能代替文件,其中接收器的作用就如上例中的Fprintf一样。

练习3.10:编写一个非递归的comma函数,运用bytes.buffer,而不是简单的字符串拼接:

package main

import (
	"bytes"
	"fmt"
)

func main() {
	s := "12345"
	var buf bytes.Buffer
	for i := 0; i < len(s); i++ {
		buf.WriteByte(s[i])
		if (len(s)-i)%3 == 1 && i != len(s)-1 {
			buf.WriteByte(',')
		}
	}

	fmt.Println(buf.String())
}

练习3.11: 增强comma函数的功能,让其能正确处理浮点数,以及带有正负号的数字:

package main

import (
	"bytes"
	"fmt"
	"strings"
)

func main() {
	s := "-1234567.1234"
	var buf bytes.Buffer

	numBegin := 0
	if s[0] == '+' || s[0] == '-' {
		buf.WriteByte(s[0])
		numBegin = 1
	}

	dot := strings.LastIndex(s, ".")
	integerNum := len(s) - numBegin
	if dot >= 0 {
		integerNum = dot - numBegin
	}

	for i := numBegin; i <= numBegin+integerNum; i++ {
		if s[i] == '.' {
			buf.WriteString(s[i:])
			break
		}

		buf.WriteByte(s[i])
		if (integerNum+numBegin-i)%3 == 1 && i-numBegin != integerNum-1 {
			buf.WriteByte(',')
		}
	}

	fmt.Println(buf.String())
}

运行它:
在这里插入图片描述
练习3.12:编写一个函数判断两个字符串是否同文异构,即它们都含有相同的字符但排列顺序不同:

package main

func main() {
	s1 := "你好"
	s2 := "好你"

	println(isSame(s1, s2))
}

func isSame(s1, s2 string) bool {
	m := make(map[rune]int)
	for _, v := range s1 {
		m[v]++
	}
	for _, v := range s2 {
		m[v]--
	}
	for _, v := range m {
		if v != 0 {
			return false
		}
	}
	return true
}

3.5.5 字符串和数字的相互转换

strconv包进行数值及其字符串表示的转换。

要将整数转换成字符串,一种选择是使用fmt.Sprintf,另一种做法是用函数strconv.Itoa(integer to ASCII):

x := 123
y := fmt.Sprintf("%d", x)
fmt.Println(y, strconv.Itoa(x)) // "123 123"

FormatInt和FormatUint可以按不同的进位制格式化数字:

fmt.Println(strconv.FormatInt(int64(x), 2)) // "1111011",123的二进制

fmt.Sprintf里的谓词%b、%d、%o、%x往往比format函数方便,若要包含数字以外的附加信息,它就尤其有用:

s := fmt.Sprintf("x=%b", x) // "x=1111011"

strconv包内的Atoi函数或ParseInt函数用于解释表示整数的字符串,而ParseUint用于无符号整数:

s, err := strconv.Atoi("123") // x是整型
y, err := strconv.ParseInt("123", 10, 64) // 十进制,最长为64位

ParseInt的第三个参数指定结果必须匹配多大的整型,例如,16表示int16,而0作为特殊值表示int。任何情况下,结果y的类型总是int64。

有时,单行输入由字符串和数字依次混合构成,需要用fmt.Scanf解释,可惜fmt.Scanf也许不够灵活,处理不完整或不规则输入时尤甚。

3.6 常量

常量是一种表达式,可以保证在编译阶段就计算出表达式的值。所有常量本质上都属于基本类型:布尔型、字符串、数字。

常量的声明定义了具名的值,它看起来在语法上与变量类似,但该值恒定,这防止了程序运行过程中的意外(或恶意)修改。如,要表示数学常量,如圆周率,常量比变量更适合,因其值恒定不变:

const pi = 3.14159 // 近似数;math.Pi是更精准的近似

与变量类似,同一个声明可以定义一系列常量,这适用于一组相关的值:
在这里插入图片描述
许多针对常量的计算可以在编译时就完成,以减免运行时的工作量并让其他编译器优化得以实现。某些错误要在运行时才能检测到,但如果操作数是常量,编译时就会报错,例如整数除以0,字符串下标越界,以及任何产生无限大值的浮点数运算。

对于常量操作数,所有数学运算、逻辑运算、比较运算的结果依然是常量,常量的类型转换结果和某些内置函数的返回值,例如len、cap、real、imag、complex、unsafe.Sizeof,通常是常量。

因为编译器知晓其值,常量表达式可以出现在涉及类型的声明中,具体而言就是数组类型的长度:

const IPv4Len = 4
// parseIPv4函数解释一个IPv4地址(d.d.d.d)
func parseIPv4(s string) IP {
    var p [IPvLen]byte
    // ...
}

// 如果是这样就会报错
i := 5
var iArr [i]int

常量声明可以同时指定类型和值,如果没有显式指定类型,则类型根据右边的表达式推断。下例中,time.Duration是一种具名类型,其基本类型是int64,time.Minute也是基于int64的常量(实际类型是time.Duration)。下面声明的两个常量都属于time.Duration类型,通过%T(输出类型)展示:

const noDelay time.Duration = 0
const timeout = 5 * time.Minute
fmt.Printf("%T %[1]v\n", noDelay) // "time.Duration 0"
fmt.Printf("%T %[1]v\n", timeout) // "time.Duration 5m0s"
fmt.Printf("%T %[1]v\n", time.Minute) // "time.Duration 1m0s"

若同时声明一组常量,除了第一项之外,其他项在等号右侧的表达式都可以省略,这意味着会复用前面一项的表达式及其类型。例如:

const (
    a = 1
    b
    c = 2
    d
)

fmt.Println(a, b, c, d) // "1 1 2 2"

3.6.1 常量生成器iota

常量生成器iota创建一系列相关值,而不是逐个值显式写出。常量声明中,iota从0开始取值,逐项加1。

下例取自time包,它定义了Weekday的具名类型,并声明每周的7天为该类型的常量,从Sunday开始,其值为0。这种类型通常称为枚举型(enumeration,或缩写成enum)。

type Weekday int

const (
    Sunday Weekday = iota
    Monday
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
)

上面的声明中,Sunday的值为0,Monday的值为1,依此类推。

更复杂度表达式也可使用iota,借用net包的代码举例如下,无符号整数最低5位数中的每一个都逐一命名,并解释为布尔值:

type Flags uint

const (
    FlagUp Flags = 1 << iota // 向上
    FlagBroadcast // 支持广播访问
    FlagLoopback // 是环回接口
    FlagPointToPoint // 属于点对点链路
    FlagMulticast // 支持多路广播访问
)

随着itoa递增,每个常量都按1<<itoa赋值,这等价于2的连续次幂,它们分别与单个位对应。若某些函数要针对相应的位执行判定、设置、清除操作,就会用到这些常量。

func IpUp(v Flags) bool {
    return v&FlagUp == FlagUp
}

func turnDown(v *flags) {
    *v &^= FlagUp
}

func SetBroadcast(v *Flags) {
    *v |= FlagBroadcast
}

func IsCast(v Flags) bool {
    return v&(FlagBroadcast|FlagMulticast) != 0
}

func main()
{
    var v Flags = FlagMulticast | FlagUp
    fmt.Printf("%b %t\n", v, IsUp(v)) // "10001 true"
    TurnDown(&v)
    fmt.Printf("%b %t\n", v, IsUp(v)) // "10000 false"
    SetBroadcast(&v)
    fmt.Printf("%b %t\n", v, IsUp(v)) // "10010 false"
    fmt.Printf("%b %t\n", v, IsCast(v)) // "10010 true"
}

下例更复杂,声明的常量表示1024的幂:
在这里插入图片描述
然而,iota机制存在局限,比如,因为不存在指数运算符,所以无从生成更为人熟知的1000的幂(KB、MB等)。

3.6.2 无类型常量

Go的常量可以是任何基本数据类型,如int、float64,也包括具名的基本类型(如time.Duration),但许多常量不从属某一具体类型。编译器将这些从属类型待定的常量表示成某些值,这些值比基本类型的数字精度更高,且算数精度高于原生的机器精度。可以认为它们的精度至少达到256位。从属类型待定的常量有6种,分别是无类型布尔、无类型整数、无类型文字符号、无类型浮点数、无类型复数、无类型字符串。

借助推迟确定从属类型,无类型常量不仅能暂时维持更高的精度,与类型已确定的常量相比,它们还能写进更多表达式而无须转换类型。比如,上例中ZiB和YiB的值过大,用哪种整型都无法存储,但它们都是合法常量并且可以用在下面的表达式中:

fmt.Println(YiB/ZiB) // "1024"

再例如,浮点型常量math.Pi可用于任何需要浮点值或复数的地方:

var x float32 = math.Pi
var y float64 = math.Pi
var z complex128 = math.Pi

若常量math.Pi一开始就确定从属于某具体类型,如float64,就会导致结果的精度下降。另外,假设最终需要float32值或complex128值,则可能需要转换类型:

const Pi64 float64 = math.Pi

var x float32 = float32(Pi64)
var y float64 = Pi64
var z complex128 = complex128(Pi64)

字面量的类型由语法决定。0、0.0、0i和’\u0000’全都表示相同的常量值,但类型相异,分别是:无类型整数、无类型浮点数、无类型复数和无类型文字符号。类似地,true和false是无类型布尔值,而字符串字面量则是无类型字符串。

根据除法运算中操作数的类型,除法运算的结果可能是整型或浮点型。所以,常量除法表达式中,操作数选择不同的字面写法会影响结果:

var f float64 = 212
fmt.Println((f - 32) * 5 / 9) // "100";(f-32)*5的结果是float64型
fmt.Println(5 / 9 * (f - 32)) // "0";5/9的结果是无类型整数,0
fmt.Println(5.0 / 9.0 * (f - 32)) // "100";5.0/9.0的结果是无类型浮点数

只有常量才可以是无类型的。若将无类型常量声明为变量(如下第一条语句),所在类型明确的变量赋值的右方出现无类型常量(如下第2~4条语句)则常量会被隐式转换成该变量的类型:

var f float64 = 3 + 0i // 无类型复数 -> float64
f = 2 // 无类型整数 -> float64
f = 1e123 // 无类型浮点数 -> float64
f = 'a' // 无类型 -> float64

上述语句与下面的语句等价:

var f float64 = float64(3 + 0i)
f = float64(2)
f = float64(1e123)
f = float64('a')

不论是隐式或显式,常量从一种类型转换成另一种,都要求目标类型能够表示原值。实数(浮点数)和复数允许舍入取整:

const (
    deadbeef = 0xdeadbeef // 无类型整数,值为3735928559
    a = uint32(deadbeef) // uint32,值为3735928559
    b = float32(deadbeef) // float32,值为3735928576(向上取整)
    c = float64(deadbeef) // float64,值为3735928559(精确值)
    d = int32(deadbeef) // 编译错误:溢出,int32无法容纳常量值
    e = float64(1e309) // 编译错误:溢出,float64无法容纳常量值
    f = uint(-1) // 编译错误:溢出,uint无法容纳常量值
)

变量声明(包括短变量声明)中,假如没有显式指定类型,无类型常量会隐式转换成该变量的默认类型:

i := 0 // 无类型整数;隐式int(0)
r := '\000' // 无类型文字字符;隐式rune('\000')
f := 0.0 // 无类型浮点数;隐式float64(0.0)
c := 0i // 无类型整数;隐式complex128(0i)

注意各类型的不对称性:无类型整数可以转换成int,其大小不确定,但无类型浮点数和无类型复数被转换成大小明确的float64和complex128。Go中只有大小不确定的int类型,而没有大小不确定的float和complex类型,原因是,如果浮点型数据的大小不明,就很难写出正确的数值算法。

要将上例的默认变量类型转换成不同的类型,我们必须将无类型常量显式转换为期望的类型,或在声明时指明想要的类型:

var i = int8(0)
var i int8 = 0

在将无类型常量转换为接口值时(第7章),这些默认类型就分外重要,因为它们决定了接口值的动态类型:
在这里插入图片描述

  • 7
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值