Go编程基础-3. 基本数据类型
Go的数据类型分为四大类:基础类型(basic type)、聚合类型(aggregate type)、引用类型(reference type)和接口类型(interface type)。
基本数据类型
math包的MaxXXX变量给出了一些基本数据类型的上下界,在一些场景下可以用于溢出判断和处理。
1. 整数
Go 同时支持有符号整数和无符号整数:
- 有符号整数分为四种大小:8位、16位、32位、64位,分别用 int8、int16、int32、int64 表示;相应的无符号整数则分别为 uint8、uint16、uint32、uint64;
- 此外还有两种类型int和uint,其大小和平台原生的有/无符号整数相同,一般为32/64位,具体根据平台和编译器设置而变化; 注意,尽管int可能和int64/int32大小一致,但是两者之间无法直接。
特殊的整数类型/别名:
- 无符号整型uintptr, 大小并不固定,但注意完整存放指针,仅适用于底层编程;
- 有符号整型rune(int32的别名),常用来表示一个Unicode码点(code point),用于表示和输出一个中文字符 - string类型的底层是[]byte, 两者之间可以相互转换,默认输出时会以byte为单位输出ASCII码字符;如果想要表示和输出中文字符,需要先将其转化为[]rune,然后再进行输出。
特殊的运算符:
- &^ (bit clear) :如果运算符右侧数值的第 i 位为 1,那么计算结果中的第 i 位为 0;如果运算符右侧数值的第 i 位为 0,那么计算结果中的第 i 位为运算符左侧数值的第 i 位的值。
2. 浮点数
Go有两种大小的浮点数float32和float64, 前者有效数字约6位,后者15位。因此,绝大多少情况下我们都应该使用float64,除非特别小心,否则误差会在非常短的时间内快速累积起来。
var f float32 = 16777216
fmt.Println(f == f+1) // true
浮点数可以通过小数或者科学计数法的形式表示:
const e = 2.71828 // 注意,对于小数而言,浮点数只能近似保存而非精确记录
const f = 6.02e23 // 大小E均可
cosnt g = 6.02e-34
浮点数的输出有三种方式
- %g自动保持足够的精度,并选择简洁的表示方式;
- %e 有指数 %f 无指数, 手动控制输出的宽度和精度, 更加适合一些表格形式的输出,例如: %8.3f 宽度为8个字符且保留3位小数 - 宽度不够时会自动在前面补空格
除了大量常见的数学函数之外,math数学包还有函数用于创建和判断 IEEE 754 标准定义的特殊值:正无穷大和负无穷大。它们表示超出最大允许值的数以及除以零的商 (不会报错,而是表示为+Inf或-Inf)。还有NaN(Not a Number),它表示数学上无意义的运算结果,例如0/0或Sqrt(-i)(-i的平方根)。
var z float64
fmt.Println(z, -z, -z, 1/z, 1/z - 1/z, -1/z, z/z) // "0 -0 +Inf -Inf -Inf NaN"
在数字运算中,我们倾向于将NaN看作信号值(sentinel value)。但直接判断具体的计算结果是否为NaN可能导致潜在错误,因为与NaN的比较总是不成立。为了判断一个值是否是NaN,可以使用math.IsNaN
函数。该函数判断其参数是否是非数值,而math.NaN
函数则返回非数值(NaN)。
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² = -1
根据常量运算规则,复数常量可以和其他常量相加(整型或浮点型,实数和虚数皆可),这让我们可以自然地写出复数,如 1+2i
,或等价地,2i+1
。前面 x
和 y
的声明可以简写为:
x := 1 + 2i
y := 3 + 4i
可以用 ==
或 !=
判断复数是否等值。若两个复数的实部和虚部都相等,则它们相等。math/cmplx
包提供了复数运算所需的库函数,例如复数的平方根函数和复数的幂函数:
fmt.Println(cmplx.Sqrt(-1)) // "(0+1i)"
4. 布尔
Go语言中的布尔值无法隐式转化为数值0/1,如果有必要可以写一个转换函数:
func btoi(b bool) int {
if b {
return 1
}
return 0
}
反之亦然。
5. 字符串
字符串是不可变的字节序列,内置的len函数返回字符串的字节数(并非实际字符数),下标访问s[i]取得第i(0<=i<len(s))
个字节。
### 常见字符操作
- 可以通过切片的形式,对字符串进行截取-生成新的字符串。注意:这种方式生成的字符串和原字符串共用内存空间。
s[0:5]
- 可以通过+号运算符连接两个字符串从而生成一个新的字符串,注意每次通过+号进行字符串拼接都会分配一片新的空间
- 字符串可以通过比较运算符进行比较,例如 == 和 <, 比较按照字节进行,结果服从本身的字典排序
不可变性意味着两个字符串能够安全地共用同一段底层内存,使得复制任何长度字符串的开销都较低廉。类似地,字符串S及其子串(如s[7:])可以安全地共用数据,因此子串生成操作的开销也较低廉。
字符串字面量
- 字符串的值可以直接写成字符串字面量(string(字符串literal), 字面的)形式,就是带双引号的字节序列:
"Hello, 你好。世界"
因为 Go 的源文件总是按 UTF-8 编码,并且习惯上 Go 的字符串会按 UTF-8 解读,所以在源码中我们可以将 Unicode 码点写入字符串字面量。 - 在带双引号的字符串字面量中,转义序列以反斜杠(\)开始,可以将任意值的字节插入字符串中。下面是一组转义符,表示 ASCII 控制码,如换行符、回车符和制表符:
- `\a` 警告或响铃
- `\b` 退格符
- `\f` 换页符
- `\n` 换行符(指直接跳到下一行的同一位置)
- `\r` 回车符(指返回行首)
- `\t` 制表符
- `\v` 垂直制表符
- `\'` 单引号(仅用于文本字符字面量)
- `\"` 双引号(仅用于 "... " 字面量内部)
- `\\` 反斜杠
- 源码中的字符串也可以包含十六进制或八进制的任意字节。
- 原生的字符串字面量的书写形式是使用反引号而不是双引号。原生的字符串字面量内,转义序列不起作用;实质内容与字面写法严格一致,包括反斜杠和换行符,因此,在程序源码中,原生的字符串字面量可以展开多行。唯一的特殊处理是回车符会被删除(换行符会保留),使得同一字符串在所有平台上的值都相同,包括习惯在文本文件存入换行符的系统。
- 正则表达式往往含有大量反斜杠,可以方便地写成原生的字符串字面量。原生的字面量也适用于 HTML 模板、JSON 字面量、命令行提示信息,以及需要多行文本表达的场景。
Unicode && UTF-8
- UTF-8 是一种现行的 Unicode 标准,由 Go 语言的两位创建者 Ken Thompson(肯·汤普森)和 Rob Pike(罗伯·梭鱼)发明。每个文字符号用 1 到 4 个字节表示,其中 ASCII 字符的编码仅占 1 个字节,而其他常用的文字符号的编码则通常是 2 或 3 个字节。一个文字符号编码的首字节的高位指明了后面还有多少字节。如果最高位为 0,则表示它是 7 位的 ASCII 码,其文字符号的编码仅占 1 字节,与传统的 ASCII 码一致。如果最高几位是 110,则文字符号的编码占用 2 个字节,第二个字节以 1 开始。更长的编码以此类推。
- 变长编码的字符串无法通过标准直接访问第 N 个字符,然而有失有得,UTF-8 带来了许多有用的特性。UTF-8 编码紧凑,兼容 ASCII,并且具有自同步性:最多追溯 3 字节,就能定位一个字符的起始位置。
- Go的源文件总是以 UTF-8 编码。同时,需要用 Go 程序操作的文本字符串也优先采用 UTF-8 编码。Unicode 包具备针对单个文字符号的函数,例如区分字母和数字、转换大小写。而 Unicode/UTF-8 包则提供了按 UTF-8 编码和解码文字符号的函数。
- 在G语言中,通过字符串字面量的转义,我们能够使用码点的值来表示Unicode字符。有两种形式,\u表示16位码点值,而\U表示32位码点值,其中每个人代表一个十六进制数字;32位形式的码点值几乎不需要使用。这两种形式都以UTF-8编码表表示给定的码点。因此,下面几个字符串字面量都表示长度为6字节的相同串:
字符串操作示例1:
- 基本字符串操作
// 前缀
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字符串操作
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
s := "hello, 世界"
// 1. 统计字节和字符数量
fmt.Println(len(s))
fmt.Println(utf8.RuneCountInString(s))
// 2. 使用UTF8解码,获取每一个字符
for i := 0; i < len(s); {
// 自动识别下一个UTF8字符,返回其rune形式及其所占字节大小,%c输出单个字符
r, size := utf8.DecodeRuneInString(s[i:])
fmt.Printf("%d\t%c\n", i, r)
i += size
}
// 3. []byte转化为[]rune,会自动进行utf8解码处理,然后逐个字符进行操作
sRune := []rune(s)
for i := 0; i < len(sRune); i++ {
fmt.Printf("%c\n", sRune[i])
}
}
字符串相关标准包
4个标准包在字符串操作中特别重要:bytes|strings|strconv|unicode
- strings包提供了许多函数,用于搜索、替换、比较、修整、切分与连接字符串。
- bytes包也拥有类似的函数,用于操作字节切片([]byte类型,某些属性与字符串相同)。由于字符串是不可变的,因此按增量方式构建字符串会导致多次内存分配和复制。在这种情况下,使用bytes.Buffer类型会更高效。
- strconv包提供的函数主要用于将布尔值、整数、浮点数转换为相应的字符串形式,或者将字符串转换为布尔值、整数、浮点数。此外,还有一些函数用于为字符串添加/去除引号。
- unicode包提供了用于判别字符特性的函数,如IsUpper和IsLower。每个函数以单个字符值作为参数,并返回布尔值。若字符值是英文字母,转换函数(如ToUpper和ToLower)将其转换成指定的大写或小写形式。所有这些函数都遵循Unicode标准对字母、数字等的分类原则。strings包也提供了类似的函数,函数名为ToUpper和ToLower,它们对原字符串的每个字符进行指定的变换,生成并返回一个新字符串。
基于标准包的字符串操作示例
- 从路径中提取文件名(无后缀版本):
package main
import (
"fmt"
"strings"
)
func basename(s string) string {
// 从路径中截取文件名
slash := strings.LastIndex(s, "/") // 如果没有找到对应子串,会返回-1
s = s[slash+1:]
// 如果文件名带有后缀,删去
if dot := strings.LastIndex(s, "."); dot >= 0 {
s = s[:dot]
}
return s
}
func main() {
fmt.Println(basename("a/b/c.go"))
}
2.接受一个表示整数的字符串,如"12345", 从右侧开始每三位数字后面就插入一个逗号:
func comma(s string) string {
n := len(s)
if n < 3 {
return n
}
return comma(s[:n-3]) + "," + s[n-3:]
}
- 这是一种比较直观的写法,通过递归的方式每次往倒数第三位前加上一个逗号,但是每次+号都会重新开辟一篇空间用来存储整个字符串,这个方法的空间复杂度会比较高.
- 改进思路:(1)先转化为[]byte,字符串数组的操作不会重新开辟一整片空间,只会少量扩容 (2)使用bytes.Buffer类型。下为使用Buffer进行字符串操作的案例,注意该方法和上述方法并不等价:
package main
import (
"bytes"
"fmt"
)
func intsToString(values []int) string {
var buf bytes.Buffer
// 1. 字符串格式转换
buf.WriteByte('[')
for i, v := range values {
if i > 0 {
buf.WriteString(", ")
}
fmt.Fprintf(&buf, "%d", v)
}
buf.WriteByte(']')
// 2. 返回结果
return buf.String()
}
func main() {
s := intsToString([]int{1, 2, 3})
fmt.Println(s)
}
字符串和其它类型之间的相互转换(int为例)
整数转为字符串:
res = fmt.Sprintf("%d", x )
// %d十进制,%b二进制,%o八进制,%x十六进制res = strconv.Itoa(x)
// 整数转为ASCII表示,即为字符串res = strconv.FormatInt(int64(x), 2)
// 将整数转为对应进制的字符串,同理有FormatUint
字符串转为战术:
res, err := strconv.Atoi("123")
res, err := strconv.ParseInt("123", 10, 64)
// 十进制,最长64位
6. 常量
常量中需要注意的只有itoa(常量生成器)。它可以用来创建一系列相关值,而不用逐个写出来,在枚举类型的定义中非常有用。(默认从0开始,逐个加1)
// 以一周七天为例
type Weekday int
const (
Sunday Weekday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)
在第一项使用itoa后,后面的常量就会 1.遵循第一项的类型, 2.在第一项的数值上++, 来自动生成后续的枚举常量。当然一些更加复杂的数值定义中也可以使用itoa:
type Flags uint
const (
FlagUp Flags = 1 << itoa
FlagBroadcast
FlagLoopback
FlagPointToPoint
FlagMulticast
)
随着iota递增,每个常量都按照1 << iota进行赋值,等价于2的连续次幂.
无类型常量
Go的常量既可以是任何基本数据类型,也可以不从属于任意具体类型,而只是表示为字面量的形式。这些值的精度比基本数据类型更高,且算数精度高于原生机器精度,可以认为精度至少达到了256位。
- 从属类型待定的常量共有6种,分别是无类型布尔、无类型整数、无类型文字符号、无类型浮点数、无类型复数、无类型字符串。
- 字面量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的结果是无类型浮点数
- 通过推迟确定从属类型,1. 无类型常量能够暂时维持更高的精度,用来存储Pi/ZiB/YiB等常量 2.与已确定类型的常量相比,它们还能够更自由地嵌入更多表达式,而无需进行类型转换。
const (
_ = 1 << (10 * iota)
KiB
MiB
GiB
TiB
PiB
EiB
ZiB // 1180591620717411303424 (超过 1 << 64, 无法保存在任意整数类型中,但是可以保存在无类型常量中)
YiB
)
const Pi32 flaot32 = math.Pi
const Pi64 float64 = math.Pi
const PiC128 complex128 = math.Pi
但是需要注意,无论隐式还是显式,常量从一种类型/或无类型转为另外一种类型的过程中,要求目标类型必须能够表示原值。例如无法将ZiB赋值给int, 但是可以赋值给float64.