Golang学习之基本类型与流程控制

Golang环境:https://studygolang.com/dl

LiteIde:http://liteide.org/cn/

Golang标准库文档:https://studygolang.com/pkgdoc

命令行下使用基础

go version 			//查看go版本
go env 			    //查看环境信息
go build xx.go		//编译代码,生成可执行的exe文件
go run xx.go		//运行go文件

一、第一个 Go 程序

// 1.go语言以包作为管理单位
// 2.每个go文件都必须在非注释的第一行代码中声明包名
// 3.liteide下,一个文件夹(目录)下,只能有一个main包
// 4.main包中有且只有一个main()函数
package main // main 表示包的标识

import "fmt"

// 程序的入口,是从这里开始调用的
func main() { //左括号必须和函数名同行
	//打印
	//"hello go"打印到屏幕,Println()会自动换行
	//调用函数,大部分都需要导入包
	/*
		这也是注释,这是块注释
	*/
	fmt.Println("hello go") //go语言语句结尾是没有分号的

	fmt.Println("hello world")
}

注意:关键字 import 后面的最后一个元素是目录名,而不是包名。Golang 编译器在这个路径下寻找包。

1.1 入口

**一个 go 工程有且只有一个入口函数 main()**。
注意:main() 函数不能有任何参数和返回值。

1.2 Golang 保留的关键字

包管理(2个):
	import	package

程序实体声明与定义(8个):
	chan	const	func	interface	map	struct	type	var

程序流程控制(15个):
	break	case	continue	default	defer	else	fallthrough	
	for		go		goto		if		range	return	select		switch

1.3 Golang预定义标识符

常量:
true, false, iota, nil

类型:
int, int8, int16, int32, int64, uint,
uint8, uint16, uint32, uint64, uintptr,
float32, float64, complex128, complex64,
bool, byte, rune, string

函数:
make, len, cap, new, append, copy, close, 
delete, complex, real, imag, panic, recover

二、数据类型

类型表示同一类的数据,计算机用来计算,计算前需要把数据存储起来,那么如何存储数据?

2.1 数据类型的作用

告诉编译器,这个数据应该以多大的内存进行存储,方便内存分配空间。
例如:写了一个数字 10,以 byte 类型存储,在内存中就占 1 个字节的大小;以 int 类型存储,在内存中就占4个字节。

2.2 数据类型的命名规则

1.由字母、下划线、数字构成
2.不能以数字开头
3.不能使用关键字
4.严格区分大小写

2.3 变量

2.3.1 何为变量

程序运行期间,可以改变的量。简而言之:一开始给它一个值,之后可以改变它的值。

2.3.2 声明变量

变量在使用前(赋值、打印等操作),必须先声明
语法:var 变量名 数据类型
1.声明一个变量 var a int
2.同时声明多个变量 var b, c int

2.3.3 变量的声明及赋值

例1:

var a int
a = 10

2.3.4 变量的初始化

所谓变量的初始化就是:声明变量的时候同时赋值。
例1:

var b int = 10 // 声明变量时,同时赋值(一步到位)
// int是可以省略不写的

例2:

var b int
b = 20
// 此例中分了两步走:
// 1.先声明
// 2.后赋值

2.3.5 自动推导类型

注意:自动推导类型必须给初始化值,它是通过这个值来确定具体的数据类型
例1:

package main

import "fmt"

func main() {
	c := 30 // 编译器会根据 30 这个值来推导对应的数据类型
	// %T 打印变量所属的类型
	fmt.Printf("c type is : %T\n", c)
	// 运行结果:
	// c type is : int
}

自动推导类型的大致流程:先声明 c 的类型,再给 c 赋值为整数值 30。
注意:海象运算符 := 前面,必须要有新的变量,不然就会报错。因为海象运算符的流程是先声明变量的类型,再给变量赋值(几乎同时发生),如果没有新的变量,那么就会变成重复声明了。

2.3.6 多重赋值

2.3.6.1 var() 包裹起来的多重声明
package main

import "fmt"

func main() {
	var (
		a int
		b float64
	) // 关键字var+(),把多个变量包裹起来
	a = 10
	b = 3.14

	fmt.Println(a, b) // 10 3.14
}
2.3.6.2 自动推导类型(官方推荐写法)

使用海象运算符 :=

func main() {
	a, b := 10, 3.14
	fmt.Println(a, b) // 10 3.14
}

2.3.7 匿名变量

使用单个下划线 _ 表示匿名变量,丢弃数据且不处理,也不会占用内存空间。

package main

import "fmt"

func main() {
	i, j := 10, 20
	tmp, _ := i, j // 这里,j就被丢弃掉了,不会处理j这个变量了
	fmt.Println(tmp, j) // 打印j的值依然是用之前的20
}

2.3.8 变量的输入

使用内建 fmt 包下的 Scan() 函数:

func main() {
	var a int
	fmt.Printf("输入变量a的值:")

	fmt.Scanf("%d", &a) // 取a的地址,此处会阻塞等待用户的输入
	// 或者下面更加简便的等价的写法:
	// fmt.Scan(&a) // 别忘了取地址
	fmt.Printf("刚才输入的值,a=%d\n", a)
}

/*
运行结果:
输入变量a的值:11
刚才输入的值,a=11
*/

2.3.9 变量使用注意事项

1.函数外的每个语句都必须以关键字开始。(varconstfunc 等)
2.海象运算符 := 不能在函数外使用。
3.下划线 _ 多用于占位,不会占用内存空间,表示丢弃该值。

2.4 常量

2.4.1 何为常量

程序运行期间,不能改变的量。一开始给了值,就不允许再改变了!

2.4.2 初始化常量

常量必须给值,所以就是直接初始化常量了,声明和赋值一步完成。

2.4.2.1 语法

const 常量名 [数据类型] = 值

2.4.2.2 例1,单个初始化
package main

import "fmt"

const a = 10
const b = 10

func main() {
	fmt.Println(a, b)
}
2.4.2.3 例2,包裹起来一起初始化

把例1的常量声明,用括号括起来,写在一起:

const (
	a = 10
	b = 10
)
2.4.2.4 例3,多重初始化
const a, b = 10, 3.14

func main() {
	fmt.Println(a, b) //10 3.14
}

2.4.3 iota枚举

2.4.3.1 iota的特性1

iota 是常量自动生成器,每一行,自动累加1。

2.4.3.2 iota的特性2

iota 只能在常量表达式中使用(只能在 const 中使用)。
特性1 + 特性2的示例:

package main

import "fmt"

const (
	a = iota
	b = iota
	c = iota
)

func main() {
	fmt.Printf("a = %d, b = %d, c = %d\n", a, b, c) //a = 0, b = 1, c = 2
}
2.4.3.3 iota的特性3

遇到另一个 constiota 的值重新从 0 开始计算。

package main

import "fmt"

const (
	a = iota
	b
	c
)

const (
	d = iota
	e
)

func main() {
	const (
		f = iota
		g
		h
	)

	fmt.Println("a=", a)
	fmt.Println("b=", b)
	fmt.Println("c=", c)
	fmt.Println("d=", d)
	fmt.Println("e=", e)
	fmt.Println("f=", f)
	fmt.Println("g=", g)
	fmt.Println("h=", h)
}

/*
运行结果:
a= 0
b= 1
c= 2
d= 0
e= 1
f= 0
g= 1
h= 2
*/
2.4.3.4 iota的特性4

同一块 const 内,可以只写一个 iota 预定义标识符。

package main

import "fmt"

func main() {
	const (
		a1 = iota
		b1
		c1
		d1
	)

	fmt.Printf("a1 = %d, b1 = %d, c1 = %d, d1 = %d\n", a1, b1, c1, d1) //a1 = 0, b1 = 1, c1 = 2, d1 = 3
}
2.4.3.5 iota的特性5

如果是同一行,iota 值都一样。

package main

import "fmt"

func main() {
	const (
		i          = iota
		j1, j2, j3 = iota, iota, iota
		k          = iota
	)

	fmt.Printf("i=%d, j1=%d, j2=%d, j3=%d, k=%d\n", i, j1, j2, j3, k) //i=0, j1=1, j2=1, j3=1, k=2
}

