Go 基本数据

第 2 章 基本数据类型

Go 的数值类型包括了不同大小的整数 、浮点数 、复数;

各种数值类型分别有自己的大小,对正负号支持也各不相同;

   

1. 整数(OK)

整数类型(整型)
整数类型Go 语言同时支持 有符号整数无符号整数
有符号整数

有符号整数分 4 种大小:8 位 、16 位 、32 位 、64 位

int8int16int32int64
无符号整数

无符号整数也分 4 种大小:8 位 、16 位 、32 位 、64 位

uint8uint16uint32uint64

平台

默认整型

还有两种类型
intuint

在特定平台上,其大小与原生的有符号整数 \ 无符号整数相同,

或等于该平台上的运算效率最高的值

" int " 是目前使用最广泛的数值类型

" int " 和 " uint " 在同一台机器上的大小相等,要么都是 32 位,要么都是 64 位;

但不能认为一定就是 32 位,或一定就是 64 位;

即使在同样的硬件平台上,不同的编译器可能选用不同的大小

两个类型

同义词

" rune " 类型是 int32 类型的同义词

常用于指明一个值是 Unicode 码点(code point)

" byte " 类型是 uint8 类型的同义词

强调一个值是原始数据,而非量值

rune 和 int32 可以互换使用

byte 和 uint8 可以互换使用

特殊

无符号整数

还有一种无符号整数 " uintptr " ,其大小并不明确,但足以完整存放指针

" uintptr " 类型仅仅用于底层编程

例如,在 Go 程序与 C 程序库或操作系统的接口界面

类型有别

显式转换

" int " 、" uint " 、" uintptr " 都有别于其他大小明确的相似类型的类型;

也就是说," int " 和 " int32 " 是不同类型,尽管 " int " 天然的大小就是 32 位

如果 " int " 值要当做 " int32 " 来使用,必须显式转换;反之亦然

取值范围

有符号整数在机器中以 " 补码 " 表示,保留最高位作为符号位

n 位类型的取值范围是 -2^{n-1} ~ 2^{n-1} - 1

无符号整数由全部位构成非负数值;

n 位类型的取值范围是 0 ~ 2^n - 1

有符号

类型示例

有符号整型 int8 占 8 位,最高位是符号位,剩余 7 位表示数值;

表示范围是 -128 ~ 127

有符号类型的

三种表示

有符号整数用最高位作为符号位,0 代表正数,1 代表负数

此处用 int8 作为示例:

由于最高位作为符号位,只有 7 个位能用来表示数值

原码表示法:

0,000 0000 表示 +0

0,111 1111 表示 +127

1,000 0000 表示 -0

1,111 1111 表示 -127

反码表示法:

原码的符号位不变,剩余数值位,均取反;数值位进位,但不影响符号位

0,111 1111 表示 +0

0,000 0000 表示 +127

1,111 1111 表示 -0

1,000 0000  表示 -127

补码表示法:

符号位不变,反码加 1

0,111 1111 + 1 -> 0,000 0000  表示 +0 即为 0

0,000 0000 + 1 -> 0,000 0001 表示 +127

1,111 1111 + 1 -> 1,000 0000 表示 -0 ,不看符号位,值为 128 ,重定义为 -128

1,000 0000 + 1 -> 1,000 0001 表示 -127

从意义上看,-127 的二进制数减 1 后应该是 -128 ,也就是上面的 -0 ,

所以将 -0 定义为 -128

无符号

类型示例

无符号整型 uint8 占 8 位,全部位数用来表示数值

0000 0000 :0

1111 1111 : 255

运算符
二元运算符

Go 的二元操作符包括了 算术 、逻辑 、比较 等运算;

按优先级降序排列如下:左高右低,上高下低

运算符

优先级表

优先级降序序号值越小,优先级越高
1234567
*/%<<>>&&^
891011
+-|^
121314151617
==!=<<=>>=
18
&&
19
| |

说明

如上表所示,二元运算符分五大优先级;

同级别的运算符满足左结合律,为求清晰,可能需要圆括号来指定计算次序;

示例: mask & (1 << 28)

复合赋值

运算符

上述列表中前两行的运算符(如加法运算 +)都有对应的赋值运算符(如 +=),

用于简写赋值语句

运算符

应用场景

算术运算符 + 、- 、* 、/ 可用于整数 、浮点数 、复数
取模运算取模运算符 % 只能用于整数

取模运算符的行为因编程语言而异

在 Go 语言中,取模余数的符号总是与被除数保持一致

示例:

-5 % 3 = -1 ... -2

-5 % (-3)  = 1 ... -2

除法运算

