布尔类型#

Go语言中的布尔类型与其他语言基本一致,关键字也为 bool 。布尔型数据只有 true / false 两个值。

	var a1 = false
	var a2 = true
	fmt.Println(a1, a2)

注意:

  1. 布尔类型变量的默认值为 false
  2. Go 语言中不允许将整型强制转换为布尔型
  3. 布尔型无法参与数值运算,也无法与其他类型进行转换

复数#

复数有实部和虚部,complex64 的实部和虚部为32位,complex128 的实部和虚部为64位,用的比较少不做详细记录。

	var c1 complex64
	c1 = 1 + 2i
	var c2 complex128
	c2 = 2 + 3i
	fmt.Println(c1)
	fmt.Println(c2)

整型#

整型分为以下两个大类:有符号整形按长度分为:int8int16int32int64,对应的无符号整型:uint8uint16uint32uint64

类型范围占用空间(字节)
int8(-128 到 127) -2^7 到 2^7-11 Byte
int16(-32768 到 32767) -2^15 到 2^15-12 Byte
int32(-2147483648 到 2147483647) -2^31 到 2^31-14 Byte
int64(-9223372036854775808 到 9223372036854775807) -2^63 到 2^63-18 Byte
uint8(0 到 255) 0 到 2^8-11 Byte
uint16(0 到 65535) 0 到 2^16-12 Byte
uint32(0 到 4294967295) 0 到 2^32-14 Byte
uint64(0 到 18446744073709551615) 0 到 2^64-18 Byte

特殊整型#

类型描述
uint32 位操作系统上就是 uint32,64 位操作系统上就是 uint64
int32 位操作系统上就是 int32,64 位操作系统上就是 int64
uintptr无符号整型,用于存放一个指针

注意: 在使用 intuint 类型时,不能假定它是 32 位或 64 位的整型,而是考虑 intuint 可能在不同平台上的差异

字节(Byte)#

字节也叫 Byte,是计算机数据的基本存储单位。1Byte(字节)=8bit(位) 1024Byte(字节)=1KB。中文字符在 unicode 下占2个字节,在 utf-8 编码下占3个字节,go 默认编码是 utf-8

unsafe.Sizeof(n1)unsafe包的一个函数,可以返回变量占用的字节数。

package main 
import ( 
    "fmt" 
    "unsafe" 
)

func main() { 
    var a int8 = 120 
    fmt.Printf("%T\n", a) 
    fmt.Println(unsafe.Sizeof(a)) 
}

输出:

PS D:\GoProject> go run .\test.go
int8
1

数字字面量语法#

Go1.13 版本之后引入了数字字面量语法,便于以二进制、八进制或十六进制浮点数的格式定义数字,例如:

func numberLiteralsSyntax() {
	// 代表二进制的 101101,相当于十进制的 45
	v1 := 0b00101101
	fmt.Printf("value:%v type:%T \n", v1, v1)

	// 代表八进制的377,相当于十进制的 255
	v2 := 0o377
	fmt.Printf("value:%v type:%T \n", v2, v2)

	// 代表十六进制的 1 除以 2²,也就是 0.25
	v3 := 0x1p-2
	fmt.Printf("value:%v type:%T \n", v3, v3)

	// 使用“_”分隔数字
	v4 := 123_456
	fmt.Printf("value:%v type:%T \n", v4, v4)
}

借助 fmt 函数以不同进制形式显示整数

func convertOutput() {
	// 十进制
	var a int = 10
	fmt.Printf("十进制:%d 二进制:%b \n", a, a) // %d表示十进制,%b表示二进制

	// 八进制以0开头
	var b int = 077
	fmt.Printf("八进制:%o 二进制:%d\n", b, b) // %o表示八进制,%d表示十进制

	// 十六进制 以 0x 开头
	var c int = 0xff
	fmt.Printf("十六进制:%x 十六进制大写:%X 十进制:%d 变量类型:%T \n", c, c, c, c) //%x 表示十六进制,%X 表示大写的十六进制,%d 表示十进制,%T 查看变量类型

	// int不同长度转换
	var num1 int8 = 127
	num2 := int32(num1)
	fmt.Printf("value:%v type:%T \n", num2, num2)
}

不同长度转换#

package main 
import ( 
    "fmt" 
)

func main() { 
    var num1 int8 
    num1 = 127 
    num2 := int32(num1) 
    fmt.Printf("值:%v 类型%T", num2, num2) 
}