原因:iota 是每一行自动累加 1,在同一行内的 iota 值都是相同的。

2.4.3.6 iota的特性6

iota 的值,只取决于它所在第几行(下标从 0 开始),可以理解成为:行索引。

package main

import "fmt"

func main() {
	const (
		i          = 10
		j1, j2, j3 = iota, iota, iota
		k          = 20
		l, m       = iota, iota
		n          = "abc"
		o          = iota
	)

	fmt.Printf("i=%d, j1=%d, j2=%d, j3=%d, k=%d, l=%d, m=%d, n=%s, o=%d\n", i, j1, j2, j3, k, l, m, n, o)
}

/*
运行结果:
i=10, j1=1, j2=1, j3=1, k=20, l=3, m=3, n=abc, o=5
*/

2.4.4 常量使用的注意事项

2.4.4.1 初始化常量不允许使用海象运算符:=
const :b = 10
2.4.4.2 常量不允许被修改

下例中,对常量重新赋值就会报错:

const a = 10
a = 20
2.4.4.3 常量必须给值

常量表示不可变的值,不给初始值,怎么让编译器去常量化呢?

const(
	a int
	b
)

三、基础数据类型

指定数据类型是告诉编译器,这个值需要分配多大的内存空间。

3.1 总述

3.1.1 Go 语言中的数据类型

以下是 Go 语言中的数据类型:

类型名称长度零值说明
bool布尔类型1false其值不为真即为假,不可以用数字代表true或false
byte字节型10uint8别名
rune字符类型40专用于存储unicode编码,等价于uint32
int, uint整型4或8032位或64位
int8, uint8整型10-128 ~ 127, 0 ~ 255
int16, uint16整型20-32768 ~ 32767, 0 ~ 65535
int32, uint32整型40-21亿 ~ 21 亿, 0 ~ 42 亿
int64, uint64整型80
float32浮点型40.0小数位精确到7位
float64浮点型80.0小数位精确到15位
complex64 复数类型8
complex128 复数类型16
uintptr 整型4或8以存储指针的uint32或uint64整数
string字符串“”utf-8字符串

3.1.2 Go 语言的数据类型分类

1.基础类型(Basic Types)。包括了:数值类型(支持整型、浮点型、复数)、字符串类型、布尔类型。
2.符合类型(Aggregate Types)。包括了:数组、结构体。
3.引用类型(Reference Types)。包括了:指针、切片、map、channel、接口、函数。

3.1.3 注意数据类型的溢出

每个数据类型都有一个固定的取值范围,数据类型的值,不能超过这个范围。
数据类型的溢出有 2 种:编译时、运行时。

3.1.3.1 数据类型编译时的溢出

以下示例会导致编译时就报错:

func main() {
	var i uint8
	for i = 0; i < 270; i++ {
	}
}

/*
运行结果:
# command-line-arguments
.\main.go:5:15: constant 270 overflows uint8

Compilation finished with exit code 2
*/

这个示例就相当于语法错误了,数值已经溢出了该数据类型,编译都编不过。
使用 IDE,就会提示错误:

3.1.3.2 数据类型运行时的溢出

以下示例会导致运行时的溢出:

func main() {
	var myint uint8
	myint = 250

	for i := 0; i < 15; i++ {
		fmt.Println(myint)
		myint++
	}
}

/*
运行结果:
250
251
252
253
254
255
0
1
2
3
4
5
6
7
8

Process finished with exit code 0
*/

这个示例属于逻辑错误,不会报错,但是会影响到最终的结果。

3.2 浮点型

下例中,f2 会被自动推导成为 float64 类型:

func main() {
	f2 := 3.14
	fmt.Printf("f2 type is : %T\n", f2) //f2 type is : float64
}

float64float32 更加准确。使用自动推导类型的时候,浮点数会被推导成为 float64 类型。

3.3 字符类型

字符只是数值的特殊用例,Golang 使用数值来表示一个字符。
Golang 中,使用 byteint32rune 类型,来代表一个字符,一个字符由一对单引号 '' 包裹起来。
byte 类型(字节类型),它的本质是 uint8 类型,存储的时候会以一个 uint8 数值进行存储。
int32 类型,代表一个 UTF-8 字符。它还有另外一个书写方式:rune,两者是完全等价的,只是书写方式的不同而已(就像本名和小名,int32 是本名,rune 是小名)。

3.3.1 用byte表示一个单字符

byte 类型的本质是用 uint8 类型进行存储,uint8 的范围是 [0~255]。使用 byte 类型存储一个字符的时候,注意不要超出 uint8 的范围。

3.3.1.1 byte类型的基本使用

可以把 [0~255] 范围内的任意整数赋值给一个 byte 类型。

func main() {
	var ch byte //声明字符类型
	ch = 97
	fmt.Println("ch =", ch)        //打印出数字97,因为Golang是使用一个整型数值来表示一个字符
	fmt.Printf("%c, %d\n", ch, ch) //%c,指定以字符方式打印

	fmt.Println("--------------------------")

	ch = 'A'
	fmt.Println("ch =", ch)
	//%v是万能匹配格式符,表示该变量本身的值
	//因为Golang是使用一个整型数值来表示一个字符,因此ch本身的值就是一个整型数值
	fmt.Printf("%%v=%v, %%c=%c\n", ch, ch)
}

/*
运行结果:
ch = 97
a, 97
--------------------------
ch = 65
%v=65, %c=A
*/

输出时,如果想要看到一个完整的字符,需要指定以字符 %c 格式显示,否则字符类型是以它本质的 uint8 数值来显示的。

3.3.1.2 使用自动推导时,一个字符会被推导成 int32 类型

使用自动推导类型的时候,一个字符会被推导成 int32 类型。

func main() {
	ch := 'a' //'a'是一个字符
	fmt.Println("ch = ", ch)
	fmt.Printf("ch type is : %T\n", ch) //自动推导成int32类型
	fmt.Printf("%%v = %v\n", ch)        //%v万能匹配格式符,表示该变量本身的值
	fmt.Printf("%%c = %c\n", ch)        //%c指定以字符输出

	fmt.Println("----------------------")

	ch = '中'
	fmt.Println("ch = ", ch)
	fmt.Printf("ch type is : %T\n", ch)
	fmt.Printf("%%v = %v\n", ch)
	fmt.Printf("%%c = %c\n", ch)

	fmt.Println("----------------------")

	ch = ',' //英文状态下的逗号
	fmt.Println("ch = ", ch)
	fmt.Printf("ch type is : %T\n", ch)
	fmt.Printf("%%v = %v\n", ch)
	fmt.Printf("%%c = %c\n", ch)

	ch = ',' //中文状态下的逗号
	fmt.Println("ch = ", ch)
	fmt.Printf("ch type is : %T\n", ch)
	fmt.Printf("%%v = %v\n", ch)
	fmt.Printf("%%c = %c\n", ch)

	fmt.Println("----------------------")

	ch = '\n' //换行符
	fmt.Println("ch = ", ch)
	fmt.Printf("ch type is : %T\n", ch)
	fmt.Printf("%%v = %v\n", ch)
	fmt.Printf("%%c = %c", ch) //换行符是控制字符,不会直接显示,但会产生换行效果
}

/*
运行结果:
ch =  97
ch type is : int32
%v = 97
%c = a
----------------------
ch =  20013
ch type is : int32
%v = 20013
%c = 中
----------------------
ch =  44
ch type is : int32
%v = 44
%c = ,
ch =  65292
ch type is : int32
%v = 65292
%c = ,
----------------------
ch =  10
ch type is : int32
%v = 10
%c = 
//'\n'会产生换行效果,即使fmt.Printf()格式化字符串中不在末尾写'\n',也会发生换行
*/

可以看到,无论是单字符还是复合字符,就算本身是个 ASCII 码字符。只要用了自动推导类型,那么这个字符就会被推导成 int32 类型。
我个人猜想,Golang 并不能很明确地确定(或者判定时会影响一点性能)这个字符到底是单字符还是复合字符。所以就用了 int32 类型来保证能够存储任何类型字符,int32 类型也可以书写成 rune 类型,rune 类型的本质是 int32 类型。