除法运算( / )的行为取决于操作数是否都为整数

整数相除,商会舍弃小数部分

示例:

5.0 / 4.0 -> 1.25

5 / 4 -> 1

溢出

无论是有符号数还是无符号数,若表示算术运算结果所需的位超出该类型的范围,

就称为 "溢出"

溢出的高位部分会无提示地丢弃

假如原本的计算结果是有符号类型,且最左侧位是 1 ,则会形成负值,

以 int8 为例:

var u uint8 = 255

fmt.Ptintln(u, u+1, u*u)    // " 255  0  1 "

var i int8 = 127

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

比较运算

下列二元比较运算符用于比较两个类型相同的整数;

比较表达式本身的类型是布尔型

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

(i). 实际上,所有基本类型的值(布尔值 、数值 、字符串)都可以比较,

     这意味着两个相同类型的值,可以用 " == " 和 " != " 运算符比较

(ii). 整数 、浮点数 、字符串能根据比较运算进行排序

      许多其他类型的值是不可比较的,也无法排序

后面介绍每种类型时,将会分别说明比较规则

一元运算Go 语言支持一元加法和一元减法运算符
+一元取正(无实际影响)
-一元取负

对于整数,+x 是 0+x 的简写,而 -x 是 0-x 的简写;

对于浮点数和复数,+x 就是 x ,-x 是 x 的相反数

位运算Go 语言支持下列 " 位运算符 "
&位运算 "与" AND

对操作数的运算逐位独立进行,

不涉及算术进位或正负号

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

(i).  如果作为二元运算符,运算符 " ^ " 表示按位 " 异或(XOR) " ,

      即两数相同返回 0 ,两数不同返回 1

(ii). 如果作为一元前缀运算符,运算符 " ^ " 表示按位取反或按位取补,

      运算结果就是操作数逐位取反

与反 &^

运算符 " &^ " 的作用是按位清除(AND NOT)

" &^ " 两侧操作数的对应位,先进行 " 与 " 操作,再执行 " 取反 " 操作

表达式  z = x &^ y 中,若 y 的某位是 1,则 z 的对应位等于 0 ;

否则,就等于 x 的对应位

位运算示例

下面的代码说明了如何用位运算,将一个 uint8 的值当作位集(bitset)处理,

其含有 8 个独立的位,高效且紧凑

Printf 用谓词 %b 以二进制形式输出数值,

