1. 常量
Go语言中的常量使用关键字const
定义,用于存储不会改变的数据,常量是在编译时被创建的,即使定义在函数内部也是如此,并且只能是布尔型
、数字型
(整数型、浮点型和复数)和字符串型
。
由于编译时的限制,定义常量的表达式必须为能被编译器求值的常量表达式。
声明格式:
const name [type] = value
例如:
const pi = 3.14159
type可以省略
和变量声明一样,可以批量声明多个常量:
const (
e = 2.7182818
pi = 3.1415926
)
2
3
4
所有常量的运算都可以在编译期完成,这样不仅可以减少运行时的工作,也方便其他代码的编译优化,当操作数是常量时,一些运行时的错误也可以在编译时被发现,例如整数除零、字符串索引越界、任何导致无效浮点数的操作等。
常量间的所有算术运算、逻辑运算和比较运算的结果也是常量,对常量的类型转换操作或以下函数调用都是返回常量结果:len、cap、real、imag、complex 和 unsafe.Sizeof。
因为它们的值是在编译期就确定的,因此常量可以是构成类型的一部分
如果是批量声明的常量,除了第一个外其它的常量右边的初始化表达式都可以省略,如果省略初始化表达式则表示使用前面常量的初始化表达式,对应的常量类型也是一样的。例如:
const (
a = 1
b
c = 2
d
)
fmt.Println(a, b, c, d) // "1 1 2 2"
2
3
4
5
6
7
1.1 iota 常量生成器
常量声明可以使用 iota 常量生成器初始化,它用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。
在一个 const 声明语句中,在第一个声明的常量所在的行,iota 将会被置为 0,然后在每一个有常量声明的行加1
比如,定义星期日到星期六,从0-6
const (
Sunday = iota //0
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday //6
)
2
3
4
5
6
7
8
9
2. 指针
指针(pointer)在Go语言中可以被拆分为两个核心概念:
- 类型指针,允许对这个指针类型的数据进行修改,传递数据可以直接使用指针,而无须拷贝数据,类型指针不能进行偏移和运算。
- 切片,由指向起始元素的原始指针、元素数量和容量组成。
受益于这样的约束和拆分,Go语言的指针类型变量即拥有指针高效访问的特点,又不会发生指针偏移,从而避免了非法修改关键性数据的问题。
同时,垃圾回收
也比较容易对不会发生偏移的指针进行检索和回收。
切片比原始指针具备更强大的特性,而且更为安全。
切片在发生越界时,运行时会报出宕机,并打出堆栈,而原始指针只会崩溃。
2.1 如何理解指针
var a int = 10
如果用大白话来解释上述语句:
在内存中开辟了一片空间,空间内存放着数值10,这片空间在整个内存当中,有一个唯一的地址,用来进行标识,指向这个地址的变量就称为指针
如果用类比的说明:
内存比作酒店,每个房间就是一块内存,上述代码表示为:定了一间房间a,让10住进了房间,房间有一个门牌号px,这个px就是房间的地址,房卡可以理解为就是指针,指向这个地址。
一个指针变量可以指向任何一个值的内存地址,它所指向的值的内存地址在 32 和 64 位机器上分别占用 4 或 8 个字节,占用字节的大小与所指向的值的大小无关。
当一个指针被定义后没有分配到任何变量
时,它的默认值为 nil
。
每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。
Go语言中使用在变量名前面添加&
操作符(前缀)来获取变量的内存地址(取地址操作),格式如下:
//其中 v 代表被取地址的变量,变量 v 的地址使用变量 ptr 进行接收,ptr 的类型为*T,称做 T 的指针类型,*代表指针。
ptr := &v // v 的类型为 T
2
package main
import (
"fmt"
)
func main() {
var cat int = 1
var str string = "码神之路"
fmt.Printf("%p %p", &cat, &str)
}
2
3
4
5
6
7
8
9
变量、指针和地址三者的关系是,每个变量都拥有地址,指针的值就是地址
当使用
&
操作符对普通变量进行取地址操作并得到变量的指针后,可以对指针使用*
操作符,也就是指针取值
// 指针与变量
var room int = 10 // room房间 里面放的 变量10
var ptr = &room // 门牌号px 指针 0xc00000a0a8
fmt.Printf("%p\n", &room) // 变量的内存地址 0xc00000a0a8
fmt.Printf("%T, %p\n", ptr, ptr) // *int, 0xc00000a0a8
fmt.Println("指针地址",ptr) // 0xc00000a0a8
fmt.Println("指针地址代表的值", *ptr) // 10
2
3
4
5
6
7
8
9
10
取地址操作符&
和取值操作符*
是一对互补操作符,&
取出地址,*
根据地址取出地址指向的值
变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:
- 对变量进行取地址操作使用
&
操作符,可以获得这个变量的指针变量。 - 指针变量的值是指针地址。
- 对指针变量进行取值操作使用
*
操作符,可以获得指针变量指向的原变量的值。
2.2 使用指针修改值
通过指针不仅可以取值,也可以修改值。
package main
func main(){
// 利用指针修改值
var num = 10
modifyFromPoint(num)
fmt.Println("未使用指针,方法外",num)
var num2 = 22
newModifyFromPoint(&num2) // 传入指针
fmt.Println("使用指针 方法外",num2)
}
func modifyFromPoint(num int) {
// 未使用指针
num = 10000
fmt.Println("未使用指针,方法内:",num)
}
func newModifyFromPoint(ptr *int) {
// 使用指针
*ptr = 1000 // 修改指针地址指向的值
fmt.Println("使用指针,方法内:",*ptr)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2.3 创建指针的另一种方法
Go语言还提供了另外一种方法来创建指针变量,格式如下:
new(类型)
str := new(string)
*str = "码神之路Go语言教程"
fmt.Println(*str)
2
3
new() 函数可以创建一个对应类型的指针,创建过程会分配内存,被创建的指针指向默认值。
2.4 指针小案例
获取命令行的输入信息
Go语言内置的 flag 包实现了对命令行参数的解析,flag 包使得开发命令行工具更为简单。
package main
// 导入系统包
import (
"flag"
"fmt"
)
// 定义命令行参数
var mode = flag.String("mode", "", "fast模式能让程序运行的更快")
func main() {
// 解析命令行参数
flag.Parse()
fmt.Println(*mode)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
3. 变量的生命周期
变量的生命周期指的是在程序运行期间变量有效存在的时间间隔。
变量的生命周期与变量的作用域有不可分割的联系:
- 全局变量:它的生命周期和整个程序的运行周期是一致的;
- 局部变量:它的生命周期则是动态的,从创建这个变量的声明语句开始,到这个变量不再被引用为止;
- 形式参数和函数返回值:它们都属于局部变量,在函数被调用的时候创建,函数调用结束后被销毁。
go的内存中应用了两种数据结构用于存放变量:
- 堆(heap):堆是用于存放进程执行中被动态分配的内存段。它的大小并不固定,可动态扩张或缩减。当进程调用 malloc 等函数分配内存时,新分配的内存就被动态加入到堆上(堆被扩张)。当利用 free 等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减);
- 栈(stack):栈又称堆栈, 用来存放程序暂时创建的局部变量,也就是我们函数的大括号
{ }
中定义的局部变量。
栈是先进后出,往栈中放元素的过程,称为入栈,取元素的过程称为出栈。
栈可用于内存分配,栈的分配和回收速度非常快
在程序的编译阶段,编译器会根据实际情况自动选择
在栈
或者堆
上分配局部变量的存储空间,不论使用 var 还是 new 关键字声明变量都不会影响编译器的选择。
var global *int
func f() {
var x int
x = 1
global = &x
}
func g() {
y := new(int)
*y = 1
}
2
3
4
5
6
7
8
9
10
上述代码中,函数 f 里的变量 x 必须在堆上分配,因为它在函数退出后依然可以通过包一级的 global 变量找到,虽然它是在函数内部定义的。
用Go语言的术语说,这个局部变量 x 从函数 f 中逃逸了。
相反,当函数 g 返回时,变量 y 不再被使用,也就是说可以马上被回收的。因此,y 并没有从函数 g 中逃逸,编译器可以选择在栈上分配 *y 的存储空间,也可以选择在堆上分配,然后由Go语言的 GC(垃圾回收机制)回收这个变量的内存空间。
4. 类型别名
类型别名是 Go 1.9 版本添加的新功能,主要用于解决代码升级、迁移中存在的类型兼容性问题。
格式:
//TypeAlias 只是 Type 的别名,本质上 TypeAlias 与 Type 是同一个类型,就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。
type TypeAlias = Type
2
还有一种是类型定义:
//定义Name为Type类型 ,定义之后 Name为一种新的类型
type Name Type
2
类型别名与类型定义表面上看只有一个等号的差异,那么它们之间实际的区别有哪些呢?
package main
import (
"fmt"
)
// 将NewInt定义为int类型
type NewInt int
// 将int取一个别名叫IntAlias
type IntAlias = int
func main() {
// 将a声明为NewInt类型
var a NewInt
// 查看a的类型名 main.NewInt
fmt.Printf("a type: %T\n", a)
// 将a2声明为IntAlias类型
var a2 IntAlias
// 查看a2的类型名 int
//IntAlias 类型只会在代码中存在,编译完成时,不会有 IntAlias 类型。
fmt.Printf("a2 type: %T\n", a2)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
5. 注释
Go语言的注释主要分成两类,分别是单行注释和多行注释。
- 单行注释简称行注释,是最常见的注释形式,可以在任何地方使用以
//
开头的单行注释; - 多行注释简称块注释,以
/*
开头,并以*/
结尾,且不可以嵌套使用,多行注释一般用于包的文档描述或注释成块的代码片段。
单行注释的格式如下所示
//单行注释
多行注释的格式如下所示
/*
第一行注释
第二行注释
...
*/
2
3
4
5
每一个包都应该有相关注释,在使用 package 语句声明包名之前添加相应的注释,用来对包的功能及作用进行简要说明。
同时,在 package 语句之前的注释内容将被默认认为是这个包的文档说明。一个包可以分散在多个文件中,但是只需要对其中一个进行注释说明即可。
6. 关键字和标识符
关键字
关键字即是被Go语言赋予了特殊含义的单词,也可以称为保留字。
Go语言中的关键字一共有 25 个:
break | default | func | interface | select |
---|---|---|---|---|
case | defer | go | map | struct |
chan | else | goto | package | switch |
const | fallthrough | if | range | type |
continue | for | import | return | var |
之所以刻意地将Go语言中的关键字保持的这么少,是为了简化在编译过程中的代码解析。
和其它语言一样,关键字不能够作标识符使用。
标识符
标识符是指Go语言对各种变量、方法、函数等命名时使用的字符序列,标识符由若干个字母、下划线_
、和数字组成,且第一个字符必须是字母。
下划线_
是一个特殊的标识符,称为空白标识符
标识符的命名需要遵守以下规则:
- 由 26 个英文字母、0~9、
_
组成; - 不能以数字开头,例如
var 1num int
是错误的; - Go语言中严格区分大小写;
- 标识符不能包含空格;
- 不能以系统保留关键字作为标识符,比如 break,if 等等。
命名标识符时还需要注意以下几点:
- 标识符的命名要尽量采取简短且有意义;
- 不能和标准库中的包名重复;
- 为变量、函数、常量命名时采用驼峰命名法,例如 stuName、getVal;
在Go语言中还存在着一些特殊的标识符,叫做预定义标识符,如下表所示:
append | bool | byte | cap | close | complex | complex64 | complex128 | uint16 |
---|---|---|---|---|---|---|---|---|
copy | false | float32 | float64 | imag | int | int8 | int16 | uint32 |
int32 | int64 | iota | len | make | new | nil | panic | uint64 |
println | real | recover | string | true | uint | uint8 | uintptr |
预定义标识符一共有 36 个,主要包含Go语言中的基础数据类型和内置函数,这些预定义标识符也不可以当做标识符来使用。
7. 运算符优先级
所谓优先级,就是当多个运算符出现在同一个表达式中时,先执行哪个运算符。
Go语言有几十种运算符,被分成十几个级别,有的运算符优先级不同,有的运算符优先级相同,请看下表。
优先级 | 分类 | 运算符 | 结合性 |
---|---|---|---|
1 | 逗号运算符 | , | 从左到右 |
2 | 赋值运算符 | =、+=、-=、*=、/=、 %=、 >=、 <<=、&=、^=、|= | 从右到左 |
3 | 逻辑或 | || | 从左到右 |
4 | 逻辑与 | && | 从左到右 |
5 | 按位或 | | | 从左到右 |
6 | 按位异或 | ^ | 从左到右 |
7 | 按位与 | & | 从左到右 |
8 | 相等/不等 | ==、!= | 从左到右 |
9 | 关系运算符 | <、<=、>、>= | 从左到右 |
10 | 位移运算符 | <<、>> | 从左到右 |
11 | 加法/减法 | +、- | 从左到右 |
12 | 乘法/除法/取余 | *(乘号)、/、% | 从左到右 |
13 | 单目运算符 | !、*(指针)、& 、++、--、+(正号)、-(负号) | 从右到左 |
14 | 后缀运算符 | ( )、[ ]、-> | 从左到右 |
注意:优先级值越大,表示优先级越高。
一下子记住所有运算符的优先级并不容易,还好Go语言中大部分运算符的优先级和数学中是一样的,大家在以后的编程过程中也会逐渐熟悉起来。如果实在搞不清,可以加括号,就像下面这样:
d := a + (b * c)
括号的优先级是最高的,括号中的表达式会优先执行,这样各个运算符的执行顺序就一目了然了。
8. 字符串与其他数据类型的转换
整数 与 字符串
// 字符串与其他类型的转换 // str 转 int newStr1 := "1" intValue, _ := strconv.Atoi(newStr1) fmt.Printf("%T,%d\n", intValue, intValue) // int,1 // int 转 str intValue2 := 1 strValue := strconv.Itoa(intValue2) fmt.Printf("%T, %s\n", strValue, strValue)
1
2
3
4
5
6
7
8
9
10浮点数 与字符串
// str 转 float string3 := "3.1415926" f,_ := strconv.ParseFloat(string3, 32) fmt.Printf("%T, %f\n", f, f) // float64, 3.141593 //float 转 string floatValue := 3.1415926 //4个参数,1:要转换的浮点数 2. 格式标记(b、e、E、f、g、G) //3. 精度 4. 指定浮点类型(32:float32、64:float64) // 格式标记: // ‘b’ (-ddddp±ddd,二进制指数) // ‘e’ (-d.dddde±dd,十进制指数) // ‘E’ (-d.ddddE±dd,十进制指数) // ‘f’ (-ddd.dddd,没有指数) // ‘g’ (‘e’:大指数,‘f’:其它情况) // ‘G’ (‘E’:大指数,‘f’:其它情况) // // 如果格式标记为 ‘e’,‘E’和’f’,则 prec 表示小数点后的数字位数 // 如果格式标记为 ‘g’,‘G’,则 prec 表示总的数字位数(整数部分+小数部分) formatFloat := strconv.FormatFloat(floatValue, 'f', 2, 64) fmt.Printf("%T,%s",formatFloat,formatFloat)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
9. 练习:开发一款游戏
//捕获标准输入,并转换为字符串 reader := bufio.NewReader(os.Stdin) input, err := reader.ReadString('\n')
if err != nil {
//如果有错误 退出
panic(err) }
需求:能打怪升级
package main
import (
"bufio"
"fmt"
"os"
)
var level = 1
var ex = 0
func main() {
fmt.Println("请输入你的角色名字")
//捕获标准输入,并转换为字符串
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')
if err != nil {
panic(err)
}
//删除最后的\n
name := input[:len(input)-1]
fmt.Printf("角色创建成功,%s,欢迎你来到码神游戏,目前角色等级%d \n",name,level)
s := `你遇到了一个怪物,请选择是战斗还是逃跑?
1.战斗
2.逃跑`
fmt.Printf("%s \n",s)
for {
input, err := reader.ReadString('\n')
if err != nil {
panic(err)
}
selector := input[:len(input)-1]
switch selector {
case "1":
ex += 10
fmt.Printf("杀死了怪物,获得了%d经验 \n",ex)
computeLevel()
fmt.Printf("您现在的等级为%d \n",level)
case "2":
fmt.Printf("你选择了逃跑\n")
fmt.Printf("%s \n",s)
case "exit":
fmt.Println("你退出了游戏")
//退出
os.Exit(1)
default:
fmt.Println("你的输入我不认识,请重新输入")
}
}
}
func computeLevel() {
if ex < 20 {
level = 1
}else if ex < 40{
level = 2
}else if ex < 200{
level = 3
}else {
level = 4
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62