Go语言将数据类型分为四类:基础类型、复合类型、引用类型和接口类型。虽然数据种类很多,但它们都是对程序中一个变量或状态的间接引用。这意味着对任一引用类型数据的修改都会影响所有该引用的拷贝

基础类型

整型

Go语言同时提供了有符号和无符号类型的整数运算。这里有int8int16int32int64四种截然不同大小的有符号整形数类型,分别对应8163264bit大小的有符号整形数,与此对应的是uint8uint16uint32uint64四种无符号整形数类型
Unicode字符rune类型是和int32等价的类型,通常用于表示一个Unicode码点。这两个名称可以互换使用。同样byte也是uint8类型的等价类型,byte类型一般用于强调数值是一个原始的数据而不是一个小的整数

其中有符号整数采用2的补码形式表示,也就是最高bit位用作表示符号位,一个n-bit的有符号数的值域是从-2^n-1到2^n-1-1。无符号整数的所有bit位都用于表示非负数,值域是0到2^n-1。例如,int8类型整数的值域是从-128到127,而uint8类型整数的值域是从0到255。无符号数往往只有在位运算或其它特殊的运算场景才会使用,就像bit集合、分析二进制文件格式或者是哈希和加密操作等

尽管Go语言提供了无符号数和运算,即使数值本身不可能出现负数我们还是倾向于使用有符号的**int**类型,就像数组的长度那样,虽然使用uint无符号类型似乎是一个更合理的选择。事实上,内置的len函数返回一个有符号的int,如果len(medals)返回无符号数,那么i>=0永远成立,死循环
image.png

类型不匹配

需要一个显式的转换将一个值从一种类型转化位另一种类型,并且算术和逻辑运算的二元操作中必须是相同的类型
image.png
上面的代码会报错,

  1. invalid operation: apples + oranges (mismatched types int32 and int16)

最常见的修复方法是将它们都显式转型为一个常见类型

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

对于每种类型T,如果转换允许的话,类型转换操作T(x)x转换为T类型。许多整形数之间的相互转换并不会改变数值;它们只是告诉编译器如何解释这个值。但是对于将一个大尺寸的整数类型转为一个小尺寸的整数类型,或者是将一个浮点数转为整数,可能会改变数值或丢失精度
image.png

位操作符

位操作运算符^作为二元运算符时是按位异或(XOR),当用作一元运算符时表示按位取反;也就是说,它返回一个每个bit位都取反的数。位操作运算符&^用于按位置零(ANDNOT):表达式z=x&^y结果zbit位为0,如果对应ybit位为1的话,否则对应的bit位等于x相应的bit位的值

算术上,一个x<<n左移运算等价于乘以2^n,一个x>>n右移运算等价于除以2^n。 1111<<2111100;1111>>20011

image.png

浮点数

Go语言提供了两种精度的浮点数,float32float64。它们的算术规范由IEEE754浮点数国际标准定义
常量math.MaxFloat32表示float32能表示的最大数值,大约是3.4e38;对应的math.MaxFloat64常量大约是1.8e308。它们分别能表示的最小值近似为1.4e-45和4.9e-324
两个浮点数之间不应该使用=或!=进行比较操作,高精度科学计算应该使用**math**标准库

布尔型

一个布尔类型的值只有两种:truefalseiffor语句的条件部分都是布尔类型的值,并且==<等比较操作也会产生布尔型的值。一元操作符!对应逻辑非操作,因此!true的值为false
布尔值可以和&&(AND)||(OR)操作符结合,并且可能会有短路行为:如果运算符左边值已经可以确定整个布尔表达式的值,那么运算符右边的值将不在被求值,因此下面的表达式总是安全的

s != "" && s[0] == 'x'
// s[0]操作如果应用于空字符串将会导致panic异常

因为&&的优先级比||高,下面形式的布尔表达式是不需要加小括弧的
image.png
布尔值并不会隐式转换为数字值**0****1**,反之亦然。必须使用一个显式的if语句辅助转换

func btoi(b bool) int {
if b {
    return 1
} 
    return 0
}

字符串

一个字符串是一个不可改变的字节序列。字符串可以包含任意的数据,包括**byte**0,但是通常是用来包含人类可读的文本。文本字符串通常被解释为采用UTF8编码的Unicode码点(rune)序列
内置的len函数可以返回一个字符串中的字节数目不是**rune**字符数目),索引操作s[i]返回第i个字节的字节值,i必须满足0≤i<len(s)条件约束
image.png

字符