浮点型#

Go 语言支持两种浮点型数:float32float64

这两种浮点型数据格式遵循 IEEE 754 标准

  • float32 最大范围约为 3.4e38,可以使用常量定义:math.MaxFloat32
  • float64 最大范围约为 1.8e308,可以使用常量定义:math.MaxFloat64

打印浮点数时可以使用 fmt 包配合动词 %f,比如:

	var f float64 = 3.1415926
	fmt.Printf("%f\n", f)             // 默认保留6位小数
	fmt.Printf("%.2f\n", f)           // 保留2位小数
	fmt.Printf("值:%v,类型:%T \n", f, f) // 浮点数默认类型是float64

	fmt.Println(math.MaxFloat32) // 输出float32最大值
	fmt.Println(math.MaxFloat64) // 输出float64最大值

	// 科学计数法表示浮点类型
	n1 := 5.1234e2
	n2 := 5.1234e2
	n3 := 5.1234e-2
	fmt.Println("n1=", n1, "n2=", n2, "n3=", n3)	

精度丢失问题#

精度丢失是几乎所有的编程语言都有的问题,是典型的二进制浮点数精度损失问题。在定长条件下,二进制小数和十进制小数互转可能有精度丢失,Go也不例外,观察以下代码:

func floatPrecisionLossTest() {
	d := 1129.6
	fmt.Println(d * 100) // 此处结果应该是:112960,实际打印的却是:112959.99999999999

	m1 := 8.2
	fmt.Printf("值:%v--类型:%T \n", m1, m1)
	m2 := 3.8
	fmt.Printf("值:%v--类型:%T \n", m2, m2)
	fmt.Println(m1 - m2) // 此处结果应该是:4.4,实际打印:4.3999999999999995
}

解决精度丢失问题#

使用第三方包需要使用 go mod ,关于 go mod 参考之前的章节。引入第三方包解决经度丢失问题:

PS D:\GoProject\src> go get github.com/shopspring/decimal
go: downloading github.com/shopspring/decimal v1.3.1
go get: added github.com/shopspring/decimal v1.3.1
import (
	"github.com/shopspring/decimal"
)
/*
使用第三方包解决float精度丢失问题:go get github.com/shopspring/decimal
文档:https://pkg.go.dev/github.com/shopspring/decimal#section-readme
*/
func floatPrecisionLossSolveTest() {
	a := decimal.NewFromFloat(1129.6)
	b := decimal.NewFromInt(100)
	fmt.Println(a.Mul(b)) // output:112960

	c := decimal.NewFromFloat(8.2)
	d := decimal.NewFromFloat(3.8)
	fmt.Println(c.Sub(d)) // output:4.4

	// 初始化一个变量
	d0 := decimal.NewFromFloat(0)
	// 设置精度 为三位 四舍五入的精度
	decimal.DivisionPrecision = 3
	fmt.Println(d0)

	// 加法 Add
	var f1 float64 = 2.1
	var i1 int = 3
	fmt.Println(decimal.NewFromFloat(f1).Add(decimal.NewFromFloat(float64(i1)))) // 2.1 + 3: float和int相加=>output: "5.1"

	var f2 float64 = 2.1
	var f3 float64 = 3.1
	fmt.Println(decimal.NewFromFloat(f2).Add(decimal.NewFromFloat(f3))) // 2.1 + 3.1 (float和float相加)=>output: "5.2"

	var f4 float64 = 2
	var f5 float64 = 3
	fmt.Println(decimal.NewFromFloat(f4).Add(decimal.NewFromFloat(f5))) // 2 + 3 (int和int相加 可以直接相加 d1 = num1+num2)=> output: "5"

	// 	减法 Sub
	var f7 float64 = 3.1
	var f8 int = 2
	d1 := decimal.NewFromFloat(f7).Sub(decimal.NewFromFloat(float64(f8))) // 3.1 - 2 float和int相减 => output: "1.1"
	fmt.Println(d1)

	var n1 float64 = 2.1
	var n2 float64 = 3.1
	fmt.Println(decimal.NewFromFloat(n1).Sub(decimal.NewFromFloat(n2))) // 2.1 - 3.1 (float和float相减)=>output: "-1"

	var n3 int = 2
	var n4 int = 3
	fmt.Println(decimal.NewFromFloat(float64(n3)).Sub(decimal.NewFromFloat(float64(n4)))) // 2 - 3 (int和int相减 d1 = num1 - num2) => output: "-1"

	// 	乘法 Mul
	var n5 float64 = 3.1
	var n6 int = 2
	fmt.Println(decimal.NewFromFloat(n5).Mul(decimal.NewFromFloat(float64(n6)))) // 3.1 * 2 float和int相乘 => output: "6.2"

	var n7 float64 = 2.1
	var n8 float64 = 3.1
	fmt.Println(decimal.NewFromFloat(n7).Mul(decimal.NewFromFloat(n8))) // 2.1 * 3.1 (float和float相乘) => output: "6.51"

	var n9 int = 2
	var n10 int = 3
	fmt.Println(decimal.NewFromFloat(float64(n9)).Mul(decimal.NewFromFloat(float64(n10)))) // 2 * 3 int和int相乘(d1 = num1 * num2) =>  output: "6"

	// 	除法 Div
	var n11 int = 2
	var n12 int = 3
	fmt.Println(decimal.NewFromFloat(float64(n11)).Div(decimal.NewFromFloat(float64(n12)))) // 2 / 3 = 0.6666666666666667  =>  output: "0.6666666666666667"

	// 	float64 和 int 相除
	var num13 float64 = 2.1
	var num14 int = 3
	fmt.Println(decimal.NewFromFloat(num13).Div(decimal.NewFromFloat(float64(num14)))) // output: "0.7"

	// 	float64 和 float64 相除
	var num15 float64 = 2.1
	var num16 float64 = 0.3
	fmt.Println(decimal.NewFromFloat(num15).Div(decimal.NewFromFloat(num16))) // output: "7"

	// int 和 int 相除 并保持3位精度
	var num17 int = 2
	var num18 int = 3
	decimal.DivisionPrecision = 3
	result := decimal.NewFromFloat(float64(num17)).Div(decimal.NewFromFloat(float64(num18))) // 2/3 = 0.667 =>  output: "0.667"
	fmt.Println(result)
}