副词 08 在这个输出结果前补零,补够 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.Printf(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 中:

(i). 操作数 n 决定操作数 x(二进制形式)左移/右移的位数,且 n 必须为无符号整型

(ii). 操作数 x 可以是有符号整型,也可以是无符号整型

(iii). 算术意义上 :

       左移运算 x << n 等价于 x * 2^n

       右移运算 x >> n 等价于 x / (2^n) ,最终结果舍弃小数部分

(iv). 左移用 0 填补右边空位;

       无符号整数右移用 0 填补左边空位,有符号整数右移用符号位的值填补左边空位

        注意:如果将整数以位模式进行处理,须使用无符号整型

说明

尽管 Go 具备无符号整型和相关算术运算,也尽管某些量值不可能为负,

但是我们往往采用有符号整型数,如数组的长度(尽管长度不可能为负)

示例

下面例子从后往前输出奖牌名称,循环里面用到了内置的 len 函数,

len 返回有符号整数

medals := []string{ "gold","silver","bronze" }

for  i  :=  len(medals) - 1;i  >=  0;i-- {

    fmt.Println(medals[i])    // "bronze","silver","gold"

}

无符号整型

运算易导致

严重错误

相反,假如 len 返回的结果是无符号整型,就会导致严重错误,

因为 i 随之也成为无符号整型,而根据定义,条件 i >= 0 将恒成立;

第 3 轮迭代后,有 i == 0 ,语句 i-- 使得 i 变成无符号整型的最大值,比如 2^{64} - 1

而非 -1 ;

导致 medals[i] 试图越界访问元素,超出 slice 范围,引发运行失败或宕机

Tips 

无符号整数往往只用于位运算符和特定算术运算符,如实现位集 、

解析二进制格式的文件 、散列 、加密

一般而言,无符号整数极少用于表示非负值

类型转换

通常,将某种类型的值转换成另一种类型,需要显示转换

算术和逻辑(不含移位)的二元运算符,其操作数的类型必须相同

虽然这样有时会导致表达式相对冗长,但是一个系列的错误得以避免,程序更易理解

示例

var  apples  int32  =  1

var  oranges  int16  =  2

var  compote  int  =  apples  +  oranges    // 编译错误:不同类型无法在一起运算

尝试编译这三个声明将产生错误消息:

非法操作:apples + oranges ( int32 与 int16 类型不匹配 )

类型不匹配(+ 的问题)有几种方法修正,最直接地,将全部操作数转换成同一类型:

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

类型转换

对于某种类型 T ,若允许转换,操作 T(x) 会将 x 的值转换成类型 T;

var x X = x1

var t T = t1

t = T(x)

很多整型 - 整型转换( int16 <-> int32 )不会引起值的变化,

仅告知编译器如何解读该值;

不过,缩减大小的整型转换( int64 -> int16 ),

以及整型与浮点型的相互转换( int64 <-> float64 ),

可能会改变值或损失精度

示例

f  :=  3.141  // a  float64

i  :=  int(f)

fmt.Println(f,i)    // " 3.141   3 "

f = 1.99

fmt.Println(int(f))    // " 1 "

类型转换

浮点型转换为整型,会舍弃小数部分,即趋零截尾(正值向下取整,负值向上取整),最终的结果相比之前,都更接近零

如果有些转换的操作数的值,超出了目标类型的表示范围 ( float64 -> int8 ),

就应当避免这种转换,因为其行为依赖于具体实现

示例

f  :=  1e100    // a float64

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

整型

三种进制

无论有无大小和符号限制 :

(i).  源码中的整数都能写成常见的十进制数;

(ii). 也能写成八进制数,以 0 开头,如 0888

(iii). 还能写成十六进制数,以 0x 或 0X 开头,如 0xdeadBEEF

       十六进制数中的 x 或 a 到 f ,大小写皆可

(iv). 当前,八进制数似乎只有一种用途 -- 表示 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\n",x)

// 输出:

// 3735928559  deadbeef  0xdeadbeef  0XDEADBEEF

说明

注意 fmt 的两个技巧:

(i). 通常 Printf 的格式化字符串含有多个 % 谓词,这要求提供相同数目的操作数,

     而 % 后面的副词 [1] 告诉 Printf 函数,重复使用第一个操作数

(ii). 其次,%o 、%x 或 %X 之前的副词 # ,则告诉函数 Printf 输出相应的前缀 0 、

      0x 、0X

说明

在源码中,文字符号(rune literal)的形式是字符写在一对单引号内;

最简单的例子就是 ASCII 码字符,如 'a' ,但也可以直接使用 Unicode 码点(codepoint)或码值转义

示例

用 %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' "

2. 浮点数

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

浮点型

表示范围

这两个类型的值可从极细微到超宏大

math 包给出了浮点值的极限;

常量 math.MaxFloat32 是 float32 的最大值,大约为 3.4e38,最小值为 1.4e-45

常量 math.MaxFloat64 是 float64 的最大值,大约为 1.8e308,最小值为 4.9e-324

十进制下,float32 的有效数字大约是 6 位,float64 的有效数字大约是 15 位;

绝大多数情况下,应优先选用 float64 ,因为除非格外小心,否则 float32 的运算

会迅速累积误差;

另外,float32 能精确表示的正整数范围有限:

示例

var  f  float32  =  16777216  // 1 << 24

fmt.Println( f  ==  f+1 )          // "true"

在源码中,浮点数可以写成小数,如:

const  e  =  2.71828  // ( 近似值 )

特别技巧

(i).  小数点前的数字可以省略( .707 即 0.707),小数点后面的也可以省略( 1. )

      省去的部分都为 0

(ii). 非常小或非常大的数字最好使用科学计数法表示,此方法在数量级指数前

      写字母 e 或 E

      const  Avogadro  =  6.02214129e23

      const  Planck  =  6.62606957e-34

浮点数的

格式化输出

浮点值能方便地通过 Printf 的谓词 %g 输出,该谓词会自动保持足够的精度,

并选择最简洁的表示方式,但是对于数据表,%e(有指数)或 %f(无指数)的形式可能更合适。

这三个谓词( %g 、%e 、%f )都能掌控输出宽度和数值精度

%n.xf  n 表示输出一个占多少个位的长度,x 表示小数点后保留多少位

示例

for  x  :=  0;x  <  8;x++ {

        fmt.Ptintf("x = %d e^x = %8.3f",x,math.Exp(float64(x)))

}

上面的代码按 8 个字符的宽度输出自然对数 e 的各个幂方,结果保留三位小数

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 包还有一些函数,用于创建和判断 IEEE 754 标准定义的特殊值:

正无穷大和负无穷大,表示超出最大许可值的数 、以及除以零的商

NaN(Not a Number),它表示数学上无意义的运算结果(如 0/0 或 Sqrt(-1) )

math.IsNaN 函数判断其参数是否为非数值,math.Nan 函数则返回非数值( NaN )

在数字运算中,我们倾向于将 NaN 当做信号值( sentinel value ),但直接判断具体的计算结果是否为 NaN 可能导致潜在错误,因为与 NaN 的比较总不成立(除了 != ,它总是与 ==  相反)

示例

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),绘出