i个字节并不一定是字符串的第i个字符,因为对于非ASCII字符的UTF8编码会要两个或多个字节。
字符串的工作方式:

  1. 子字符串操作s[i:j]基于原始的s字符串的第i个字节开始到第j个字节(并不包含j本身)生成一个新字符串。生成的新字符串将包含j-i个字节
  2. 如果索引超出字符串范围或者j小于i的话将导致panic异常
  3. 不管i还是j都可能被忽略,当它们被忽略时将采用0作为开始位置,采用len(s)作为结束的位置
  4. +操作符将两个字符串链接构造一个新字符串
  5. 字符串可以用==<进行比较;比较通过逐个字节比较完成的,因此比较的结果是字符串自然
    编码的顺序
  6. 字符串的值是不可变的:一个字符串包含的字节序列永远不会被改变,当然我们也可以给一个字符串变量分配一个新字符串值。如将一个字符串追加到另一个字符串,这并不会导致原始的字符串值被改变,但是变量s将因为+=语句持有一个新的字符串值,但t依然是包含原先的字符串值

    s := "left foot"
    t := s
    s += ", right foot"
    fmt.Println(s) // "left foot, right foot"
    fmt.Println(t) // "left foot"
    
  7. 尝试修改字符串内部数据的操作也是被禁止的

不变性意味如果两个字符串共享相同的底层数据的话也是安全的,这使得复制任何长度的字符串代价是低廉的。同样,一个字符串s和对应的子字符串切片s[7:]的操作也可以安全地共享相同的内存,因此字符串切片操作代价也是低廉的。在这两种情况下都没有必要分配新的内存

unicode

Unicodehttp://unicode.org),它收集了这个世界上所有的符号系统,包括重音符号和其它变音符号,制表符和回车符,还有很多神秘的符号,**每个符号都分配一个唯一的**`**Unicode**`**码点**,`Unicode`码点对应`Go`语言中的`rune`整数类型

UTF-8

UTF8是一个将Unicode码点编码为字节序列的变长编码。(unicode是字符集,UTF-8unicode字符集的实现)
UTF8编码使用1-4个字节来表示每个Unicode码点,ASCII部分字符只使用1个字节,常用字符部分使用23个字节表示。每个符号编码后第一个字节的高端bit位用于表示总共有多少编码个字节。如果第一个字节的高端bit0,则表示对应7bitASCII字符,ASCII字符每个字符依然是一个字节,和传统的ASCII编码兼容。如果第一个字节的高端bit110,则说明需要2个字节;后续的每个高端bit都以10开头
编码规则如下,

  1. 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。
  2. 对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。

image.png
举例:
已知“严”的unicode4E25100111000100101),根据上表,可以发现4E25处在第三行的范围内(00000800-0000FFFF),因此“严”的UTF-8编码需要三个字节,即格式是“1110xxxx10xxxxxx10xxxxxx”。然后,从“严”的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,“严”的UTF-8编码是“111001001011100010100101”,转换成十六进制就E4B8A5
优点:

  1. UTF8编码比较紧凑,完全兼容ASCII码,并且可以自动同步:它可以通过向前回朔最多2个字节就能确定当前字符编码的开始字节的位置。它也是一个前缀编码,所以当从左向右解码时不会有任何歧义也并不需要向前查看
  2. 没有任何字符的编码是其它字符编码的子串,或是其它编码序列的字串,因此搜索一个字符时只要搜索它的字节编码序列即可,不用担心前后的上下文会对搜索

Go语言字符串面值中的Unicode转义字符让我们可以通过Unicode码点输入特殊的字符。有两种形式:\uhhhh对应16bit的码点值,\Uhhhhhhhh对应32bit的码点值,其中**h**是一个十六进制数字,

package main

import "fmt"

func main() {
    s := "\u4e16\u754c"
    s2 := "\U00004e16\U0000754c"
    fmt.Printf("%s\t%s\n",s, s2)

}
// out
// 世界    世界

例如,得益于UTF8编码优良的设计,诸多字符串操作都不需要解码操作。我们可以不用解码直接测试一个字符串是否是另一个字符串的前缀

// 前缀测试
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
}

当包含中西混合字符的时候,例如

package main

import "fmt" 
import "unicode/utf8"

func main() {
    s := "abc你好"
    fmt.Printf("%s\n",s)
    fmt.Println(len(s))
    fmt.Println(utf8.RuneCountInString(s))
}
// out :
// abc你好
// 9
// 5

为了处理这些真实的字符,我们需要一个UTF8解码器。unicode/utf8包提供了该功能,我们可以这样使用,每一次调用DecodeRuneInString函数都返回一个r和长度,r对应字符本身,长度对应r采用UTF8编码后的编码字节数目。长度可以用于更新第i个字符在字符串中的字节索引位置