3.3.2 案例

3.3.2.1 案例1:英文字母大小写转换

byte 本质上是 uint8 类型,所以两者可以直接相互转换、运算。格式化时,用 %c 来表示一个字符,用 %d 来表示整型数值。
英文字母大小写转换时,使用字符类型进行操作会非常好用。大小写之间的规律:大小写相差 32,小写的数值大。(大写 A 是 65,小写 a 是 97)
例:

func main() {
	fmt.Printf("大写A:%d,小写a: %d\n", 'A', 'a') //大写A:65,小写a:97
	fmt.Printf("大写A转小写a: %c\n", 'A'+32)     //大写A转小写a: a
	fmt.Printf("小写a转大写A:%c\n", 'a'-32)      //小写a转大写A:A
}

/*
运行结果:
大写A:65,小写a: 97
大写A转小写a: a
小写a转大写A:A
*/
3.3.2.2 案例2:计算英文单词每个字母加起来的值
3.3.2.2.1 题目描述

英语 26 个字母分别代表 1 到 26 的数字,编写一段代码,计算出单词的每个字母加起来等于多少。
例如:
hardwork(勤奋)8+1+18+4+23+15+18+11=98
knowledge(知识)11+14+15+23+12+5+4+7+5=96
love(爱)12+15+22+5=54
luck(运气)12+21+3+11=47
attitude(态度)1+20+20+9+20+21+4+5=100

3.3.2.2.2 实现方式

实现方式有很多,这里采用:对字符本身的 Unicode 码值的运算来实现。
实现思路:
Golang 中,字符串的存储方式是:采用 UTF-8 编码格式下的 Unicode 码值,进行存储。遍历字符串中的每个字符,都将得到这个字符的 Unicode 码值。
假设,输入的都是正确的英文单词。那么单词中的每个字母,它就是一个字符,可以得到它本身的 Unicode 码值。
通过减值的方式来确定字母的大小写。大写字母的范围是 [65 ~ 90], 小写字母的范围是 [97 ~ 122],任何一个大写字母减去 96,都会小于 1,因此遇到大写字母,需要减去 64,刚好能够对应上 26 个字母,由 1 到 26 的数值排列。小写字母直接减去 96,就能对应上了。

package main

import "fmt"

// 计算一个英文单词中的每个字母之和
func computeletters(s string) (sum int64) {
	for i, char := range s {
		// 非英文字母的情况
		// 空格的 ASCII 码值为 32,需要把空格的情况添加进去
		if char != 32 && !(char >= 65 && char <= 90) && !(char >= 97 && char <= 122) {
			fmt.Printf("char:%c, index:%d. does not an english letter.\n", char, i)
			return -1
		}

		if char == 32 {
			// 遇到空格,什么都不做
		} else if char-96 < 1 {
			// Unicode 值减去 96,小于了 1,说明这个英文字母是大写的,大写应该减 64
			sum += int64(char - 64)
		} else {
			sum += int64(char - 96)
		}
	}

	return
}

func main() {
	arr := []string{"hardwork", "knowledge", "love", "luck", "attitude"}

	for i := range arr {
		v := computeletters(arr[i])
		fmt.Printf("word: %s, total value is:%d\n", arr[i], v)
	}
}


/*
运行结果:
word: hardwork, total value is:98
word: knowledge, total value is:96
word: love, total value is:54
word: luck, total value is:47
word: attitude, total value is:100
*/

byte 类型的本质是 uint8 类型,uint8 类型是一个整型数值,范围 [0~255] 的整数,所以它能与另一个整型数值进行运算。

3.3.2 用int32类型存储一个复合字符

当处理中文、日文或者其他复合字符时,则需要用 int32 类型来处理 Unicode 文本。
int32 类型也可以书写成 runerune 类型本质就是 int32 类型。

3.3.2.1 存储一个中文字符的方式

先看一个错误的示例:

func main() {
	var ch byte
	ch = '中' //这行会报错:constant 20013 overflows byte,超出了ASCII码的范围
	fmt.Printf("ch type is : %T\n", ch)
}

注意:byte 的范围是 [0~255],20013 明显超出了范围!
接下来,使用自动推导的技巧来实现存储一个中文字符:

func main() {
	ch := '中'
	fmt.Printf("ch type is : %T\n", ch) //int32
	fmt.Printf("using %%v, ch = %v\n", ch)
	fmt.Printf("using %%c, ch = %c\n", ch)
}

/*
运行结果:
ch type is : int32
using %v, ch = 20013
using %c, ch = 中
*/

与 3.3.1.2 中的示例演示一样:使用自动推导的时候,一个中文字符会被推导成为 int32 类型。

3.3.4 int32和rune的关系以及区别

3.3.4.1 int32和rune之间的关系

等价的关系。rune 只是 int32 的一个别名,在功能上完全等价。

3.3.4.2 见名知意的区别

用于更好地让程序猿区分,这个变量是字节值还是无符号整数值。用 rune 来表示一个字符值,用 int32 来表示一个整数值。
让人一看这个数据类型就能知道,这个变量的最终用途是什么。
例如:

func main() {
	chars := []rune{443, 27017, 6379, 3306}
	fmt.Println(string(chars)) //最终作为字符来呈现

	commonPorts := []int32{443, 27017, 6379, 3306}
	fmt.Println("there are some common ports: ", commonPorts) //最终作为数值来呈现
}

/*
运行结果:
ƻ榉ᣫ೪
there are some common ports:  [443 27017 6379 3306]
*/

看到 rune 就知道,最终呈现的是字符;看到 int32 就知道,最终显示数值。个人认为,int32rune 的区别就是见名知意的作用:看它们的数据类型就知道了该数据的最终意图和呈现方式。

3.3.5 转义字符

有两种字符:一种是控制字符,另一种是可显示字符。
可显示字符就是可以输出,能看得到内容的字符。
控制字符就是转义字符,有特殊的含义,是不可见的。以反斜杠 \ 开头的是转义字符。
例1:先看这个例子:

func main() {
	fmt.Printf("hello go")
	fmt.Printf("abcdefg")
}

/*
运行结果:
hello goabcdefg
//没有换行符,内容会紧跟在上一行的内容后面
/*

例2:
\n 不会输出到屏幕上,是看不见的。但它会进行换行操作,属于功能性的字符:

func main() {
	fmt.Printf("hello go%c", '\n')
	fmt.Printf("abcdefg")
}

/*
运行结果:
hello go
abcdefg
*/

3.3.6 注意事项

3.3.6.1 字符只能使用一对单引号包裹起来

一个字符只能使用一对单引号''包裹起来!
下例中,ch = “a” 这行代码会报错:

func main() {
	var ch byte //声明字符类型
	ch = "a"    //报错:cannot use "a" (type string) as type byte in assignment(不能使用字符串作为byte字符的值)
	fmt.Printf("%c, %d\n", ch, ch)
}

下例是能够正确运行的:

func main() {
	var ch byte //声明字符类型
	ch = 'a'
	fmt.Printf("%c, %d\n", ch, ch) //a, 97
}
3.3.6.2 一对单引号 '' 中,只能放一个字符

a := 'abc'会报错

3.3.6.3 byte类型的范围

byte 类型实质上是一个 uint8 类型,uint8 的范围是 [0 ~ 255](0 和 255 都能被取到),不在这个范围就是值溢出,会报错!
数值不在 uint8 范围内的错误示例:

func main() {
	var ch byte //声明字符类型
	ch = 256
	fmt.Printf("%c, %d\n", ch, ch)
	/* 报错内容如下:
	# command-line-arguments
	.\main.go:7:7: constant 256 overflows byte
	*/
}

3.3.7 ASCII码参考表