三维的网线状曲面,绘制过程中运用了可缩放矢量图形(Scalabe Vector Graphics,SVG),绘制线条的一种标准 XML 格式;图 3-1 是函数 sin(r)/r 的图形输出样例,

其中 r 为 sqrt(x*x + y*y)

代码

// 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(angel)  // sin(30),cos(30)

func main() {

    

3. 复数(OK)

复数类型
两种复数类型

Go 语言中内置了两种大小的复数 complex64 和 complex128 ,

两者分别由 float32 和 float64 构成

complex内置的 complex 函数,根据给定的实部和虚部创建复数
real 、imag内置的 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.1415926i 或 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 包提供了复数运算所需的库函数;

例如,复数的平方根函数 、复数的幂函数

示例:下面的程序用 complex128 运算生成一个 Mandelbrot 集

// madelbrot 函数生成一个 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
}

4. 布尔值(OK)

布尔类型

布尔类型声明

var a bool

var b bool = false

取值范围true(真)或 false(假)
使用场景

if 和 for 语句中的条件就是布尔值,

比较操作符(如 == 和 < )也能得出布尔值结果

取反操作

一元操作符(!)表示逻辑取反,因此 !true 就是 false ,

或者 (!true == false) == true

判断简写

考虑到代码风格,布尔表达式 x == true 相对冗长,简化为 x

if (x == true) 替换为 if (x) ,其中 x 是布尔类型

短路概念

布尔值可以由运算符 &&(AND)以及 ||(OR)组合运算,这会引起短路行为:

如果逻辑运算符左侧的表达式已经能确定最终结果,

那么逻辑运算符右侧的表达式不会被再次计算

逻辑运算规则

&&(与)运算规则:只要有一个表达式结果为假,则最终的结果就是假

|| (或)运算规则:只要有一个表达式结果为真,则最终的结果就是真

逻辑运算示例

下面的表达式是安全的:

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

若字符串 s 为空字符串,则 s != "" 表达式返回 false ,

则不会再判断 s[0] == 'x'

若 s 为空字符串,像 s[0] == 'x' 这样使用 s[0] ,会触发宕机异常

优先级&& 较 || 优先级更高(助记窍门:&& 表示逻辑乘法,|| 表示逻辑加法)
优先级示例

所以如下形式的条件组合无须加圆括号:

if 'a' <= c && c <= 'z' ||

   'A' <= c && c <= 'Z' ||

   '0' <= c && c <= '9' {

   // ...

}

零值布尔类型的零值(默认值)为 false

布尔类型无法与

其他类型相互转换

布尔值无法隐式转换成数值(如 true 转化为 1 ,false 转换为 0);

数值也无法隐式转换成布尔值(如 0 转换为 false ,非 0 数如 5 转换为 true)

总结:布尔型无法参与数值运算

不允许强制将数值类型转换为布尔型,不允许强制将布尔型转换为数值类型
布尔型无法与其他类型相互转换
简单解决办法

i := 0

if b {

    i = 1

}

说明:用变量 i 的值代替布尔变量 b 的值

布尔变量 b 不能参与数值运算,根据 b 的 true 还是 false 决定 i 的值为 1 还是 0

让整型变量 i 参与数值运算

专用转换函数

假如转换操作常常会用到,那就应该专门为此写个函数

// 如果 b 为真,btoi 返回 1 ;如果 b 为假,btoi 返回 0

// 布尔值转数值

func btoi(b bool) int {

    if b {

        return 1

    }

    return 0

}

// 数值转布尔值

func itob(i int) bool { return i != 0 }

5. 字符串

字符串
字符串概念

字符串是不可变的字节序列,它可以包含任意数据,包括 0 值字节,

但主要是人类可读的文本

习惯上,文本字符串被解读成按 UTF-8 编码的 Unicode 码点(文字符号)序列

字符串操作内置的 len 函数返回字符串的字节数并非文字符号的数目
下标访问操作 s[i] 则取得第 i 个字符,其中 0 \leq i \leq 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(不含边界值),即 i\leq n < 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"
字符串比较

字符串可以通过比较运算符,来比较两个字符串,如 " == " 和 " < " ;

比较运算按字节进行比较,结果服从本身的字典排序

字符串

不可改变

