Go编程基础-3. 基本数据类型

52

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大小一致,但是两者之间无法直接。

特殊的整数类型/别名:

  1. 无符号整型uintptr, 大小并不固定,但注意完整存放指针,仅适用于底层编程;
  2. 有符号整型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

浮点数的输出有三种方式

  1. %g自动保持足够的精度,并选择简洁的表示方式;
  2. %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以下是修改后的文本:

具备两种大小的复数 complex64complex128,二者分别由 float32float64 构成。内置的 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.141592i2i,它就变成一个虚数,表示一个实部为 0 的复数:

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

根据常量运算规则,复数常量可以和其他常量相加(整型或浮点型,实数和虚数皆可),这让我们可以自然地写出复数,如 1+2i,或等价地,2i+1。前面 xy 的声明可以简写为:

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))个字节。
### 常见字符操作

  1. 可以通过切片的形式,对字符串进行截取-生成新的字符串。注意:这种方式生成的字符串和原字符串共用内存空间。 s[0:5]
  2. 可以通过+号运算符连接两个字符串从而生成一个新的字符串,注意每次通过+号进行字符串拼接都会分配一片新的空间
  3. 字符串可以通过比较运算符进行比较,例如 == 和 <, 比较按照字节进行,结果服从本身的字典排序
    不可变性意味着两个字符串能够安全地共用同一段底层内存,使得复制任何长度字符串的开销都较低廉。类似地,字符串S及其子串(如s[7:])可以安全地共用数据,因此子串生成操作的开销也较低廉。
    字符串底层共用

字符串字面量

  1. 字符串的值可以直接写成字符串字面量(string(字符串literal), 字面的)形式,就是带双引号的字节序列:"Hello, 你好。世界"因为 Go 的源文件总是按 UTF-8 编码,并且习惯上 Go 的字符串会按 UTF-8 解读,所以在源码中我们可以将 Unicode 码点写入字符串字面量。
  2. 在带双引号的字符串字面量中,转义序列以反斜杠(\)开始,可以将任意值的字节插入字符串中。下面是一组转义符,表示 ASCII 控制码,如换行符、回车符和制表符:
- `\a` 警告或响铃
- `\b` 退格符
- `\f` 换页符
- `\n` 换行符(指直接跳到下一行的同一位置)
- `\r` 回车符(指返回行首)
- `\t` 制表符
- `\v` 垂直制表符
- `\'` 单引号(仅用于文本字符字面量)
- `\"` 双引号(仅用于 "... " 字面量内部)
- `\\` 反斜杠
  1. 源码中的字符串也可以包含十六进制或八进制的任意字节。
  2. 原生的字符串字面量的书写形式是使用反引号而不是双引号。原生的字符串字面量内,转义序列不起作用;实质内容与字面写法严格一致,包括反斜杠和换行符,因此,在程序源码中,原生的字符串字面量可以展开多行。唯一的特殊处理是回车符会被删除(换行符会保留),使得同一字符串在所有平台上的值都相同,包括习惯在文本文件存入换行符的系统。
  • 正则表达式往往含有大量反斜杠,可以方便地写成原生的字符串字面量。原生的字面量也适用于 HTML 模板、JSON 字面量、命令行提示信息,以及需要多行文本表达的场景。
    原生字符串字面量

Unicode && UTF-8

  1. UTF-8 是一种现行的 Unicode 标准,由 Go 语言的两位创建者 Ken Thompson(肯·汤普森)和 Rob Pike(罗伯·梭鱼)发明。每个文字符号用 1 到 4 个字节表示,其中 ASCII 字符的编码仅占 1 个字节,而其他常用的文字符号的编码则通常是 2 或 3 个字节。一个文字符号编码的首字节的高位指明了后面还有多少字节。如果最高位为 0,则表示它是 7 位的 ASCII 码,其文字符号的编码仅占 1 字节,与传统的 ASCII 码一致。如果最高几位是 110,则文字符号的编码占用 2 个字节,第二个字节以 1 开始。更长的编码以此类推。
    Alt text
  2. 变长编码的字符串无法通过标准直接访问第 N 个字符,然而有失有得,UTF-8 带来了许多有用的特性。UTF-8 编码紧凑,兼容 ASCII,并且具有自同步性:最多追溯 3 字节,就能定位一个字符的起始位置。
  3. Go的源文件总是以 UTF-8 编码。同时,需要用 Go 程序操作的文本字符串也优先采用 UTF-8 编码。Unicode 包具备针对单个文字符号的函数,例如区分字母和数字、转换大小写。而 Unicode/UTF-8 包则提供了按 UTF-8 编码和解码文字符号的函数。
  4. 在G语言中,通过字符串字面量的转义,我们能够使用码点的值来表示Unicode字符。有两种形式,\u表示16位码点值,而\U表示32位码点值,其中每个人代表一个十六进制数字;32位形式的码点值几乎不需要使用。这两种形式都以UTF-8编码表表示给定的码点。因此,下面几个字符串字面量都表示长度为6字节的相同串:Unicode表示

字符串操作示例1:

  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
}
  1. 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

  1. strings包提供了许多函数,用于搜索、替换、比较、修整、切分与连接字符串。
  2. bytes包也拥有类似的函数,用于操作字节切片([]byte类型,某些属性与字符串相同)。由于字符串是不可变的,因此按增量方式构建字符串会导致多次内存分配和复制。在这种情况下,使用bytes.Buffer类型会更高效
  3. strconv包提供的函数主要用于将布尔值、整数、浮点数转换为相应的字符串形式,或者将字符串转换为布尔值、整数、浮点数。此外,还有一些函数用于为字符串添加/去除引号。
  4. unicode包提供了用于判别字符特性的函数,如IsUpper和IsLower。每个函数以单个字符值作为参数,并返回布尔值。若字符值是英文字母,转换函数(如ToUpper和ToLower)将其转换成指定的大写或小写形式。所有这些函数都遵循Unicode标准对字母、数字等的分类原则。strings包也提供了类似的函数,函数名为ToUpper和ToLower,它们对原字符串的每个字符进行指定的变换,生成并返回一个新字符串。

基于标准包的字符串操作示例

  1. 从路径中提取文件名(无后缀版本):
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. 改进思路:(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为例)

整数转为字符串:

  1. res = fmt.Sprintf("%d", x ) // %d十进制,%b二进制,%o八进制,%x十六进制
  2. res = strconv.Itoa(x) // 整数转为ASCII表示,即为字符串
  3. res = strconv.FormatInt(int64(x), 2) // 将整数转为对应进制的字符串,同理有FormatUint

字符串转为战术:

  1. res, err := strconv.Atoi("123")
  2. 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.