GO语言基础 - 基础语法 | 青训营笔记

110 阅读4分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 1 天

一、本堂课重点内容:

  1. Go 语言基础
  2. Go 语言开放入门,包括开发环境配置、基础语法、标准库
  3. Go 实战,包括三个实战项目

二、详细知识点介绍

1.1 什么是 Go 语言

Go语言其实是Golanguage的简称,Go(又称 Golang)是 Google 的 Robert Griesemer,Rob Pike 及 Ken Thompson 开发的一种静态强类型、编译并发型语言。

Go语言主要用在安全领域,它不仅是区块链最主流的编程语言,同时也是当前最具发展潜力的语言。此外它支持数据处理和大并发处理能力,所以也有很多公司开始大规模使用Golang作为主要开发语言。

image.png

简单总结一下:

  • 高性能、高并发
  • 语法简单、学习曲线平缓
  • 丰富的标准库
  • 完善的工具链
  • 静态链接(在可执行文件之前就进行链接,不需要依赖库,程序可以独立执行)
  • 快速编译
  • 跨平台
  • 垃圾回收(内存自动回收,不需要开发人员管理,防止造成内存泄露)

1.2 哪些公司在使用 Go 语言

image.png

2.1 开发环境 - 安装 Golang 并 配置集成开发环境

Go语言中文网下载

VScode配置Go环境

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++非常类似,所以基础语法学起来还是相对简单,同时也经常会弄混淆。

接下来我应该主动学习,尝试做项目,去弥补自己空缺的部分。

五、引用参考

[GO语言基础] 一.为什么我要学习Golang以及GO语言入门普及

Golang基础教程

Golang介绍

Golang 学习笔记1:Go 基础