尽管肯定可以将新值赋予字符串变量,但是字符串值无法改变:

字符串值本身所包含的字节序列永远不会变(参考 python 字符串性质)

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

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

字符串

追加

要在一个字符串后面添加另一个字符串,可以像下面这样编写代码:

s  :=  "left foot"

t  :=  s

s  +=  ",right foot"

上面这样的操作,不会改变 s 原有的字符串值,只是将 " += " 语句生成的新字符串

赋值给 s ;同时,t 仍然持有旧的字符串值

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

fmt.Println(t)  // " left foot "

共用

底层内存

不可改变意味着两个字符串能安全地共用同一段底层内存,使得复制任何长度字符串

的开销都低廉;类似地,字符串 s 及其子串(如 s[7:] )可以安全地共用数据,因此,

子串生成操作的开销也低廉;这两种情况(复制字符串 、提取子串)都没有分配内存

3.5.1 字符串字面量

字符串

字面量

字符串的值,可以直接写成字符串字面量(string literal),形式上就是:

带双引号的字节序列

示例"Hello,世界"
任意字符

因为 Go 的源文件总是按 UTF-8 编码,并且习惯上 Go 的字符串会按 UTF-8 解读,

所以在源码中,可以将 Unicode 码点写入字符串字面量

var s string = "我们のworld"

转义字符

在带双引号的字符串字面量中,转义序列以反斜杠( \ )开始,可以将任意值的字节

插入字符串中;

下面是一组转义符,表示 ASCII 控制码,如换行符 、回车符 、制表符

\a   "警告" 或响铃

\b   退格符

\f    换页符

\n    换行符(直接跳到下一行的同一位置)

\r    回车符(返回行首)

\t    制表符

\v   垂直制表符

\'    单引号(仅用于文字字符字面量 ' \' ')

\"   双引号(仅用于 "..." 字面量内部)

\\    反斜杠

其他进制

数字字节

源码中的字符串也可以包含十六进制或八进制的任意字节

十六进制的转义字符写成  ' \xhh ' 的形式,' h ' 是十六进制数字(大小写皆可以),

且必须是两位

八进制的转义字符写成 ' \ooo ' 的形式,必须使用三位八进制数字(0 ~ 7),

且不能超过 '\377 '

这两者都表示单个字节,内容是给定值
原生字符串

原生的字符串字面量的书写形式是 `...` ,使用反引号而不是双引号;

原生的字符串字面量中,转义序列不起作用

(i).  也就是说,实质内容与字面写法严格一致,包括反斜杠和换行符

(ii). 因此,在程序源码中,原生的字符串字面量可以展开多行

(iii). 唯一的特殊处理是回车符会被删除(换行符会保留),使得同一字符串在所有

       平台上的值都相同,包括习惯在文本文件存入换行符的系统

使用场景

正则表达式往往含有大量反斜杠,可以方便地写成原生的字符串字面量;

原生的字符串字面量也适用于 HTML 模板 、JSON 字面量 、命令行提示信息 ,

以及需要多行文本表达的场景

多行文本

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

Usage:

         go command [arguments]

