这是我参与「第五届青训营 」伴学笔记创作活动的第 1 天
一、本堂课重点内容:
- Go 语言基础
- Go 语言开放入门,包括开发环境配置、基础语法、标准库
- Go 实战,包括三个实战项目
二、详细知识点介绍
1.1 什么是 Go 语言
Go语言其实是Golanguage的简称,Go(又称 Golang)是 Google 的 Robert Griesemer,Rob Pike 及 Ken Thompson 开发的一种静态强类型、编译并发型语言。
Go语言主要用在安全领域,它不仅是区块链最主流的编程语言,同时也是当前最具发展潜力的语言。此外它支持数据处理和大并发处理能力,所以也有很多公司开始大规模使用Golang作为主要开发语言。
简单总结一下:
- 高性能、高并发
- 语法简单、学习曲线平缓
- 丰富的标准库
- 完善的工具链
- 静态链接(在可执行文件之前就进行链接,不需要依赖库,程序可以独立执行)
- 快速编译
- 跨平台
- 垃圾回收(内存自动回收,不需要开发人员管理,防止造成内存泄露)
1.2 哪些公司在使用 Go 语言
2.1 开发环境 - 安装 Golang 并 配置集成开发环境
2.2 基础语法
常量与变量
//常量
const a = 1
const Pi float64 = 3.14159265358979323846 // 单行常量声明
// 以const代码块形式声明常量
const (
size int64 = 4096
i, j, s = 13, 14, "bar" // 单行声明多个常量
)
iota // go 中的特殊常量,常用于枚举
//变量
var a int // 显示指定类型,不进行初始化,值为类型的默认值
var a int = 3 // 显示指定类型,并进行初始化
var a = 3 // 不显示指定类型,具体类型由 GO 自行推断
// 整型值的默认类型 int
// 浮点值的默认类型为 float64
// 复数值的默认类型为 complex128
// 布尔值的默认类型是 bool
// 字符值默认类型是 rune
// 字符串值的默认类型是 string
a := 3 // 短变量声明,只允许出现在函数内部
// 变量声明块
var (
a int = 128
b int8 = 6
s string = "hello"
c rune = 'A'
t bool = true
)
// 一行中声明多个变量
var a, b, c int = 5, 6, 7
var (
a, b, c int = 5, 6, 7
c, d, e rune = 'C', 'D', 'E'
)
// 声明多个不同类型的变量
var a, b, c = 12, 'A', "hello"
a, b, c := 12, 'A', "hello" // 短变量声明方式
基本数据类型
- 布尔类型
bool,默认值FALSE - 字符串类型
string,默认"",字符串类型是不可变的,提高了字符串的并发安全性和存储利用率 - 字符类型
rune,存储的是Unicode字符 - 整数类型
int,默认值0 - 浮点数类型
float,默认值0.0 - 复数类型
complex
高级数据类型(默认值nil)
- 数组
array - 切片
slice - 字典
map - 通道
channel - 结构体
struct - 接口
interface - 指针
(*Xxx, unsafe.Pointer, uintptr) - 函数
function
数组
// N 必须是整型数字面值或常量表达式,其值必须是确定的
var arr [N]T
// a 与 b 是不同的类型
var a [3]int // 如果不进行显示初始化,那么值就是 0 值
var b [5]int
len(a) // len 函数用于计算数组的长度
unsafe.Sizeof(a) // 用于计算 a 所占空间大小
// 显示初始化数组的三种方式
var arr2 = [6]int {
11, 12, 13, 14, 15, 16,
} // [11 12 13 14 15 16]
var arr3 = [...]int { // 可以将 N 用 ... 替代,Go 会自己推算长度
21, 22, 23,
} // [21 22 23]
fmt.Printf("%T\n", arr3) // [3]int
var arr4 = [...]int{
99: 39, // 将第100个元素(下标值为99)的值赋值为39,其余元素值均为0
}
fmt.Printf("%T\n", arr4) // [100]int
// 数组的遍历
// 方法 1
for i := 0; i < len(arr); i++ {
// arr[i]
}
// 方法 2
for index, value := range arr {
}
// index 可省略
for _, value := range arr {
}
// 数组的截取
arr := [...]{1, 2, 3, 4, 5}
arr[1:2] // 2
arr[1:3] // 2, 3
arr[1:len(arr)] // 2,3,4,5
arr[1:] // 2,3,4,5
arr[:3] // 1,2,3
arr[:] // 1,2,3,4,5
Go 值传递的机制让数组在各个函数间传递起来比较“笨重”,开销较大,且开销随数组长度的增加而增加。为了解决这个问题,Go 引入了切片这一不定长同构数据类型。
切片
切片可以提供比指针更为强大的功能,比如下标访问、边界溢出校验、动态扩容等。而且,指针本身在 Go 语言中的功能也受到的限制,比如不支持指针算术运算。
type slice struct {
array unsafe.Pointer // 是指向底层数组的指针
len int // 是切片的长度,即切片中当前元素的个数,当访问下标大于等于 len 时的元素时,会报数组越界
cap int // 是底层数组的长度,也是切片的最大容量,cap 值永远大于等于 len 值
// 与数组的不同是,切片不需要显示的长度,但切片也有长度
// 切片的长度是变化的,可以用 len() 函数计算切片的长度
var n = []int{1, 2, 3, 4, 5}
// 使用 append 函数向切片中添加元素
n = append(n, 6) // 切片变为[1 2 3 4 5 6]
sl := make([]byte, 6, 10) // 其中10为cap值,即底层数组长度,6为切片的初始长度
// 切片中前 6 个元素的值是 byte 类型的零值,未初始化的元素不能访问
// 如果没有在 make 中指定 cap 参数,那么底层数组长度 cap 就等于 len
sl := make([]byte, 6) // cap = len = 6
// 数组的切片化
arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
sl := arr[3:7:9]
}
切片还具有动态扩容的能力,当通过append操作向切片追加数据的时候,如果这时切片的len值和cap值是相等的,也就是说切片底层数组已经没有空闲空间再来存储追加的值了,Go运行时就会对这个切片做扩容操作,来保证切片始终能存储下追加的新值。
动态扩容时,会新建一个更大的数组,append 会把旧数组中的数据拷贝到新数组中,之后新数组便成为了切片的底层数组,旧数组会被垃圾回收掉。
sl = append(s1, item) // 要接收返回值
字典
Go 中 map 的定义方式:
// 包含了 key 类型和 value 类型
map[key_type]value_type
如果两个 map 类型的 key 元素类型相同,value 元素类型也相同,那么我们可以说它们是同一个 map 类型,否则就是不同的 map 类型
map[string]string // key 与 value元素的类型相同
map[int]string // key 与 value元素的类型不同
map 类型对 value 的类型没有限制,但对 key 的类型却有严格要求,因为 map 类型要保证 key 的唯一性。Go 语言中要求,key 的类型必须支持==和!=两种比较操作符。
在 Go 语言中,函数类型、map 类型自身,以及切片只支持与 nil 的比较,而不支持同类型两个变量的比较。如下所示:
s1 := make([]int, 1)
s2 := make([]int, 2)
f1 := func() {}
f2 := func() {}
m1 := make(map[int]string)
m2 := make(map[int]string)
println(s1 == s2) // 错误:invalid operation: s1 == s2 (slice can only be compared to nil)
println(f1 == f2) // 错误:invalid operation: f1 == f2 (func can only be compared to nil)
println(m1 == m2) // 错误:invalid operation: m1 == m2 (map can only be compared to nil)
map 的声明
var m map[string]int // 一个 map[string]int 类型的变量
和切片类型变量一样,如果我们没有显式地赋予 map 变量初值,map 类型变量的默认值为 nil。初值为零值 nil 的切片类型变量,可以借助内置的 append 的函数进行操作,这种在 Go 语言中被称为“零值可用”。但 map 类型,因为它内部实现的复杂性,无法“零值可用”。所以,如果我们对处于零值状态的 map 变量直接进行操作,就会导致运行时异常(panic),从而导致程序进程异常退出
var m map[string]int // m = nil
m["key"] = 1 // 发生运行时异常:panic: assignment to entry in nil map
所以,必须对 map 类型变量进行显式初始化后才能使用。 map 的声明初始化 和切片一样,为 map 类型变量显式赋值有两种方式:
- 使用复合字面值
// 这里,显式初始化了 map 类型变量 m
// 此时 m 中没有任何键值对,但 m 也不等同于初值为 nil 的 map 变量
// 这时对 m 进行键值对的插入操作,不会引发运行时异常
m := map[int]string{} // 空 map
m1 := map[int]string{1:"a", 2:"b"} // 非空 map
- 使用 make 函数,通过 make 函数,可以指定 map 的初始容量,但无法进行具体的键值对赋值
m1 := make(map[int]string) // 未指定初始容量
m2 := make(map[int]string, 8) // 指定初始容量为 8
map 的常用操作
// map 的插入操作
m := make(map[int]string)
m[1] = "value1"
m[2] = "value2"
m[3] = "value3"
m[1] = "value5" // 会覆盖原来的 value1
len(m) // len 函数可以计算 map 的长度
// cap 函数不能用于 map 类型
// 使用 delete 函数进行删除
delete(m, "key1")
// 使用 range 进行遍历
for k, v := range m {
fmt.Printf("[%d, %d] ", k, v)
}
// 如果不关心值,也可以这样
for k, _ := range m {
// 使用k
}
// 只遍历 key
for k := range m {
// 使用k
}
// 只关心 value
for _, v := range m {
// 使用v
}
m := make(map[string]int)
// 获取 map 中的值
// 如果 key1 存在与 map 中,则返回其对应的 value
// 如果 key1 不存在,也不会报错,会返回 value 元素的 0 值
v := m["key1"]
// 下面方法可判断 key1 是否存在于 map 中
// 返回的 ok 是一个布尔类型
v, ok := m["key1"]
if !ok {
// "key1"不在map中
}
// "key1"在map中,v将被赋予"key1"键对应的value
// 如果不关心某个键对应的 value,而只关心某个键是否在于 map 中
// 我们可以使用空标识符替代变量 v,忽略可能返回的 value
_, ok := m["key1"]
对同一 map 做多次遍历的时候,每次遍历元素的次序都不相同。这时Go 语言 map 类型的一个重要特点,所以一定要记住:程序逻辑千万不要依赖遍历 map 所得到的元素次序。
map 实例不是并发写安全的,也不支持并发读写。如果对 map 实例进行并发读写,程序运行时就会抛出异常。
结构体
type 用于创建自定义数值类型。本质上相同的两个类型,它们的变量可以通过显式转型进行相互赋值。相反,如果本质上是不同的两个类型,它们的变量间连显式转型都不可能,更不要说相互赋值了。
// MyInt 的底层类型是 init32
// 它的数值性质与 int32 完全相同
// 但它们仍然是完全不同的两种类型
type MyInt int32
var m int = 5
var n int32 = 6
var a MyInt = m // 错误:在赋值中不能将m(int类型)作为MyInt类型使用
var a MyInt = n // 错误:在赋值中不能将n(int32类型)作为MyInt类型使用
// 需要经过显示转换
var a MyInt = MyInt(m) // ok
var a MyInt = MyInt(n) // ok
// MyInt 与 int32 完全等价,所以这个时候两种类型就是同一种类型
type MyInt = int32
var n int32 = 6
var a MyInt = n // ok
struct 定义结构体
// 自定义了 Person 类型
type Person struct {
Name string
Phone string
Addr string
}
// 自定义了 Book 类型
type Book struct {
Title string
Author Person // Person 类型为 Book 类型的内部元素
... ...
}
// 访问 Book 结构体字段 Author 中的 Phone 字段
var book Book
println(book.Author.Phone)
由于内存对齐的要求,结构体类型各个相邻字段间可能存在“填充物”,结构体的尾部同样可能被Go编译器填充额外的字节,满足结构体整体对齐的约束。正是因为这点,我们在定义结构体时,一定要合理安排字段顺序,要让结构体类型对内存空间的占用最小。
函数
func 函数名 (形参列表) (返回值类型列表) {
执行语句..
return + 返回值列表
}
复制代码
包的细节详解
- package 进行包的声明,建议:包的声明这个包和所在的文件夹同名
- main包是程序的入口包,一般main函数会放在这个包下。main函数一定要放在main包下,否则不能编译执行
通道
Go语言中的通道(channel)是一种特殊的类型。在任何时候,同时只能有一个 goroutine 访问通道进行发送和获取数据。goroutine 间通过通道就可以通信。
通道像一个队列,遵循先进先出。
channel是引用类型,必须make之后才能使用,一旦make,它的容量就确定了,不会动态增加。
package main
import "fmt"
func main(){
//定义变量
var c1 chan int
var i1 int
//初始化 channel
c1 = make(chan int, 100)
//向 channel c1 发送(写入)一个 int 20
c1 <- 20
//从 channel c1 接收(读取)一个 int 并赋值给 i1
i1 = <- c1
//将 i1 打印输出
fmt.Println("received: ", i1, " from c1")
}
错误处理
defer关键字。在函数中,程序员经常需要创建资源,为了在函数执行完毕后,及时的释放资源,Go的设计者提供defer关键字
package main
import "fmt"
func main() {
test()
fmt.Println("上面的语句执行成功")
}
func test() {
//利用defer+recover来捕获错误:defer后加上匿名函数的调用
defer func() {
//调用defer内置函数,可以捕获错误
err := recover();
//如果没有捕获错误,返回值为零值:nil
if err != nil {
fmt.Println("错误已经捕获")
fmt.Println("err是", err)
}
}()
num1 := 33
num2 := 66
result := num1 + num2
fmt.Println(result)
}
复制代码
package main
import (
"fmt"
"errors"
)
func main() {
err := test()
if err != nil {
fmt.Println("自定义错误:", err)
}
fmt.Println("上面的语句执行成功")
}
func test() (err error){
num1 := 10
num2 := 0
if num2 == 0 {
//抛出自定义错误:
return errors.New("除数不能为零!!!")
} else {
result := num1 / num2
fmt.Println(result)
//如果没有错误,返回零值
return nil
}
}
复制代码
三、实践练习例子
1. 猜谜游戏 - 生成随机数
(1)先写一个随机数生成的程序
package main
import (
"fmt"
"math/rand"
)
func main() {
maxNum := 100
secretNumber := rand.Intn(maxNum)
fmt.Println("The secret number is ", secretNumber)
}
(2)加入读取用户输入
package main
import (
"bufio"
"fmt"
"math/rand"
"os"
"strconv"
"strings"
"time"
)
func main() {
maxNum := 100
rand.Seed(time.Now().UnixNano())
secretNumber := rand.Intn(maxNum)
// fmt.Println("The secret number is ", secretNumber)
fmt.Println("Please input your guess")
reader := bufio.NewReader(os.Stdin)
for {
input, err := reader.ReadString('\n') // 读取一行输入
if err != nil {
fmt.Println("An error occured while reading input. Please try again", err)
// return
continue
}
input = strings.TrimSuffix(input, "\r\n") // 去掉换行符
guess, err := strconv.Atoi(input) // 转换成数字
if err != nil {
fmt.Println("Invalid input. Please enter an integer value")
// return
continue
}
fmt.Println("You guess is ", guess)
if guess > secretNumber {
fmt.Println("Your guess is bigger than the secret number. Please try again")
} else if guess < secretNumber {
fmt.Println("Your guess is smaller than the secret number. Please try again")
} else {
fmt.Println("Correct, you Legend!")
break
}
}
}
四、课后个人总结
本章最不容易理解的就是另外两个项目,这里有我很多不知道的知识和技术,所以理解起来有些困难。
由于Golang与C/C++非常类似,所以基础语法学起来还是相对简单,同时也经常会弄混淆。
接下来我应该主动学习,尝试做项目,去弥补自己空缺的部分。