1. 常量

Go语言中的常量使用关键字const定义,用于存储不会改变的数据,常量是在编译时被创建的,即使定义在函数内部也是如此,并且只能是布尔型数字型(整数型、浮点型和复数)和字符串型

由于编译时的限制,定义常量的表达式必须为能被编译器求值的常量表达式。

声明格式:

const name [type] = value
1

例如:

const pi = 3.14159
1

type可以省略

和变量声明一样,可以批量声明多个常量:

const (
    e  = 2.7182818
    pi = 3.1415926
)
1
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"
1
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
)
1
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
1
2
package main
import (
    "fmt"
)
func main() {
    var cat int = 1
    var str string = "码神之路"
    fmt.Printf("%p %p", &cat, &str)
}
1
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
1
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)
}
1
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(类型)
1
str := new(string)
*str = "码神之路Go语言教程"
fmt.Println(*str)
1
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)
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

3. 变量的生命周期

变量的生命周期指的是在程序运行期间变量有效存在的时间间隔。

变量的生命周期与变量的作用域有不可分割的联系:

  1. 全局变量:它的生命周期和整个程序的运行周期是一致的;
  2. 局部变量:它的生命周期则是动态的,从创建这个变量的声明语句开始,到这个变量不再被引用为止;
  3. 形式参数和函数返回值:它们都属于局部变量,在函数被调用的时候创建,函数调用结束后被销毁。

go的内存中应用了两种数据结构用于存放变量:

  1. 堆(heap):堆是用于存放进程执行中被动态分配的内存段。它的大小并不固定,可动态扩张或缩减。当进程调用 malloc 等函数分配内存时,新分配的内存就被动态加入到堆上(堆被扩张)。当利用 free 等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减);
  2. 栈(stack):栈又称堆栈, 用来存放程序暂时创建的局部变量,也就是我们函数的大括号{ }中定义的局部变量。

栈是先进后出,往栈中放元素的过程,称为入栈,取元素的过程称为出栈。

栈可用于内存分配,栈的分配和回收速度非常快

在程序的编译阶段,编译器会根据实际情况自动选择或者上分配局部变量的存储空间,不论使用 var 还是 new 关键字声明变量都不会影响编译器的选择。

var global *int
func f() {
    var x int
    x = 1
    global = &x
}
func g() {
    y := new(int)
    *y = 1
}
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
1
2

还有一种是类型定义:

//定义Name为Type类型 ,定义之后 Name为一种新的类型
type Name Type
1
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)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

5. 注释

Go语言的注释主要分成两类,分别是单行注释和多行注释。

  • 单行注释简称行注释,是最常见的注释形式,可以在任何地方使用以//开头的单行注释;
  • 多行注释简称块注释,以/*开头,并以*/结尾,且不可以嵌套使用,多行注释一般用于包的文档描述或注释成块的代码片段。

单行注释的格式如下所示

//单行注释
1

多行注释的格式如下所示

/*
第一行注释
第二行注释
...
*/
1
2
3
4
5

每一个包都应该有相关注释,在使用 package 语句声明包名之前添加相应的注释,用来对包的功能及作用进行简要说明。

同时,在 package 语句之前的注释内容将被默认认为是这个包的文档说明。一个包可以分散在多个文件中,但是只需要对其中一个进行注释说明即可。

6. 关键字和标识符

关键字

关键字即是被Go语言赋予了特殊含义的单词,也可以称为保留字。

Go语言中的关键字一共有 25 个:

breakdefaultfuncinterfaceselect
casedefergomapstruct
chanelsegotopackageswitch
constfallthroughifrangetype
continueforimportreturnvar

之所以刻意地将Go语言中的关键字保持的这么少,是为了简化在编译过程中的代码解析。

和其它语言一样,关键字不能够作标识符使用。

标识符

标识符是指Go语言对各种变量、方法、函数等命名时使用的字符序列,标识符由若干个字母、下划线_、和数字组成,且第一个字符必须是字母。

下划线_是一个特殊的标识符,称为空白标识符

标识符的命名需要遵守以下规则:

  • 由 26 个英文字母、0~9、_组成;
  • 不能以数字开头,例如 var 1num int 是错误的;
  • Go语言中严格区分大小写;
  • 标识符不能包含空格;
  • 不能以系统保留关键字作为标识符,比如 break,if 等等。

命名标识符时还需要注意以下几点:

  • 标识符的命名要尽量采取简短且有意义;
  • 不能和标准库中的包名重复;
  • 为变量、函数、常量命名时采用驼峰命名法,例如 stuName、getVal;

在Go语言中还存在着一些特殊的标识符,叫做预定义标识符,如下表所示:

appendboolbytecapclosecomplexcomplex64complex128uint16
copyfalsefloat32float64imagintint8int16uint32
int32int64iotalenmakenewnilpanicuint64
printprintlnrealrecoverstringtrueuintuint8uintptr

预定义标识符一共有 36 个,主要包含Go语言中的基础数据类型和内置函数,这些预定义标识符也不可以当做标识符来使用。

7. 运算符优先级

所谓优先级,就是当多个运算符出现在同一个表达式中时,先执行哪个运算符。

Go语言有几十种运算符,被分成十几个级别,有的运算符优先级不同,有的运算符优先级相同,请看下表。

优先级分类运算符结合性
1逗号运算符,从左到右
2赋值运算符=、+=、-=、*=、/=、 %=、 >=、 <<=、&=、^=、|=从右到左
3逻辑或||从左到右
4逻辑与&&从左到右
5按位或|从左到右
6按位异或^从左到右
7按位与&从左到右
8相等/不等==、!=从左到右
9关系运算符<、<=、>、>=从左到右
10位移运算符<<、>>从左到右
11加法/减法+、-从左到右
12乘法/除法/取余*(乘号)、/、%从左到右
13单目运算符!、*(指针)、& 、++、--、+(正号)、-(负号)从右到左
14后缀运算符( )、[ ]、->从左到右

注意:优先级值越大,表示优先级越高。

一下子记住所有运算符的优先级并不容易,还好Go语言中大部分运算符的优先级和数学中是一样的,大家在以后的编程过程中也会逐渐熟悉起来。如果实在搞不清,可以加括号,就像下面这样:

d := a + (b * c)
1

括号的优先级是最高的,括号中的表达式会优先执行,这样各个运算符的执行顺序就一目了然了。

8. 字符串与其他数据类型的转换

  1. 整数 与 字符串

    // 字符串与其他类型的转换
    // 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
  2. 浮点数 与字符串

    
        // 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
	}
}

1
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