package main

import "fmt" 
import "unicode/utf8"

func main() {
    s := "abc你好"
    for i := 0; i < len(s); {
        r, size := utf8.DecodeRuneInString(s[i:])
        fmt.Printf("%d\t%c\n", i, r)
        i += size
    }
}
// 0    a
// 1    b
// 2    c
// 3    你
// 6    好

**Go**语言的**range**循环在处理字符串的时候,会自动隐式解码**UTF8**字符串

package main

import "fmt" 

func main() {
    s := "abc你好"
    for i, r := range s {
        fmt.Printf("%d\t%q\t%d\n", i, r, r)
    }
}
// 0    'a'    97
// 1    'b'    98
// 2    'c'    99
// 3    '你'    20320
// 6    '好'    22909

可以使用以下程序统计字符串长度,或者直接调用utf8.RuneCountInString()

package main

import "fmt" 

func main() {
    s := "abc你好"
    n := 0
    for _, _ = range s {
        n++
    }
    fmt.Println(n)
}
// out: 6

rune

UTF8字符串作为交换格式是非常方便的,但是在程序内部采用rune序列可能更方便,因为rune大小一致,支持数组索引和方便切割,如果是将一个[]rune类型的Unicode字符slice或数组转为string,则对它们进行UTF8编码

package main

import "fmt" 

func main() {
    s := "abc你好"
    r := []rune(s)
    fmt.Printf("%x\t%x\t%x\n", r, r[0], r[3])
    fmt.Println(string(r))
}
// out: [61 62 63 4f60 597d]    61    4f60
// abc你好

将一个整数转型为字符串意思是生成以只包含对应Unicode码点字符的UTF8字符串
image.png
如果对应码点的字符是无效的,则用’\uFFFD无效字符作为替换
image.png

字符串和Byte切片

标准库中有四个包对字符串处理尤为重要:bytesstringsstrconvunicode

  • strings包提供了许多如字符串的查询、 替换、 比较、 截断、 拆分和合并等功能
  • bytes包也提供了很多类似功能(strings)的函数, 但是针对和字符串有着相同结构的[]byte类型。 因为字符串是只读的, 因此逐步构建字符串会导致很多分配和复制。 在这种情况下, 使用bytes.Buffer类型将会更有效
  • strconv包提供了布尔型、 整型数、 浮点数和对应字符串的相互转换, 还提供了双引号转义相关的转换
  • unicode包提供了IsDigitIsLetterIsUpperIsLower等类似功能, 它们用于给字符分类。 每个函数有一个单一的rune类型的参数, 然后返回一个布尔值。 而像ToUpperToLower之类的转换函数将用于rune字符的大小写转换。

    byte

    一个字符串是包含的只读字节数组, 一旦创建, 是不可变的。 相比之下, 一个字节slice的元素则可以自由地修改, 字符串和字节slice也可以相互转换 ```go package main

import “fmt”

func main() { s := “AAATTT” b := []byte(s) b[1] = 66 // 修改元素 s2 := string(b) fmt.Println(s) fmt.Println(b) fmt.Println(s2) } // out // AAATTT // [65 66 65 84 84 84] // ABATTT

一个`[]byte(s)`转换是分配了一个**新的字节数组**用于保存字符串数据的拷贝,** 然后引用这个底层的字节数组**。 编译器的优化可以避免在一些场景下分配和复制字符串数据, **但总的来说需要确保在变量**`**b**`**被修改的情况下, 原始的s字符串也不会改变**。 将一个字节`slice`转到字符串的`string(b)`操作则是**构造一个字符串拷贝**, 以确保`s2`字符串是**只读**的<br />`bytes`包和`strings`同时提供了许多实用函数。 下面是`strings`包中的六个函数
```go
package main

import (
    "fmt"
    "strings"
)

func main() {
    s1 := "AAATTT"
    s2 := "ASSSSA"
    s3 := []string{s1, s2}
    fmt.Println(strings.Contains(s1, "AAA")) // func Contains(s, substr string) bool
    fmt.Println(strings.Count(s2, "A")) // func Count(s, sep string) int
    fmt.Println(strings.Fields(s2)) // func Fields(s string) []string, 转换为字符串数组
    fmt.Println(strings.HasPrefix(s2, "SSS")) // func HasPrefix(s, prefix string) bool, 相当于python的str.startswith()
    fmt.Println(strings.Index(s2, "SSS")) // func Index(s, sep string) int, 相当于python的str.find()
    fmt.Println(strings.Join(s3, "--+--")) // func Join(a []string, sep string) string
}
// out 
// true
// 2
// [ASSSSA]
// false
// -1
// AAATTT--+--ASSSSA

