20小时快速入门go语言视频 - Day1
- 一、第一个 Go 程序
- 二、数据类型
- 三、基础数据类型
- 3.1 总述
- 3.2 浮点型
- 3.3 字符类型
- 3.4 字符串类型
- 3.5 字符和字符串的区别
- 3.6 bool类型
- 3.7 复数类型
- 3.8 类型转换
- 3.9 类型别名
- 3.10 格式化输出
- 3.11 非十进制可选前缀
- 3.12 `_` 增强数值的可读性
- 四、运算符
- 五、流程控制
- 六、值类型 && 引用类型
一、第一个 Go 程序
// 1.go语言以包作为管理单位(简单理解:一个文件夹就是一个 go 包)
// 2.每个go文件都必须在非注释的第一行代码中声明包名
// 3.一个文件夹(目录)下,只能有一个main包
// 4.main包中有且只有一个main()函数
package main // main 表示包的标识
import "fmt"
// 程序的入口,是从这里开始调用的
func main() {
fmt.Println("你好 go")
}
注意:关键字 import
后面的最后一个元素是目录名,而不是包名。Golang 编译器在这个路径下寻找包。
1.1 入口
一个 go 工程有且只有一个入口函数 main()
。
注意:main()
函数不能有任何参数和返回值。
1.2 Golang 保留的关键字
1.3 Golang预定义标识符
二、数据类型
类型表示同一类的数据,计算机用来计算,计算前需要把数据存储起来。
Go 有四类数据类型:
- 基本类型:数字、字符串、布尔。
- 聚合类型:数组、结构。
- 引用类型:指针、切片、映射、函数、通道。
- 接口类型:接口。
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.后赋值
例3:
使用具体值初始化变量时,编译器会自动推断其类型。
var (
firstName = "John"
lastName = "Doe"
age = 32
)
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。
注意:海象运算符 :=
前面,必须要有新的变量,不然就会报错。因为海象运算符的流程是先声明变量的类型,再给变量赋值(几乎同时发生),如果没有新的变量,那么就会变成重复声明了。另外,在函数外无法使用该特性,函数外声明变量必须使用 var
关键词。
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.函数外的每个语句都必须以关键字开始。(var
、const
、func
等)
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
遇到另一个 const
,iota
的值重新从 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 初始化常量不允许使用海象运算符:=
2.4.4.2 常量不允许被修改
下例中,对常量重新赋值就会报错:
2.4.4.3 常量必须给值
常量表示不可变的值,不给初始值,怎么让编译器去常量化呢?
三、基础数据类型
指定数据类型是告诉编译器,这个值需要分配多大的内存空间。
3.1 总述
3.1.1 Go 语言中的数据类型
以下是 Go 语言中的数据类型:
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
}
float64
比 float32
更加准确。使用自动推导类型的时候,浮点数会被推导成为 float64
类型。
3.3 字符类型
字符只是数值的特殊用例,Golang 使用数值来表示一个字符。
Golang 中,使用 byte
、int32
、rune
类型,来代表一个字符,一个字符由一对单引号 ''
包裹起来。
byte
类型(字节类型),它的本质是 uint8
类型,存储的时候会以一个 uint8
数值进行存储。
int32
类型,代表一个 UTF-8
字符。它还有另外一个书写方式:rune
,两者是完全等价的,只是书写方式的不同而已(就像本名和小名,int32
是本名,rune
是小名,用于表示 Unicode
字符)。
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
类型也可以书写成 rune
,rune
类型本质就是 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
就知道,最终显示数值。个人认为,int32
和 rune
的区别就是见名知意的作用:看它们的数据类型就知道了该数据的最终意图和呈现方式。
3.3.4.3 查看官方文档对他们的解释
在 Goland 中,按住 Ctrl
键,鼠标移动到 rune
这个类型上,就能看到 rune
的定义:
点击后,便能看到 byte
类型和 rune
类型的文档:
byte
类型的大意:byte
是 uint8
的别名,在所有方面都等同于 uint8
。按照惯例,它用于区分字节值和8 位无符号整数值。
rune
类型的大意:rune
是 int32
的别名,在所有方面都等同于 int32
。按照惯例,它用于区分字符值和整数值。
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 一对单引号 ''
中,只能放一个字符
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码参考表
https://zh.wikipedia.org/wiki/ASCII
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
数值转换为字符串后再打印。
注:Unicode
和 ASCII
一样,都是一种字符集,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]。
rune
是 int32
的别名,因此打印类型时,就显示了 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.Buffer
的 WriteString()
方法去拼接字符串,时间复杂度为 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 测试结果
测试环境:某度的云服务器,最低级的那种:1 核 CPU、1 G 内存。OS:ubuntu server 18.04 64bit。
测试 3 次后,得到结果如下:
3.4.7.9 总结
在确切知道有多少内容的情况下,可以提前将 append()
和 copy()
这两个内建函数的容量值(也就是 cap
),给申请下来,避免了 cap
的重复检查、扩容。
因此,在事先知道字符串的长度时:append()
的性能最高。copy()
的开销最少。
如果无法确定字符串内容的多少,最佳的方案就是 strings.Builder
的 WriteString()
方法。
3.4.7.10 参考文献
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 中,布尔类型的值只能是 true
或 false
。
一些脚本编程语言或是一些弱类型编程语言中,数值 0 或 1 、空数组、空集合等也可以用来表真或假。但在 Golang 中,这是不允许的!Golang 中,bool
值只能是 true
或 false
!
错误示例:
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() {
ch := 'a'
var i int
i = int(ch) //byte类型本质上就是一个uint8类型,和int兼容类型,所以可以相互转换
fmt.Printf("%%d:i=%d, %%c:i=%c, i type is : %T\n", i, i, i)
//%d:i=97, %c:i=a, i type is : int
}
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 格式化输出
参考网站:
https://godoc.org/fmt
%v
属于万能格式,自动匹配格式输出。
3.11 非十进制可选前缀
一个整数数值字面量无需带前缀,除非它是负数,或者自己想要添加一个加号。
非十进制可选前缀设置表示法:
二进制:0b
或 0B
八进制:0
, 0o
或 0O
十六进制:0x
或 0X
; 在十六进制中,[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 算术运算符
Golang 中自增自减的注意:
在 Golang 中,++
(自增)和 --
(自减)是单独的语句,并不是运算符。
而且自增自减必须是单独一行(单独的语句),下例中的自增是非法的:
i := 1
j = i++ //此处的 i++ 是非法的,i++ 必须是单独的语句
4.3 关系运算符
4.4 逻辑运算符
4.5 位运算符
位运算符是对整数在内存中的二进制位进行操作。
示例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 运算符优先级
优先级:从上往下由高到低
使用小括号()
把一个表达式包裹起来可以提升优先级。
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 类型的值 true
,true <= 10
。10是一个 int 类型,true 是 bool 类型,两者的类型在 golang 中不兼容,因此报错!
需要使用与
(&&
)运算符:
func main() {
a := 7
fmt.Println("0 <= a <= 10 的结果:", 0 <= a && a <= 10)
//0 <= a <= 10 的结果: true
}
4.9 运算符参考网站
https://www.runoob.com/go/go-operators.html
五、流程控制
对程序做出逻辑性控制。
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 {
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
分支中的值重复!并且,在 case
表达式中的值,必须与 swith
语句变量的类型一致,否则无法通过编译。
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 省略条件
在 Go 中,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("其他")
}
}
/*
运行结果:
良好
*/
注意:此种方式下,每一个 case
分支中表达式的最终值都必须是布尔值:
func main() {
score := 80
name := "aaa"
switch {
case score > 90:
fmt.Println("good")
//case score > 70:
// fmt.Println("fine")
case name > "aaaa":
fmt.Println("name is aaa")
case 1.0 > 0.9:
fmt.Println("bigger")
}
}
/*
运行结果:
bigger
*/
可以将上例中的 case
语句逐个注释、取消注释,来看运行结果。
但是,如果 case
分支的表达式中有非布尔类型的值,将无法通过编译。
反面案例:
func main() {
score := 80
name := "aaa"
level := 1
switch {
case score > 90:
fmt.Println("good")
//case score > 70:
// fmt.Println("fine")
case name > "aaaa":
fmt.Println("name is aaa")
case 1.0 > 0.9:
fmt.Println("bigger")
case level: // Invalid case 'level' in switch on 'true' (mismatched types 'int' and 'bool')
fmt.Println("level one.")
}
}
最后一个分支中的表达式的结果是一个 int
类型,无法与布尔类型匹配,无法通过编译!
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 基本语法
for 初始化条件;判断条件;条件变化 {
语句
}
5.4.2 基本流程
1.初始化条件。
2.判断条件是否为真,如果为真进入循环体内,如果为假则跳出循环。
3.执行条件变化的语句。
4.重复 2.3.4 步骤。
5.4.3 range迭代
一种自动实现的迭代器,常用于:数组、切片、通道。需要在 for
循环中使用。
range
有两个返回值:第一个返回值是元素的下标;第二个返回值是元素自身的值。
示例:
func main() {
str1 := "abc"
for i := range str1 { //第2个返回值会被丢弃,只取用第1个返回值(元素的下标)
fmt.Printf("str[%d]=%c\n", i, str1[i])
}
}
/*
运行结果:
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.4.4.3 水仙花数
打印出十进制 100-999 以内的“水仙花数”。
所谓“水仙花数”是指一个三位数,其各位数字立方之和等于该数本身。例如:153 是一个“水仙花数”,因为:153 = 1的三次方 + 5的三次方 + 3的三次方。
如下图所示:
//严格意义来说水仙花数指三位数
import (
"fmt"
"math"
)
//把这个三位数进行拆分,得到百位、十位、个位
func GetEveryNum(n int) []int {
sli := make([]int, 3)
sli[0] = n / 100 //得到百位的数字
sli[1] = n / 10 % 10 //得到十位的数字
sli[2] = n % 10 //得到个位的数字
return sli
}
//得到这个三位数,每个数的立方和
func GetMul(sli []int) int {
return GetCubeResult(sli[0], 3) + GetCubeResult(sli[1], 3) + GetCubeResult(sli[2], 3)
}
//计算立方的函数
func GetCubeResult(x, y int) int {
return int(math.Pow(float64(x), float64(y)))
}
func main() {
narcissisticNumbers := make([]int, 0) //不要指定len,不然默认会用
for i := 100; i < 1000; i++ {
sli := GetEveryNum(i)
mulResult := GetMul(sli)
if mulResult == i { //各位数字立方之和等于i,即表示是一个水仙花数
narcissisticNumbers = append(narcissisticNumbers, mulResult)
}
}
fmt.Println(narcissisticNumbers)
}
/*
运行结果:
[153 370 371 407]
*/
5.5 跳转语句
5.5.1 break
break
可以用在 for
、switch
、select
。
5.5.2 continue
continue
只能用在 for
循环中!
5.5.3 break和continue不能同时出现在同一级语句块中
break
和 continue
同时出现在同一级语句块中,就会自相矛盾,导致另一个语句无法到达!
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 //函数