科学计数法表示#

	n1 := 5.1234e2
	n2 := 5.1234e2
	n3 := 5.1234e-2
	fmt.Println("n1=", n1, "n2=", n2, "n3=", n3)

字符串#

Go 语言中,一个字符串是一个不可改变的字节序列。字符串可以包含任意的数据,包括byte值0,但是通常用来保存人类可读的文本。

Go的字符串是由 Utf-8 编码之后的字节序列,所以不能修改。

  • 1个汉字占用3个字节
  • 1个英文字母占用1个字节

Go使用 rune(1个int32类型的数字) 表示中文字符,使用 byte(1个uint8类型的数字)表示英文。Go语言中组成字符串的最小单位是字符,存储的最小单位是字节,字符串本身不支持修改。字节是数据存储的最小单元,每个字节的数据都可以用整数表示,例如一个字节储存的字符 a,实际存储的是 97 而非字符的字形,将这个实际存储的内容用数字表示的类型,称之为 byte

len#

内置的 len 函数用于返回一个字符串中的字节数目(不是 rune 字符数目),索引操作 s[i] 返回第 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')

如果试图访问超出字符串索引范围的字节将会导致 panic 异常:

c := s[len(s)] // panic: index out of range

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

比如:

	s := "abc北京"
	fmt.Printf("字节长度:%d \n", len(s)) //output:9
	// 返回字符串每个字节的值
	for i := 0; i < len(s); i++ {
		fmt.Println(s[i])
	}

输出:

字节长度:9 
97        // a  
98        // b
99        // c  
229       // 北  
140       // 北  
151       // 北
228       // 京 
186       // 京
172       // 京

英文字母占用一个字节:97:a/98:b/99:c,汉字占用三个字节:229/140/151组成228/186/172组成

如果要获取字符串中字符的个数,需要先把字符串转换成 []rune 类型,然后用 len() 函数获取字符个数:

	r := []rune(s)
	fmt.Print(len(r)) // output:5

字符串不可变性#

字符串的值是不可变的:一个字符串包含的字节序列永远不会被改变,当然也可以给一个字符串变量分配一个新字符串值。可以像下面这样将一个字符串追加到另一个字符串:

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

这并不会导致原始的字符串值被改变,但是变量 s 将因为 += 语句持有一个新的字符串值,但是 t 依然是包含原先的字符串值。

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

因为字符串是不可修改的,因此尝试修改字符串内部数据的操作也是被禁止的:

s[0] = 'L' // compile error: cannot assign to s[0]