...`

3.5.2 Unicode
背景与问题

此前,软件只须处理一个字符集:ASCII(美国信息交换标准码)

ASCII( 或更确切地说,US-ASCII )码使用 7 位来表示 128 个 "字符" :

大小写英文字母 、数字 、各种标点 、设备控制符

这对早期的计算机行业已经足够了,但是让世界上众多使用其他语言的人无法在计算机

上使用自己的文字书写体系;随着互联网的兴起,包含纷繁语言的数据屡见不鲜;

到底怎样才能应付语言的繁杂多样,还能兼顾效率?

解决方法

新问题

答案是 Unicode(unicode.org),它囊括了世界上所有文字书写体系的全部字符,还有重音符和其他变音符,控制码(如制表符和回车符),以及许多特有文字,对它们各自赋予一个叫 Unicode 码点的标准数字;

在 Go 的术语中,这些字符记号称为文字符号( rune )

Unicode 第 8 版定义了超过一百种语言文字的 12 万个字符的码点;

它们在计算机程序和数据中如何表示?天然适合保存单个文字符号的数据类型就是 int32 ,被 Go 所采用;正因如此,runne 类型作为了 int32 类型的别名

我们可以将文字符号的序列表示成 int32 值序列,这种表示方式称作 UTF-32 或

UCS-4 ,每个 Unicode 码点的编码长度相同,都是 32 位;这种编码简单划一,

可能因为大多数面向计算机的可读文本是 ASCII 码,每个字符只需 8 位,也就是 1 字节,导致了不必要的存储空间的消耗;

而广泛使用的字符数目也少于 65536 个,字符用 16 位就能容纳,我们能做改进吗?

3.5.3 UTF-8

UTF-8

编码

UTF-8 以字节为单位对 Unicode 码点作变长编码

UTF-8 是现行的一种 Unicode 标准,

由 Go 的两位创建者 Ken Thompson 和 Rob Pike 发明

每个文字符号用 1 ~ 4 个字节表示,ASCII 字符的编码仅占 1 个字节,

而其他常用的文字字符的编码只占 2 或 3 个字节

一个文字符号编码的首字母的高位,指明了后面还有多少字节;

(i).  若最高位为 0 ,则标示着它是 7 位的 ASCII 码,其文字符号的编码仅占 1 字节,这样就与传统的 ASCII 码一致

(ii). 若最高几位是 110 ,则文字符号的编码占用 2 个字节,第二个字节以 10 开始;

      更长的编码以此类推

0xxxxxxx                                                   文字符号 0 ~ 127(ASCII)

110xxxxx 10xxxxxx                                   128 ~ 2047            少于 128 个未使用的值

1110xxxx 10xxxxxx 10xxxxxx                   2048 ~ 65535      少于 2048 个未使用的值

11110xxx 10xxxxxx 10xxxxxx 10xxxxxx   65536 ~ 0x10ffff   其他未使用的值

缺点变长编码的字符串无法按下标直接访问第 n 个字符
优点

然而有失有得,UTF-8 换来许多有用的特性:

(i).  UTF-8 编码紧凑,兼容 ASCII 

(ii). 自同步 :最多追溯 3 个字节,就能定位一个字符的起始位置

(iii). 前缀编码,能从左向右编码而不产生歧义,也无须超前预读;

       于是查找文字符号仅需搜索它自身的字节,不必考虑前文内容

(iv). 文字符号的字典字节顺序与 Unicode 码点顺序一致(Unicode 设计如此),

       因此按 UTF-8 编码排序自然就是对文字符号排序

(v). UTF-8 编码本身不会嵌入 NUL 字节( 0 值 ),这便于某些程序语言用 NUL 

      标记字符串结尾

Go 对 UTF-8

的支持

Go 的源文件总是以 UTF-8 编码,同时,需要用 Go 程序操作的文本字符串也优先采用

UTF-8 编码;

unicode 包具备针对单个文字符号的函数(例如区分字母和数字,转换大小写),

而 unicode/utf8 包,则提供了按 UTF-8 编码和解码文字符号的函数

用码点表示

Unicode字符

许多 Unicode 字符难以直接从键盘输入;有的看起来十分相似几乎无法辨认;

有些甚至不可见

在 Go 语言中,字符串字面量的转义让我们得以用码点的值来指明 Unicode 字符;

有两种形式 :

(i).  \uhhhh 表示 16 位码点值

(ii). \uhhhhhhhh 表示 32 位码点值

其中,每个 h 代表一个十六进制数字;32 位形式的码点值几乎不需要用到

这两种形式都以 UTF-8 编码表示出给定的码点

示例

因此,下面几个字符串字面量都表示长度为 6 个字节的相同串:

"世界"

"\xe4\xb7\x96\xe7\x95\x8c"

"\u4e16\u754c"

"\U00004e16\U0000754c"

后面三行的转义序列用不同形式表示第一行的字符串,但实质上它们的字符串值都一样

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

'世'     '\u4e16'      '\U00004e16'

特别注意

码点值小于 256 的文字符号可以写成单个十六进制数转义的形式,如 'A' 写成 '\x41' ,

而更高的码点值则必须使用 \u 或 \U 转义

这就导致,'\xe4\xb8\x96' 不是合法的文字符号,虽然这三个字节构成某个有效的

UTF-8 编码码点

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(suffix) && 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 函数的具体实现,使用了散列方法,让搜索更高效)

另一方面,如果我们真的要逐个逐个处理 Unicode 字符,则必须使用其他编码机制;

考虑到我们第一个例子的字符串 "世界" ,该字符串包含两个中文字符;

图 3-5 说明了该字符串的内存布局;

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

import  "unicode/utf8"

s := "Hello,世界"

fmt.Println(len(s))                                   // "13"

fmt.Println(utf8.RuneCountInString(s))  // "9"

我们需要 UTF-8 解码器来处理这些字符,unicode/utf8 包就具备一个:

for  i := 0;i < len(s);{

    r,size := utf8.DecodeEuneInString(s[i:])

    fmt.Printf("%d\t%c\n",i,r)

    i += size

}

说明:

每次 DecodeRuneInString 的调用都返回 r(文字符号本身)

和一个值 offset(表示 r 按 UTF-8 编码所占用的字节数)

offset 这个值用来更新下标 i ,定位字符串内下一个文字符号;

可是按此方法,我们总是需要使用上例中的循环方式;

所幸,Go 的 range 循环也适用于字符串,按 UTF-8 隐式解码;

图 3-5 也展示了以下循环的输出;注意,对于非 ASCII 文字符号,下标增量大于 1

for  i,r  :=  range  "Hello,世界"  {

    fmt.Printf("%d\t%q\t%d\n",i,r,r)

}

// 我们可以用简单的 range 循环统计字符串中的文字符号数目,如下所示

n  :=  0

for _,_   =  range  s  {

    n++

}

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

n  :=  0

for  range  s  {

    n++

}

// 或者,直截了当地调用 utf8.RuneCountInString(s)

之前提到过,文本字符串作为按 UTF-8 编码的 Unicode 码点序列解读,很大程度上是出于习惯,但为了确保使用 range 循环能正确处理字符串,则必须要求而不仅仅是按照习惯;如果字符串含有任意二进制数,也就是说,UTF-8 数据出错,而我们对它做 range 循环,会发生什么?

每次 UTF-8 解码器读入一个不合理的字节,无论是显式调用 utf8.DecodeRuneInString ,还是在 range 循环内隐式读取,都会产生一个专门的 Unicode 字符 '\uFFFD' 替换它,其输出通常是个黑色六角形或类似钻石的形状,里面有个白色问号;

如果程序碰到这个文字符号值,通常意味着,生成字符串数据的系统上游部分在处理文本编码方面存在瑕疵

UTF-8 是一种分外便捷的交互格式,而在程序内部使用文字字符类型可能更加方便,因为它们大小一致,便于在数组和 slice 中用下标访问
当 []rune 转换作用于 UTF-8 编码的字符串时,返回该字符串的 Unicode 码点序列:

// 日语片假名 "程序"

s  :=  "プログラム"

fmt.Printf("% x\n",s)  // "e3 83 97 e3 83 ad e3 82 b0 e3 83 a9 e3 83 a0"

r  :=  []rune(s)

fmt.Printf("%x\n",r)  //  "[30d7  30ed  30b0  30e9  30e0]"

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

如果把文字符号类型的 slice 转换成一个字符串,它会输出各个文字符号的 UTF-8 编码拼接结果:

fmt.Println(string(r))  "プログラム"

若将一个整数值转换成字符串,其值按文字符号类型解读,并且产生代表该文字符号值的 UTF-8 码:

fmt.Println(string(65))           // "A",而不是 "65"

fmt.Println(string(0x4eac))    // "京"

如果文字符号值非法,将被专门的替换字符取代(见前面的 '\uFFFD')

fmt.Println(string(1234567))    // "�"

3.5.4 字符串和字节 slice

4 个标准

字符串包

4 个标准包对字符串操作特别重要:strings 、bytes 、strconv 、unicode
strings 包strings 包提供了许多函数,用于搜索 、替换 、比较 、修整 、切分 、连接字符串
bytes 包bytes 包也有 strings 包中类似的函数,用于操作字节 slice( []byte 类型,其某些属性和字符串相同 );由于字符串不可变,因此按增量方式构建字符串会导致多次内存分配和复制;这种情况下,使用 bytes.Buffer 类型会更高效
strconv 包strconv 包具备的函数,主要用于转换布尔值 、整数 、浮点数 为与之对应的字符串形式,或者把字符串转换为 布尔值 、整数 、浮点数,另外还有为字符串添加 / 去除引号的函数
unicode 包

unicode 包备有判别文字符号值特性的函数,如 IsDigit、IsLetter、IsUpper 、IsLower 

每个函数以单个文字符号值作为参数,并返回布尔值;

若文字符号值是英文字母,转换函数(如 ToUpper 和 ToLower)将其转换成指定的大小写;

上面所有函数都遵循 Unicode 标准,对字母数字等的分类原则;

strings 包也有类似的函数,函数名也是 ToUpper 和 ToLower ,它们对原字符串的每个字符做指定变换,生成并返回一个新字符串

下例中,basename 函数模仿 UNIX shell 中的同名实用程序;只要 s 的前缀看起来像是文件系统路径(各部分由斜杠分隔),该版本的 basename(s) 就将其移除,貌似文件类型的后缀也被移除: 

fmt.Println(basename("a/b/c.go"))  // "c"

fmt.Println(basename("c.d.go"))     // "c.d"

fmt.Println(basename("abc"))         // "abc"

初版的 basename 独自完成全部工作,并不依赖任何库:

// gop.io/ch3/basename1

// basename 移除路径部分和 . 后缀

// e.g. ,a => a ,a.go => a , a/b/c.go => c ,a/b.c.go => b.c

func basename( s string ) string {

    // 

}

6. 常量

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

常量的声明定义了具名的值(命名对象,其值一旦确定,就不可修改),语法上与变量类似,防止了程序运行过程中的意外(或恶意)修改

例如,姚表示数学常量,像圆周率,在 Go 程序中用常量表示比变量更适合,因为值是恒定不变的

常量声明

声明单个常量

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

声明一组常量

与变量类似,同一个声明可以定义一系列常量,适用于一组相关的值;

const (

    e = 2.718281828

    pi = 3.1415926

)

为什么使用常量?

许多针对常量的计算完全可以在编译阶段完成,从而减少运行时的工作量,并让其他编译器优化得以实现;

某些错误通常要在运行时才能检测到,但如果操作数是常量,编译时就会报错,例如整数除以 0 ,字符串下标越界,以及任何产生无限大值的浮点数运算

1. 对于常量操作数,所有数学运算 、逻辑运算 、比较运算的结果依旧是常量;

2. 常量的类型转换结果还是常量;

3. 某些内置函数的返回值是常量,如 len 、cap 、real 、imag 、complex 、unsafe.Sizeof 等

因为编译器知道常量(表达式)的值,所以常量表达式可以出现在涉及类型的声明中,主要就是数组类型的长度

const IPv4Len = 4

// parseIPv4 函数解析一个 IPv4 地址(d.d.d.d)

func parseIPv4(s string) IP {

    var p [IPv4Len]byte

    // ...

}

常量声明可以同时指定类型和值,如果没有显式指定类型,则类型根据右边的表达式推断(有点类似使用 var 关键字声明变量)

var a int = 5

var b int

var c = 10

fmt.Println(b)    // " 0 "

fmt.Printf("%T\n", c)  // " int "

下面的例子中,time.Duration 是一种具名类型,其基本类型是 int64 ,time.Minute 也是基于 int64 的常量;下面声明的两个常量都属于 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 10m0s"

若同时声明一组常量,除了第一项之外,其他项在等号右侧的表达式都可以省略;

这意味着,会复用前一项的表达式及其类型

例如:

const (

    a = 1

    b

    c = 3.14

    d

)

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

如果复用右侧表达式导致计算结果总是相同,这就不太实用;假若该结果可变,该怎么办呢?来看看 iota

常量生成器 iota
iota

常量的声明可以使用常量生成器 iota ,它创建一系列相关值,而不是逐个值显式写出;

常量声明中,iota 从 0 开始取值,逐项加 1

iota 的理解

1. iota 遇到 const ,就会被重置为 0

2. 每出现一个新的常量项,iota 的值都自动加 1

示例

下面的例子来自 time 包,它定义了 Weekday 的具名类型,并声明每周的 7 天为该类型的常量,从 Sunday 开始,其值为 0

这种类型通常称为 "枚举型(enumeration ,或缩写为 enum)"

type  Weekday  int

const (

    Sunday Weekday = iota  // Sunday为常量项,Weekday为类型,iota 是初始值,为 0

    Monday                           // iota = 1

    Tuesday                          // iota = 2

    Wednesday                     // iota = 3

    Thurday                           // iota = 4

    Friday                              // iota = 5

    Saturday                          // iota = 6

)

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

type  Flags  uint

const (

    FlagUp Flags = 1 << iota    // 向上

    FlagBroadcast                    // 支持广播访问

    FlagLoopback                     // 环回接口

    FlagPointToPoint                // 点对点链路

    FlagMulticast                      // 支持多路广播访问

)

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

func IsUp(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 的幂

const (

    _ = 1 << ( 10 * iota )

    KiB     // 1024

    MiB    //  1048576

    GiB    //   1073741824

    TiB     //   1099511627776

    PiB     //   1125899906842624

    EiB     //   1152921504606846976

    ZiB     //   1180591620717411303424

    YiB     //   12089258196146291774706176

)

然而,iota 机制也存在局限;

比如,因为不存在指数运算符,所以无法生成更为人熟知的 1000 的幂(KB 、MB 等)

总结

(特别重要)

1. 同时声明一组常量,除了第一个常量项之外,后续常量项在等号右侧的表达式都可以省略;这意味着,会复用前一项的表达式及其类型

2. iota 遇到 const ,就会被重置为 0

3. 每出现一个新的常量项,iota 的值都自动加 1

无类型常量

  • 22
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值