ASCII值控制字符ASCII值控制字符ASCII值控制字符ASCII值控制字符
0NUT32(space)64@96
1SOH33!65A97a
2STX3466B98b
3ETX35#67C99c
4EOT36$68D100d
5ENQ37%69E101e
6ACK38&70F102f
7BEL39,71G103g
8BS40(72H104h
9HT41)73I105i
10LF42*74J106j
11VT43+75K107k
12FF44,76L108l
13CR45-77M109m
14SO46.78N110n
15SI47/79O111o
16DLE48080P112p
17DCI49181Q113q
18DC250282R114r
19DC351383S115s
20DC452484T116t
21NAK53585U117u
22SYN54686V118v
23TB55787W119w
24CAN56888X120x
25EM57989Y121y
26SUB58:90Z122z
27ESC59;91[123{
28FS60<92/124|
29GS61=93]125}
30RS62>94^126`
31US63?95_127DEL

3.4 字符串类型

由多个字符所组成的一串内容,被称为字符串。一个字符串需要用双引号 "" 包裹起来!
Golang 中的字符串以 UTF-8 编码方式存储,处理字符串也是采用 UTF-8 的编码方式,每个字符串都是 Unicode 字符集。

3.4.1 rune 类型是数值类型,与字符串类型不兼容

func main() {
	var str1 string
	str1 = 'abc' // 报错:Cannot use ''abc'' (type rune) as type string in assignment
	fmt.Println(str1)
}

str1 = 'abc'这行会报错!字符只能使用单引号 '' 包裹起来,使用双引号 "" 包裹的是字符串!

3.4.2 字符串的截取是以字节为单位

使用 len() 函数获取字符串长度时,获取到的是该 UTF-8 编码字符串的字节长度。

func main() {
	s := "我是" // 字符串中是两个中文汉字

	fmt.Println("len(c)=", len(s))

	fmt.Printf("s[1], value=%v, char=%c\n", s[1], s[1])
	fmt.Println("s[:3]=", s[:2])
	fmt.Println("s[:3]=", s[:3])
}

/*
运行结果:
len(c)= 6
s[1], value=136, char=ˆ
s[:3]= �
s[:3]= 我
*/

在中文字符串中肆意乱截取,很大概率会输出乱码。
由 len(s) 可以得知,此例中字符串 s 的长度为 6,而字符串中只有 2 个汉字。因此,在 Golang 中,一个汉字占 3 个字节。一个汉字由 3 个字节编码而成,使用下标取值时,只是取到了某一个字节的值而已。

3.4.3 对字符串的索引,只会一个 byte

Golang 使用 UTF-8 编码方式存储、处理字符串,最终是以 Unicode 字符集的形式来表现字符串。Unicode 字符集最终也是用整数数值的方式来呈现的。
因此,对字符串的索引操作,会返回一个 byte 值,而不是一个具体显示内容的字符。
示例:

func main() {
	s := "我是gopher"

	for i := 0; i < len(s); i++ {
		fmt.Printf("s[%d], %%d=%d, %%c=%c\n", i, s[i], s[i])
	}
}

/*
运行结果:
s[0], %d=230, %c=æ
s[1], %d=136, %c=ˆ
s[2], %d=145, %c=‘
s[3], %d=230, %c=æ
s[4], %d=152, %c=˜
s[5], %d=175, %c=¯
s[6], %d=103, %c=g
s[7], %d=111, %c=o
s[8], %d=112, %c=p
s[9], %d=104, %c=h
s[10], %d=101, %c=e
s[11], %d=114, %c=r
*/

如果字符串中含有中文字符(一个中文字符在 UTF-8 编码方式下,占 3 个字节)。因为是由 3 个字节编码而成,如果按照索引来取含有 UTF-8 编码的字符,就会出现乱码。

3.4.4 遍历中英文混合字符串的示例

UTF-8 是一种变长的编码方式,字符长度从 1 个字节到 4 个字节不等。而 byte 类型只占 1 个字节。就算你想要使用多个 byte 进行表示,但也无从知晓要处理的 UTF-8 字符究竟占了几个字节。
利用 []int32()[]rune() 将字符串转为 Unicode 字符集(Unicode 的数值),再进行截取。这样就无需考虑字符串中是否含有 UTF-8 字符的情况了。

3.4.4.1 []int32() 写法
func main() {
	s := "我是gopher"

	fmt.Println("s=", s) //fmt.Print()系列函数是输出内容的本身

	//Golang使用UTF-8编码方式将字符串存储成Unicode字符集
	//UTF-8编码方式下,一个中文占3个字节
	//[:2]只是取到了前两个字节值而已,所以显示了乱码
	fmt.Println("s[:2]=", s[:2])
	fmt.Println("s[1]=", s[1])

	//遍历取到的是字符串中的每个字符。如果是中文字符,那么依然是占3个字节,遍历的时候也是取了3个字节
	for i, v := range s {
		//格式化打印是直接显示原本的字符内容
		fmt.Printf("s[%d] : unicode=%v, %%c=%c\n", i, v, v)
	}

	fmt.Println("----------------------------------------")

	//利用[]int32()将字符串存储为Unicode字符集
	unicodeString := []int32(s)
	fmt.Println("unicodeString value : ", unicodeString) //显示的是Unicode字符的数值
	fmt.Println("unicodeString[:3] : ", unicodeString[:3])

	//遍历的是Unicode字符集切片
	for i, v := range unicodeString {
		fmt.Printf("s[%d] = %v\n", i, v)
	}

	//截取到的是Unicode切片中的前2个值
	//将这2个值转换为字符串之后再输出
	fmt.Println(string(unicodeString[:2]))
	fmt.Println(unicodeString[:2]) //不转换成字符串只会输出原本的内容。fmt.Print()系列函数是输出内容的本身
}

/*
运行结果:
s= 我是gopher
s[:2]= �
s[1]= 136
s[0] : unicode=25105, %c=我
s[3] : unicode=26159, %c=是
s[6] : unicode=103, %c=g
s[7] : unicode=111, %c=o
s[8] : unicode=112, %c=p
s[9] : unicode=104, %c=h
s[10] : unicode=101, %c=e
s[11] : unicode=114, %c=r
----------------------------------------
unicodeString value :  [25105 26159 103 111 112 104 101 114]
unicodeString[:3] :  [25105 26159 103]
s[0] = 25105
s[1] = 26159
s[2] = 103
s[3] = 111
s[4] = 112
s[5] = 104
s[6] = 101
s[7] = 114
我是
[25105 26159]
*/
3.4.4.2 与 []int32() 等价的 []rune() 写法

[]rune() 将字符串转换为 Unicode 码点。

func main() {
	s := "我爱 Golang!" //感叹号是中文的

	unicodeRune := []rune(s)

	fmt.Println("unicode value : ", unicodeRune)
	fmt.Println("unicodeRune[:2], unicode value : ", unicodeRune[:2])
	fmt.Println("string(unicodeRune[:2]) = ", string(unicodeRune[:2]))
	fmt.Println("string(unicodeRune) : ", string(unicodeRune))
}

/*
运行结果:
unicode value :  [25105 29233 32 71 111 108 97 110 103 65281]
unicodeRune[:2], unicode value :  [25105 29233]
string(unicodeRune[:2]) =  我爱
string(unicodeRune) :  我爱 Golang!
*/

与 3.4.4.1 示例中的原理一致:[]int32()[]rune() 都是将字符串转换成 Unicode 字符数值。无论是截取还是直接打印,得到的都将是 Unicode 数值。若想要看到正常的文字内容,需要将 Unicode 数值转换为字符串后再打印。
注:UnicodeASCII 一样,都是一种字符集,UTF-8 是一种编码格式。

3.4.4.3 range 遍历字符串

range 遍历字符串,得到的是 rune 类型的字符。

3.4.4.3.1 获取下标的时候,得到的是 uint8 类型

使用 range 遍历字符串,通过下标去取字符串中的值,得到的是单个字符,字符的本质类型则是 uint8
示例:

func main() {
	s := "我是gopher"

	for i := range s {
		fmt.Printf("current i=%d, %%d=%[2]d, %%c=%[2]c, %%T=%[2]T\n", i, s[i])
	}

	fmt.Println("-------- 以下示例为了区别 range 遍历 --------")

	for i := 0; i < len(s); i++ {
		fmt.Printf("current i=%d, %%d=%[2]d, %%c=%[2]c, %%T=%[2]T\n", i, s[i])
	}
}

/*
运行结果:
current i=0, %d=230, %c=æ, %T=uint8
current i=3, %d=230, %c=æ, %T=uint8
current i=6, %d=103, %c=g, %T=uint8
current i=7, %d=111, %c=o, %T=uint8
current i=8, %d=112, %c=p, %T=uint8
current i=9, %d=104, %c=h, %T=uint8
current i=10, %d=101, %c=e, %T=uint8
current i=11, %d=114, %c=r, %T=uint8
-------- 以下示例为了区别 range 遍历 --------
current i=0, %d=230, %c=æ, %T=uint8
current i=1, %d=136, %c=ˆ, %T=uint8
current i=2, %d=145, %c=‘, %T=uint8
current i=3, %d=230, %c=æ, %T=uint8
current i=4, %d=152, %c=˜, %T=uint8
current i=5, %d=175, %c=¯, %T=uint8
current i=6, %d=103, %c=g, %T=uint8
current i=7, %d=111, %c=o, %T=uint8
current i=8, %d=112, %c=p, %T=uint8
current i=9, %d=104, %c=h, %T=uint8
current i=10, %d=101, %c=e, %T=uint8
current i=11, %d=114, %c=r, %T=uint8
*/

两段代码有一点区别:for i := range s 只出现了 2 个乱码,而 for i := 0; i < len(s); i++ 出现了 6 个乱码。
原因:
Golang 中,字符串是以 UTF-8 编码格式存放的 Unicode 字符码点,一个中文占 3 个编码字节。
for i := range s 的时候,虽然是以 []rune 类型在遍历。但遇到了通过下标去获取字符串中的内容(相当于指定去取字符串中,某个下标值中的内容),只能取到对应下标中的字节码值。第一次,i 是初始值 0;第二次,i 的值则变成了 3。所以,只是对应地去取了这 2 个下标中的内容。
for i := 0; i < len(s); i++ 的时候,就不再是遍历 []rune 类型了,是逐个逐个地遍历每一个字节,把字符串中的每个字节都遍历到了。

3.4.4.3.2 获取值的时候,得到的是 int32 类型

使用 range 遍历字符串的时候,值的部分是 int32 类型。
示例:

func main() {
	s := "我是gopher"

	for i, c := range s {
		fmt.Printf("current i=%d, %%d=%[2]d, %%c=%[2]c, %%T=%[2]T\n", i, c)
	}
}

/*
运行结果:
current i=0, %d=25105, %c=我, %T=int32
current i=3, %d=26159, %c=是, %T=int32
current i=6, %d=103, %c=g, %T=int32
current i=7, %d=111, %c=o, %T=int32
current i=8, %d=112, %c=p, %T=int32
current i=9, %d=104, %c=h, %T=int32
current i=10, %d=101, %c=e, %T=int32
current i=11, %d=114, %c=r, %T=int32
*/

使用 range 遍历字符串,分别得到下标和具体的值。因为一个中文字符在 Golang 的 UTF-8 编码下占 3 个字节,所以此例中,占用的下标就是 [0 ~ 2]、[3 ~ 5]。
runeint32 的别名,因此打印类型时,就显示了 rune 它本身的数据类型。

3.4.4.4 参考文献

https://juejin.im/post/6844903998634328078

3.4.5 如何修改字符串中的某个元素

我想要修改字符串中的某个元素,如何操作?

3.4.5.1 错误警示

直接对下标中的元素进行修改:

func main() {
	str := "hello"
	str[0] = 'x' //报错:cannot assign to str[0]
	fmt.Println(str)
}

编译就通不过,直接报错。原因:在 Golang 中,字符串是不可变的

3.4.5.2 正确的操作

使用 []byte()[]rune(),先将一个字符串转换成字节切片类型,然后对某个下标中的元素进行修改,最后使用 string() 转换回字符串。

3.4.5.2.1 单字符构成的字符串

如果这个字符串都是有单字符构成,那么先将这个字符串转换为 []byte 类型后,再修改某个下标中的元素,最后将这个 []byte 类型使用 string() 转换回来。

func main() {
	str := "hello"
	b := []byte(str) //先转换成 []byte 类型
	fmt.Println(b)

	b[0] = 'x'
	str = string(b) //再将 []byte 类型转换回字符串类型
	fmt.Println(str)
}

/*
运行结果:
[104 101 108 108 111]
xello
*/
3.4.5.2.2 字符串中有复合字符

如果一个字符串有复合字符,那么就需要使用 []rune[]int32 来转换。转换成字节后才能修改,最后依然用 string() 转换回字符串。

func main() {
	str := "go 你好"
	b := []rune(str) // []int32() 也可以
	fmt.Println(str, ",", b)

	fmt.Println(rune('很')) //单个字符转换,也可以直接写成:fmt.Println('很')

	b[3] = 24456
	str = string(b)
	fmt.Println(str)
}

/*
运行结果:
go 你好 , [103 111 32 20320 22909]
24456
go 很好
*/

3.4.6 其他类型与字符串的转换

3.4.6.1 十六进制转换为字符串

有一个字符串切片,里面的每个元素都是以字符串形式保存的十六进制值。现在要将这些十六进制值,转换为其原本的明文字符内容。

import (
	"fmt"
	"strconv"
)

func main() {
	original := []string{"30d7", "30ed", "30b0", "30e9", "30e0"}

	for _, v := range original {
		if s, err := strconv.ParseInt(v, 16, 32); err == nil {
			fmt.Printf("%T\t%d\t%c\n", v, s, s)
		} else {
			panic(err)
		}
	}
}

/*
运行结果:
string	12503	プ
string	12525	ロ
string	12464	グ
string	12521	ラ
string	12512	ム
*/

3.4.7 高性能字符串拼接的几个方式

将以下几个字符串拼接方式,都放入单独的函数中,最后使用 Go 语言自带的 Benchmark 进行简单的性能测试。
设置 1000 次拼接,因此:const Loop = 1000

3.4.7.1 使用 strings.Builder

它使用零值、不拷贝零值、使用内存最小。
示例:

func StrBuilder() {
	var str strings.Builder

	for i := 0; i < Loop; i++ {
		str.WriteString("a")
	}
}

注:不要拷贝 strings.Builder 的值

3.4.7.2 使用 bytes.Buffer

bytes 包的 Buffer 实现了 io.Writer 的接口,使用 bytes.BufferWriteString() 方法去拼接字符串,时间复杂度为 O(n)
示例:

func BytesBuffer() {
	var buffer bytes.Buffer

	for i := 0; i < Loop; i++ {
		buffer.WriteString("a")
	}
}
3.4.7.3 使用内建函数 copy

示例:

func GoCopy() {
	bs := make([]byte, 0, Loop)
	bl := 0

	for i := 0; i < Loop; i++ {
		bl += copy(bs[bl:], "a")
	}
}
3.4.7.4 使用内建函数 append

示例:

func GoAppend() {
	bs := make([]byte, 0, Loop)

	for i := 0; i < Loop; i++ {
		bs = append(bs, 'a')
	}
}
3.4.7.5 加号拼接

使用加号 + 进行拼接。
示例:

func StrPlus() {
	var result string

	for i := 0; i < Loop; i++ {
		result += "a"
	}
}
3.4.7.6 使用 strings.Repeat

将 N 个字符串 s,连接成一个新的字符串。
示例:

func StrRepeat() {
	for i := 0; i < Loop; i++ {
		strings.Repeat("a", Loop)
	}
}
3.4.7.7 编写 Benchmark 测试代码
import "testing"

func BenchmarkStrBuilder(b *testing.B) {
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		StrBuilder()
	}
}

func BenchmarkBytesBuffer(b *testing.B) {
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		BytesBuffer()
	}
}

func BenchmarkGoCopy(b *testing.B) {
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		GoCopy()
	}
}

func BenchmarkGoAppend(b *testing.B) {
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		GoAppend()
	}
}

func BenchmarkStrPlus(b *testing.B)  {
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		StrPlus()
	}
}

func BenchmarkStrRepeat(b *testing.B) {
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		StrRepeat()
	}
}
3.4.7.8 总结

在确切知道有多少内容的情况下,可以提前将 append()copy() 这两个内建函数的容量值(也就是 cap ),给申请下来,避免了 cap 的重复检查、扩容。
因此,在事先知道字符串的长度时:append() 的性能最高。copy() 的开销最少。
如果无法确定字符串内容的多少,最佳的方案就是 strings.BuilderWriteString() 方法。

3.4.7.9 参考文献

https://www.toutiao.com/a6736789153746256396

3.4.8 字符串不总是 UTF-8 文本

字符串的值可以包含任何字节,只有当字符串字面量(string literal)使用时,才会是 UTF-8
验证一个字符串是否为 UTF-8 文本,可以使用 unicode/utf8 包下的 utf8.ValidString() 方法。
示例:

func main() {
	s1 := "ABC"
	fmt.Println(utf8.ValidString(s1)) // true

	s2 := "\xAF"
	fmt.Println(utf8.ValidString(s2)) // false
}

备注:来源于:https://levy.at/blog/10(字符串不总是UTF8文本)

3.4.9 Go 字符串使用 byte 表示的原因

Go 字符串使用 byte 序列来表示,根本原因是因为各种语言的字符长度“飘忽不定”。
先看下面这段代码:

func main() {
	s1 := "é" // 这个是葡萄牙语,中译:它的
	fmt.Printf("len(s1)=%d, []rune=%#v, []byte=%#v\n", len(s1), []rune(s1), []byte(s1))

	// 既然在 byte 下占 3 个字节的长度,那么就可以对这个 byte 逐位修改
	b_s1 := []byte(s1)
	b_s1[0] = 'a'
	b_s1[1] = 'b'
	b_s1[2] = 'c'
	fmt.Println(string(b_s1))
}

/*
运行结果:
len(s1)=3, []rune=[]int32{101, 769}, []byte=[]byte{0x65, 0xcc, 0x81}
abc
*/

葡萄牙语的这个单词,在 []rune 中,占 2 个字节长度;而在 []byte 中,占了 3 个长度。
各国语言的字符,在不同的数据类型下([]rune []byte),长度不一等都一样。
下面看一个中文示例:

func main() {
	s1 := "中"
	fmt.Printf("len(s1)=%d, []rune=%#v, []byte=%#v\n", len(s1), []rune(s1), []byte(s1))
	fmt.Println("[]rune(s1) =", len([]rune(s1)))
}

/*
运行结果:
len(s1)=3, []rune=[]int32{20013}, []byte=[]byte{0xe4, 0xb8, 0xad}
[]rune(s1) = 1
*/

字符串中只有一个元素”中”,[]rune 下,占 1 个字节长度;而在 []byte 中,依然占了 3 个长度。
因为各个语言中的字符长度“飘忽不定”,因此只能采用一种统一的策略来进行管理,以免发生混乱。

3.5 字符和字符串的区别

1.字符只能用单引号包裹起来,字符串需要双引号包裹起来。
2.字符只能是单个字符,字符串是能多个字符所构成。
有的转义字符只是看起来是由两个字符构成。比如 \n:肉眼看上去是 \n 两个字符拼接在一起的,其实它是ASCII码 10 的另外一个书写形式。
示例:

func main() {
	var ch byte
	ch = 10
	fmt.Printf("aaa%cbbb", ch)
}

/*
运行结果:
aaa
bbb
*/

3.字符串的结尾都隐藏了一个结束符 \0
在ASCII码中它的10进制值是0,它是一个空字符,是看不到的。
str1 := "a"实则上是由'a''\0'组成了一个字符串。

3.6 bool类型

Golang 中,布尔类型的值只能是预定义标识符:true 或 false

3.6.1 注意事项

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

3.6.2 用数值来表示真假的错误示例

在 Golang 中,布尔类型的值只能是 truefalse
一些脚本编程语言或是一些弱类型编程语言中,数值 0 或 1 、空数组、空集合等也可以用来表真或假。但在 Golang 中,这是不允许的!Golang 中,bool 值只能是 truefalse
错误示例:

func main() {
	a := 0

	if a { //这行会报错:非布尔'a'(类型为int)用作条件
		fmt.Println("yes")
	}
}

3.7 复数类型

由实部和虚部构成。

func main() {
	var t1 complex128 //声明
	t1 = 3 + 5i       //赋值
	fmt.Println("t1 = ", t1)

	t2 := 7 + 9.9i
	fmt.Printf("t2 type is : %T\n", t2)

	//通过内建函数,取实部和虚部
	fmt.Printf("real(t2)=%v, imag(t2)=%v\n", real(t2), imag(t2))
}

/*
运行结果:
t1 =  (3+5i)
t2 type is : complex128
real(t2)=7, imag(t2)=9.9
*/

3.8 类型转换

类型转换只能转换相互兼容的类型,比如 byte 类型转换成 int 类型。
示例:

func main() {
	//这种不能转换的类型,叫不兼容类型
	var flag bool
	flag = true
	fmt.Printf("flag = %t\n", flag)

	//bool类型不能转换为int
	// fmt.Printf("flag = %d\n", int(flag))

	//其他语言中0就是假,非0就是真
	//整型也不能转换为bool
	//flag = bool

	var ch byte
	ch = 'a' //字符类型本质上就是整型
	var t int
	// t = ch      //会报错
	t = int(ch) //类型转换,把ch的值取出来,转换成整型
	fmt.Println("t = ", t)
}

/**运行结果
flag = true
t =  97
**/

3.9 类型别名

给一个数据类型起一个别名(小名),最常用的场景就是结构体 struct
语法:type 别名 数据类型
使用:var 变量名 别名
示例:

func main() {
	//给int64这个数据类型起个别名叫bigint
	type bigint int64
	var a bigint                      //a变量声明为bigint类型
	fmt.Printf("a type is : %T\n", a) //指向了自定义的bigint类型

	//多个别名一起声明
	type (
		char byte
		long int64
	)
	//声明变量为自定义类型
	var ch char
	var ll long
	ch = 'a'
	ll = 123456
	fmt.Printf("ch=%v, ll=%v\n", ch, ll)
	fmt.Printf("ch type is : %T, ll type is : %T\n", ch, ll)
}

/*
运行结果:
a type is : main.bigint
ch=97, ll=123456
ch type is : main.char, ll type is : main.long
*/

3.10 格式化输出

格式含义
%%一个%字面量
%b一个二进制整数值(基数为2),或者是一个(高级的)用科学计数法表示的指数为2的浮点数
%c字符型。可以把输入的数字按照ASCII码相应转换为对应的字符
%d一个十进制数值(基数为10)
%e以科学记数法e表示的浮点数或者复数值
%E以科学记数法E表示的浮点数或者复数值
%f以标准记数法表示的浮点数或者复数值
%g以%e或者%f表示的浮点数或者复数,任何一个都以最为紧凑的方式输出
%G以%E或者%f表示的浮点数或者复数,任何一个都以最为紧凑的方式输出
%o一个以八进制表示的数字(基数为8)
%p以十六进制(基数为16)表示的一个值的地址,前缀为0x,字母使用小写的a-f表示
%q使用Go语法以及必须时使用转义,以双引号括起来的字符串或者字节切片[]byte,或者是以单引号括起来的数字
%s字符串。输出字符串中的字符直至字符串中的空字符(字符串以’\0‘结尾,这个’\0’即空字符)
%t以true或者false输出的布尔值
%T使用Go语法输出的值的类型
%U一个用Unicode表示法表示的整型码点,默认值为4个数字字符
%v使用默认格式输出的内置或者自定义类型的值,或者是使用其类型的String()方式输出的自定义值,如果该方法存在的话
%x以十六进制表示的整型值(基数为十六),数字a-f使用小写表示
%X以十六进制表示的整型值(基数为十六),数字A-F使用小写表示

%v 属于万能格式,自动匹配格式输出。

func main() {
	a := 10
	b := "abc"
	c := 'a'
	d := 3.14

	//%T操作变量所属类型
	fmt.Printf("%T, %T, %T, %T\n", a, b, c, d)

	//%d	整型格式
	//%s	字符串格式
	//%c	字符格式
	//%f	浮点型格式
	fmt.Printf("a = %d, b = %s, c = %c, d = %f\n", a, b, c, d)
	//%v自动匹配格式输出
	fmt.Printf("a = %v, b = %v, c = %v, d = %v", a, b, c, d)
}

/**运行结果
int, string, int32, float64
a = 10, b = abc, c = a, d = 3.140000
a = 10, b = abc, c = 97, d = 3.14
**/

3.11 非十进制可选前缀

一个整数数值字面量无需带前缀,除非它是负数,或者自己想要添加一个加号。
非十进制可选前缀设置表示法:
二进制:0b0B
八进制:0, 0o0O
十六进制:0x0X; 在十六进制中,[a ~ f] 或 [A ~ F] 代表十进制值 10 ~ 15
只有单个 0 被认为是十进制的 0

3.12 _ 增强数值的可读性

如果有一个特别大的数值,一眼看过去,一时半会儿看不清楚它是百亿还是千亿。那么在代码中,可以在这个数值中添加下划线 _,来增强数值的可读性。
例如:

func main() {
	n := 123_456_789_000
	fmt.Printf("n type: %[1]T, value=%[1]v\n", n)

	n += 55_55555_55555
	fmt.Printf("n type: %[1]T, value=%[1]v\n", n)
}

/*
运行结果:
n type: int, value=123456789000
n type: int, value=679012344555
*/

其实是个语法糖,_ 被当做分隔符来使用了。自己看着怎么易读,就怎么分割。同样适用于十六进制的值以及小数中!

四、运算符

4.1 Golang内建的运算符

Golang 内建的运算符有:算术运算符、关系运算符、逻辑运算符、位运算符、赋值运算符。

4.2 算术运算符

运算符描述示例结果
+相加10 + 313
-相减10 - 37
*相乘10 * 330
/相除10 / 33
%取余10 % 31

Golang 中自增自减的注意
在 Golang 中,++(自增)和 --(自减)是单独的语句,并不是运算符。
而且自增自减必须是单独一行(单独的语句),下例中的自增是非法的:

i := 1
j = i++ //此处的 i++ 是非法的,i++ 必须是单独的语句

4.3 关系运算符

运算符描述示例结果
==相等于4 == 3false
!=不等于4 != 3true
>大于4 > 3true
<小于4 < 3false
>=大于等于4 >= 3true
<=小于等于4 <= 3false

4.4 逻辑运算符

运算符描述示例结果
&&逻辑与, 两边都为真, 则结果为真; 其余为假true && truetrue
||逻辑或, 两边都为假, 则结果为假; 其余为真true || falsetrue
!逻辑非, 条件为真, 则结果为假; 条件为假, 则结果为真!truefalse

4.5 位运算符

位运算符是对整数在内存中的二进制位进行操作。用得不多

运算符描述
&参与运算的两数各对应的二进位相与。 (两位均为1才为1)
|参与运算的两数各对应的二进位相或。 (两位有一个为1就为1)
^参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。 (两位不一样则为1)
<<左移n位就是乘以2的n次方。 “a<<b”是把a的各二进位全部左移b位,高位丢弃,低位补0。
>>右移n位就是除以2的n次方。 “a>>b”是把a的各二进位全部右移b位。

示例1:

const size = 2 << 4

func main() {
	fmt.Println("2 << 10 = ", 2<<10)     //表示:2*2**10,2乘以2的10次方
	fmt.Println("1024 >> 2 = ", 1024>>2) //表示:1024/2**2,1024除以2的2次方
	fmt.Println("const size = ", size)
}

/*
运算结果:
2 << 10 =  2048
1024 >> 2 =  256
const size =  32
*/

4.6 赋值运算符

运算符描述
=简单的赋值运算符,将一个表达式的值赋给一个左值
+=相加后再赋值
-=相减后再赋值
*=相乘后再赋值
/=相除后再赋值
%=求余后再赋值
<<=左移后赋值
>>=右移后赋值
&=按位与后赋值
|=按位或后赋值
^=按位异或后赋值

4.7 运算符优先级

优先级:从上往下由高到低

优先级运算符
7^ !
6* / % << >> && ^
5+ - `
4== != < <= > >=
3<-
2&&
1`

使用小括号()把一个表达式包裹起来可以提升优先级。

4.8 Golang中,不能使用复合表达式

其他编程语言中,比较一个数即要大于某个数同时也要小于某个数(比如:当 a=7 时,判断:a 是否大于等于 0 并且 a 是否小于等于 10),那么就会用到复合布尔表达式(例如:0 <= a <= 10)。
但在 Golang 中就会报错:

func main() {
	a := 7
	fmt.Println("0 <= a <= 10 的结果:", 0 <= a <= 10)
	/*
		cannot convert 10 (type untyped number) to type bool
		invalid operation: 0 <= a <= 10 (mismatched types bool and int)
		不匹配的类型bool和int
	*/
}

golang的int类型与bool类型不兼容0 <= a 得出的结果是一个 bool 类型的值 truetrue <= 10。10是一个 int 类型,true 是 bool 类型,两者的类型在 golang 中不兼容,因此报错!
需要使用&&)运算符:

func main() {
	a := 7
	fmt.Println("0 <= a <= 10 的结果:", 0 <= a && a <= 10)
	//0 <= a <= 10 的结果: true
}

五、流程控制

对程序做出逻辑性控制。

5.1 Golang最基本的三种程序运行控制

5.1.1 顺序控制

程序按顺序运行,流水账从上往下运行,不发生跳转。

5.1.2 选择控制

依据是否满足条件,有选择地执行相应功能。

5.1.3 循环控制

依据条件是否满足,循环多次执行某段代码。

5.2 if条件语句

if 语句支持1个初始化语句,初始化语句与判断语句写在同一行中。
示例:

func main() {
	if a := 10; a == 10 {
		fmt.Println("yes") //yes
	}
}

a := 10 是初始化语句,a == 10是判断语句。
这样可以非常好地控制变量的作用域:

func main() {
	if a := 10; a == 10 {	//a只能在这个代码块中有效
		fmt.Println("yes") //yes
	}

	fmt.Println(a) //undefined: a
}

5.3 switch

if...else if...else 另一种简洁灵活的写法。

5.3.1 语法

最基本的语法:

switch 变量本身 {
case 变量的值1:
	语句
case 变量的值2:
	语句
case 变量的值n:
	语句
default: //default可以省略
	语句
}

变量本身和下面 case 分支中的变量值进行比较(从上往下比较),匹配到了就进入对应的 case 分支中。
switch 也支持1个初始化语句。
注意:一个 case 分支中的值,不能与其他 case 分支中的值重复!

5.3.2 break

默认情况下,case 语句中自带了 break。执行完一个 case 分支后,自动跳出当前整个 switch,不会自动向下执行其他 case

5.3.3 fallthrough

不判断下一个case 变量的值,无条件强制执行下去。
fallthrough下面必须要接语句,无论是接 case 还是 default

func main() {
	switch num := 1; num { //支持1个初始化语句,初始化语句和变量本身使用分号分隔
	case 1:
		fmt.Println("print 1")
		fallthrough //不跳出switch语句块,下一个case语句不做判断,无条件强制执行下去
	case 2:
		fmt.Println("print 2")
		fallthrough //不跳出switch语句块,下一个case语句不做判断,无条件强制执行下去
	default:
		fmt.Println("print else")
	}
}

/*
运行结果:
print 1
print 2
print else
*/

5.3.4 根据case条件自行选择分支

switch 后面不接变量本身,而是根据case 条件的判断结果自行去选择分支。
语法:

switch { //不接变量本身,根据各个case条件进行判断、选择
case 条件1: //case后面放条件
	语句
case 条件2: //case后面放条件
	语句
case 条件n: //case后面放条件
	语句
default: //default可以省略
	语句
}

如果根据 case 条件的判断结果去自行选择分支,那么 switch 后面就不能接变量本身了:

func main() {
	grade := 80
	switch { //这里不写变量本身了
	case grade > 90: //case分支中判断是否满足条件,符合条件进入这个分支
		fmt.Println("优秀")
	case grade >= 80: //case中判断是否满足条件
		fmt.Println("良好")
	default:
		fmt.Println("其他")
	}
}

/*
运行结果:
良好
*/

5.3.5 测试多个符合条件的值

case 语句中,可以同时写上多个可能符合条件的值,使用逗号分割它们。例如:case 值1, 值2, 值3
示例:

func main() {
	switch grade := 70; grade {
	case 90:
		fmt.Println("优秀")
	case 80:
		fmt.Println("良好")
	case 60, 70:
		fmt.Println("及格")
	default:
		fmt.Println("不及格")
	}
}

/*
运行结果:
及格
*/

5.4 for循环

Golang 中的循环只有 for 循环。

5.4.1 基本语法

package main //必须有一个main包

import "fmt" //导入包含,必须要使用

func main() {
	//for初始化条件; 判断条件; 条件变化{
	// }
	//1+2+3...+100 累加

	sum := 0

	//1) 初始化条件 i := 1
	//2) 判断条件是否为真,i <= 100,如果为真,执行循环体,如果为假,跳出循环
	//3) 条件变化 i++
	//4) 重复2,3,4
	for i := 1; i <= 100; i++ {
		sum += i
	}
	fmt.Println("sum = ", sum)
}

5.4.2 基本流程

1.初始化条件。
2.判断条件是否为真,如果为真进入循环体内,如果为假则跳出循环。
3.执行条件变化的语句。
4.重复 2.3.4 步骤。

5.4.3 range迭代

一种自动实现的迭代器,常用于:数组、切片、通道。需要在 for 循环中使用。
range 有两个返回值:第一个返回值是元素的下标;第二个返回值是元素自身的值。
示例:

package main //必须有一个main包

import "fmt" //导入包含,必须要使用

func main() {
	str := "abc"

	//通过for打印每个字符
	for i := 0; i < len(str); i++ {
		fmt.Printf("str[%d]=%c\n", i, str[i])
	}

	//迭代打印每个元素,默认返回2个值:一个是元素的位置,一个是元素本身
	for i, data := range str {
		fmt.Printf("str[%d]=%c\n", i, data)
	}

	for i := range str { //第2个返回值,默认丢弃,返回元素的位置(下标)
		fmt.Printf("str[%d]=%c\n", i, str[i])
	}

	for i, _ := range str { //第2个返回值,默认丢弃,返回元素的位置(下标)
		fmt.Printf("str[%d]=%c\n", i, str[i])
	}
}


/*
运行结果:
str[0]=a
str[1]=b
str[2]=c
str[0]=a
str[1]=b
str[2]=c
str[0]=a
str[1]=b
str[2]=c
str[0]=a
str[1]=b
str[2]=c
*/

5.4.4 几个for循环小案例

5.4.4.1 九九乘法表

打印九九乘法表:

func main() {
	//外层控制共循环几次
	for i := 1; i < 10; i++ {

		//里层控制每次循环需要计算几次
		for j := 1; j <= i; j++ {
			fmt.Printf("%d x %d = %d\t", j, i, i*j) //1*9的格式来显示
		}
		fmt.Println()
	}
}

/*
运行结果:
1 x 1 = 1
1 x 2 = 2       2 x 2 = 4
1 x 3 = 3       2 x 3 = 6       3 x 3 = 9
1 x 4 = 4       2 x 4 = 8       3 x 4 = 12      4 x 4 = 16
1 x 5 = 5       2 x 5 = 10      3 x 5 = 15      4 x 5 = 20      5 x 5 = 25
1 x 6 = 6       2 x 6 = 12      3 x 6 = 18      4 x 6 = 24      5 x 6 = 30      6 x 6 = 36
1 x 7 = 7       2 x 7 = 14      3 x 7 = 21      4 x 7 = 28      5 x 7 = 35      6 x 7 = 42      7 x 7 = 49
1 x 8 = 8       2 x 8 = 16      3 x 8 = 24      4 x 8 = 32      5 x 8 = 40      6 x 8 = 48      7 x 8 = 56      8 x 8 = 64
1 x 9 = 9       2 x 9 = 18      3 x 9 = 27      4 x 9 = 36      5 x 9 = 45      6 x 9 = 54      7 x 9 = 63      8 x 9 = 72      9 x 9 = 81
*/
5.4.4.2 冒泡排序
func Bubbling(sli []int) {
	length := len(sli)

	//外层控制共循环几次
	for i := 0; i < length-1; i++ {

		//里层控制每个元素都参与比较
		for j := i + 1; j < length-1; j++ {
			if sli[i] < sli[j] { //策略:大的数字放在前面
				sli[i], sli[j] = sli[j], sli[i]
			}
		}
	}
}

func main() {
	sli := []int{5, 9, 10, 3, 6, 1, 0, 7, 8, 4, 2, 0}
	fmt.Println("before bubbling:", sli)
	Bubbling(sli)
	fmt.Println("after bubbling:", sli)
}

/*
运行结果:
before bubbling: [5 9 10 3 6 1 0 7 8 4 2 0]
after bubbling: [10 9 8 7 6 5 4 3 2 1 0 0]
*/

5.5 跳转语句

5.5.1 break

break 可以用在 forswitchselect

5.5.2 continue

continue 只能用在 for 循环中!

5.5.3 break和continue不能同时出现在同一级语句块中

breakcontinue 同时出现在同一级语句块中,就会自相矛盾,导致另一个语句无法到达!

5.6 goto

goto 可以在任何地方使用,但不能跨函数使用。
不建议使用,因为会破坏程序的结构

5.6.1 goto不能跨函数使用

package main

import "fmt"

func testFunc() {
END:
	fmt.Println("this is testFunc.")
}

func main() {
	goto END
}

/*
运行结果:
.\main.go:6:1: label END defined and not used
.\main.go:11:7: label END not defined
*/

5.6.2 goto无条件跳转

程序遇到 goto 语句,将会无条件强制跳转。

func main() {
	fmt.Println("111")

	goto END //自定义标签名,无条件强制跳转去该标签的所在代码块

	fmt.Println("3333") //此行代码永远无法到达

END:
	fmt.Println("END target")
}

/*
运行结果:
111
END target
*/

尝试将上面代码中的 END 标签和其代码块放到 main() 函数的第一行,会发生什么情况?
程序进入死循环,没完没了地一直在跳转。

func main() {
END:
	fmt.Println("END target")

	fmt.Println("111")

	goto END

	fmt.Println("3333")
}

5.6.3 goto 的使用场景示例

有这么一个场景:打印 1 ~ 10,不能用 for 循环。
goto 就能很容易实现需求:

func main() {
	n := 1

LOOP: // 定义 goto 的标签,以及实现代码逻辑
	fmt.Printf("%d ", n)
	n++

	if n <= 10 {
		goto LOOP // 跳转到该标签
	}
}

/*
运行结果:
1 2 3 4 5 6 7 8 9 10 
*/

以上代码,也是运用了循环的思维模式,只是换了跳转的方式而已。
还有一种方式就是递归:

var n = 1

func main() {
	if n <= 10 {
		fmt.Printf("%d ", n)
		n++
		main()
	}
}

/*
运行结果:
1 2 3 4 5 6 7 8 9 10
*/

递归是循环的另一种表现形式。

六、值类型 && 引用类型

6.1 值类型

bool
int(32 or 64), int8, int16, int32, int64
uint(32 or 64), uint8(byte), uint16, uint32, uint64
float32, float64
string
complex64, complex128
array // 固定长度的数组

6.2 引用类型

Golang 中,只有这几个引用类型:

slice      // 切片
map 	   // HashMap
pointer    //指针类型
channel    //管道(通道)
interface  //接口
function   //函数

本文作者: iceH
本文链接: http://www.secice.cn/p/f6d90fce
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!