或者使用 Sprintf :不会直接打印而是生成一个新的字符串

var result = fmt.Sprintf("%s%s", q1, q2)
fmt.Println(result)

字符串字面量#

Golang 支持两种类型的字符串字面量:

  • 非解释型表示法,需用用反引号"`“把字符序列包起来
  • 解释型表示法,需要用双引号"“包裹字符序列

区别在于:前者表示所见即所得的(除了回车符)。后者所表示的值中转义符会起作用

	// 字符串字面量语法:可以定义一个多行字符串
	var s1 string = "hello"
	var s2 string = `hello
				     world`
`

字符串转义符#

所谓的解释型字符串就是用双引号括起来的字符串(”"),其中的转义字符会被替换掉,这些转义字符包括:

\a    // 响铃     
\b    // 退格
\f    // 换页
\n    // 换行
\r    // 回车
\t    // 制表符
\u    // Unicode 字符
\v    // 垂直制表符
\"    // 双引号
\\    // 反斜杠

Go语言字符编码#

Go 语言采用的字符编码方案从属于 Unicode 编码规范。Go 语言的代码正是由 Unicode 字符组成的,且源码文件必须使用 UTF-8 编码格式进行存储。如果源码文件中出现了非 UTF-8 编码的字符,那么在构建、安装以及运行的时候,go 命令会报错 illegal UTF-8 encoding 。当一个 string 类型的值被转换为 []rune 类型值的时候,其中的字符串会被拆分成一个个的 Unicode 字符。

截取字符串#

可以通过下面的语法截取字符串中的内容:

	str := "abcdefg"	
	t1 := str[1:4] //startIndex=1,endIndex=4

此时 t1 的内容为 bcd,该语法通过下标索引的方式截取字符串中的内容,特点是 “左含右不含”。也就是说新的子串包含源串索引为 1 的字符,而不包含源串索引为 4 的字符。

如果要从源串的开始处截取可以省略第一个索引:

	t2 := str[:4]  //省略第一个索引从0开始,startIndex=0,endIndex=4

t2 的内容为 “abcd”。

如果要从源串的某个位置开始一直截取到末尾,可以省略第二个索引:

	t3 := str[1:]  //省略第二个索引,从1开始截取到末尾

t3 的内容为 “cdef”

访问越界问题:如果试图访问超出字符串索引范围的字节将会在运行时导致 panic 异常:

    t4 := str[:10]

runtime error: slice bounds out of range [:10] with length 7 数组越界

遍历字符串#

可以通过下标索引字符串中的字节,所以可以用这种方式遍历字符串:

	p := "abc你好"
	for i := 0; i < len(p); i++ {
		fmt.Printf("%c", p[i])
	}

输出:

abcä% å¥%

可见在字符串中含有非单字节的字符时这种方法是不正确的。range 函数解决这个问题:

for _, v := range s {
    fmt.Printf("%c", v)
}

输出:

abc你好

带有 range 子句的 for 语句会先把被遍历的字符串值拆成一个字节序列,然后再试图找出这个字节序列中包含的每一个 UTF-8 编码值,或者说每一个 Unicode 字符。因此在 range for 语句中,赋给第二个变量的值是UTF-8 编码值代表的那个 Unicode 字符,类型是 rune

字符串分割#

	url := "www.baidu.com"
	strArray := strings.Split(url, ".")
	fmt.Println(strArray)

字符串 Join#

fmt.Println(strings.Join(strArray, "."))

前缀判断#

fmt.Println(strings.HasPrefix(url, "www"))

后缀判断#

fmt.Println(strings.HasSuffix(url, "com"))

修改字符串#

Golang 中不能修改字符串的内容,就是说不能通过 s[i] 这种方式修改字符串中的字符。要修改字符串的内容,可以先将字符串的内容复制到一个可写的变量中,一般是 []byte[]rune 类型的变量,然后再进行修改。

注意:如果要对字符串中的字节进行修改,就转换为 []byte 类型,如果要对字符串中的字符修改,就转换为 []rune 类型,转换类型的过程中会自动复制数据

修改字符串中的字节([]byte) 对于单字节字符来说可以用这种方式进行修改:

	ss := "hello world"
	value := []byte(ss) // 转换为[]byte
	value[5] = ','      // 将空格替换为“,”
	fmt.Printf("%s\n", ss)
	fmt.Printf("%s\n", value)

输出:

hello world
hello,world

修改字符串中的字符([]rune)

	sss := "一梦三两年"
	value2 := []rune(sss) // 转换为[]rune
	value2[2] = '四'
	value2[3] = '五'
	fmt.Println(sss)
	fmt.Println(string(value2))

输出:

一梦三两年
一梦四五年

注意:string 类型的 0 值是长度为 0 的字符串,即空字符串 "”

strconv 包#

除了字符串、字符、字节之间的转换,字符串和数值之间的转换也比较常见。由 strconv 包提供这类转换功能。

将一个整数转为字符串,一种方法是用 fmt.Sprintf 返回一个格式化的字符串;另一个方法是用strconv.Itoa()

x := 123
y := fmt.Sprintf("%d", x)
fmt.Println(y, strconv.Itoa(x)) // "123 123" “整数到ASCII”

FormatIntFormatUint 函数可以用不同的进制来格式化数字:

fmt.Println(strconv.FormatInt(int64(x), 2)) // "1111011"

fmt.Printf 函数的 %b%d%o%x 等参数提供功能往往比 strconv 包的 Format 函数方便很多,特别是在需要包含有附加额外信息的时候:

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

如果要将一个字符串解析为整数,可以使用 strconv 包的AtoiParseInt函数,还有用于解析无符号整数的ParseUint 函数:

x, err := strconv.Atoi("123")             // x is an int
y, err := strconv.ParseInt("123", 10, 64) // base 10, up to 64 bits

ParseInt 函数的第三个参数是用于指定整型数的大小;例如16表示int16,0则表示int。在任何情况下,返回的结果y总是int64类型,可以通过强制类型转换将它转为更小的整数类型。

有时候也会使用 fmt.Scanf 来解析输入的字符串和数字,特别是当字符串和数字混合在一行的时候,它可以灵活处理不完整或不规则的输入。

字符类型#

在Go语言中支持两个字符类型,一个是byte(实际上是uint8的别名),代表UTF-8字符串的单个字节的值;另一个是rune,代表单个Unicode字符。

一个 string 类型的值既可以被拆分为一个包含多个字符的序列,也可以被拆分为一个包含多个字节的序列

  • 前者由以 rune 为元素类型的切片表示
  • 后者由以 byte 为元素类型的切片代表

rune 是Go语言特有的一个基本数据类型,它的一个值就代表一个字符,即:一个Unicode 字符(就是一个中文字符,占3个字节)

看以下代码:

	s := "helloworld 世界你好"
	fmt.Printf("string:%q\n", s)              // 原文格式输出
	fmt.Printf("rune(char):%q\n", []rune(s))  // 输出[]rune切片
	fmt.Printf("rune(hex):%x\n", []rune(s))   // 采用16进制数表示
	fmt.Printf("bytes(hex):% x\n", []byte(s)) // 输出[]byte切片

输出:

string:"helloworld 世界你好"
rune(char):['h' 'e' 'l' 'l' 'o' 'w' 'o' 'r' 'l' 'd' ' ' '世' '界' '你' '好']
rune(hex):[68 65 6c 6c 6f 77 6f 72 6c 64 20 4e16 754c 4f60 597d]
bytes(hex):68 65 6c 6c 6f 77 6f 72 6c 64 20 e4 b8 96 e7 95 8c e4 bd a0 e5 a5 bd

结果分析:

  1. 第二行输出说明:字符串在被转换为 []rune 类型的值时每个字符都会成为一个独立的 rune 类型的元素值。
  2. 由于每个 rune 底层的值都是采用 UTF-8 编码值来表达的,所以第三行采用 16进制数 来表示上述字符串,每一个16进制的字符分别表示一个字符,可以看到,当遇到中文字符时,由于底层存储需要更大的空间,所以使用的16进制数字也比较大,比如 4e16754c 分别代表世和界。
  3. 如果将整个字符的 UTF-8 编码值都拆成字节序列时,就变成了第四行的输出,可以看到一个中文字符底层占用了三个byte,比如e4 b8 96 / e7 95 8c分别对应UFT-8编码值的 4e16754c,也就是中文字符中的世和界。

总结: string 类型的值会由若干个 Unicode 字符组成,每个 Unicode 字符都可以由一个 rune 类型的值来承载。这些字符在底层都会被转换为 UTF-8 编码值,而这些 UTF-8 编码值又会以字节序列的形式表达和存储。所以:一个 string 类型的值在底层就是一个能够表达若干个 UTF-8 编码值的字节序列。