同样,byte包也有这几种函数,用法一致,只是string的输入变为[]byte
image.png

字符串和数字的转换

strconv包提供这类转换功能
将一个整数转为字符串, 一种方法是用**fmt.Sprintf**返回一个格式化的字符串; 另一个方法是用strconv.Itoa(“整数到ASCII”)

package main

import (
    "fmt"
    "strconv"
)

func main() {
    s1 := 65
    s2 := 66
    y := fmt.Sprintf("%d", s1)
    fmt.Println(y)
    fmt.Println(strconv.Itoa(s2) + y)
    fmt.Println(s1 + s2)
}
// out
// 65 
// 6665
// 131

常量

常量表达式的值在编译期计算, 而不是在运行期。 每种常量的潜在类型都是基础类型:booleanstring数字一个常量的声明语句定义了常量的名字, 和变量的声明语法类似, 常量的值不可修改, 这样可以防止在运行期被意外或恶意的修改。常量间的所有算术运算、 逻辑运算和比较运算的结果也是常量, 对常量的类型转换操作或以下函数调用都是返回常量结果: lencaprealimagcomplexunsafe.Sizeof
常量的声明

const pi = 3.14159
// 多个
const (
    pi = 3.14159
    e = 2.71828
)

常量可以是构成类型的一部分, 例如用于指定数组类型的长度
image.png
一个常量的声明也可以包含一个类型和一个值, 但是如果没有显式指明类型, 那么将从右边的表达式推断类型
time.Duration是一个命名类型, 底层类型是int64time.Minute是对应类型的常量。 下面声明的两个常量都是time.Duration类型, 可以通过%T参数打印类型信息
image.png
如果是批量声明的常量, 除了第一个外其它的常量右边的初始化表达式都可以省略, 如果省略初始化表达式则表示使用前面常量的初始化表达式写法, 对应的常量类型也一样的

const (
    a = 1
    b // b = 1
    c = 2
    d // d = 2
)

iota 常量生成器

常量声明可以使用iota常量生成器初始化, 它用于生成一组以相似规则初始化的常量, 但是不用每行都写一遍初始化表达式。 在一个const声明语句中, 在第一个声明的常量所在的行,**iota**将会被置为**0**, 然后在每一个有常量声明的行+1

type Weekday int
const (
    Sunday Weekday = iota // Sunday = 0
    Monday   // Monday = 1
    Tuesday  // Tuesday = 2
    Wednesday // Wednesday = 3
    Thursday
    Friday
    Saturday
)

生成KB MB``GB…的一系列常量

const (
    _ = 1 << (10 * iota) // 1 (10 * 0)
    KiB // 1024 --> 10000000000 (10 * 1)
    MiB // 1024 * 1024 --> 10000000000000000000 (10 * 2)
    GiB
    TiB
    PiB
    Eib
)

无类型常量

Go语言的常量有个不同寻常之处。 虽然一个常量可以有任意有一个确定的基础类型, 例如intfloat64, 或者是类似time.Duration这样命名的基础类型, 但是许多常量并没有一个明确的基础类型。 编译器为这些没有明确的基础类型的数字常量提供比基础类型更高精度的算术运算; 你可以认为至少有256bit的运算精度。 这里有六种未明确类型的常量类型

  1. 无类型的布尔型
  2. 无类型的整数
  3. 无类型的字符
  4. 无类型的浮点数
  5. 无类型的复数
  6. 无类型的字符串

例如上面的例子,ZiBYiB的值已经超出任何Go语言中整数类型能表达的范围, 但是它们依然是合法的常量, 而且可以像下面常量表达式依然有效
image.png
math.Pi无类型的浮点数常量, 可以直接用于任意需要浮点数或复数的地方,如果math.Pi被确定为特定类型, 比如float64, 那么结果精度可能会不一样, 同时对于需要float32complex128类型值的地方则会强制需要一个明确的类型转换
image.png
只有常量可以是无类型的。 当一个无类型的常量被赋值给一个变量的时候,或表达式中含有明确类型的值, 无类型的常量将会被隐式转换为对应的类型, 如果转换合法的话无论是隐式或显式转换, 将一种类型转换为另一种类型都要求目标可以表示原始值。 对于浮点数和复数, 可能会有舍入处理,对于一个没有显式类型的变量声明语法( 包括短变量声明语法) , 无类型的常量会被隐式转为默认的变量类型