Go语言入门与基础学习笔记——从HelloWorld到Go并发
0 写在前面
所有学习内容来自开源社区或资源
来源:
极客兔兔 https://geektutu.com/
Go菜鸟教程 https://www.runoob.com/go/
狂神说 https://www.kuangstudy.com/
1 学前工作
1.1 下载安装Go
Go 语言中文网https://studygolang.com/dl
选择自己操作系统相关的安装包
1.2 配置安装目录环境变量
下载安装后打开cmd
并输入go version
出现版本信息表示安装成功,否则需要配置系统环境变量,安装到哪个文件夹就将哪个文件夹添加进系统变量
1.3 配置工作目录环境变量
配置安装环境变量之后,Go还需要一个安装目录,即GOROOT和GOPATH
创建文件夹GoWorks
并配置GOPATH和GOROOT系统变量
然后在文件夹GoWorks
下创建三个新目录分别为 src
、pkg
和bin
1.4 检查前置步骤是否生效
打开cmd
输入go env
查看 GOPATH
和GOROOT
为更改后的文件,否则需要重新修改系统的环境变量
1.5 下载和安装GoLand
https://www.jetbrains.com/go/download/download-thanks.html
安装过程略
1.6 Hello,World
package main
import "fmt"
func main() {
fmt.Println("Hello,World!") // 打印输出Helloworld字符串
}
2 基础语法
2.1 注释
package main
import "fmt"
// 我是单行注释
/*
多行注释
多行注释
多行注释
多行注释
*/
func main() {
fmt.Println("Hello,World!") // 打印输出Helloworld字符串
}
2.2 变量
package main
import "fmt"
func main() {
var name string = "tylers"
name = "zhangsan"
var (
name string
age int
addr string
)
//string 默认值 空
// int 默认值 0
fmt.Println(name) // zhangsan
}
2.3 内存地址
func main() {
var num int
num = 100
//string 默认值 空
// int 默认值 0
fmt.Println("num:%d, 内存地址:%p", num, &num) // 取地址符 &变量名
num = 200
fmt.Println("num:%d, 内存地址:%p", num, &num) // 取地址符 &变量名
}
//内存地址不变
2.4 变量的交换
package main
import "fmt"
func main() {
var a int = 100
var b int = 200
b, a = a, b
fmt.Println(a, b)
}
2.5 函数的调用和返回
package main
import "fmt"
func test() (int, int) {
return 100, 200
}
func main() {
a, b := test()
fmt.Println(a, b)
}
2.6 匿名变量
package main
import "fmt"
func test() (int, int) {
return 100, 200
}
func main() {
a, _ := test()
_, b := test()
fmt.Println(a)
fmt.Println(b)
}
2.7 变量的作用域
package main
import (
"fmt"
)
var name string = "kuangshen"
func main() {
// = 赋值, 就是将右边的值,赋给左边的变量
// name 变量
// 局部变量
// 就近原则使用变量
var age int = 18
var name string = "kuangshen333"
fmt.Println(name, age)
}
func aaa() {
fmt.Println(name)
}
2.8 常量
package main
import "fmt"
func main() {
const URL string = "www.baidu.com" // 显式定义
const URL2 = "www.baidu.com" // 隐式定义
const a, b, c = 3.14, "kuangshen", false // 同时定义多个常量
fmt.Println(URL)
fmt.Println(URL2)
fmt.Println(a, b, c)
}
2.9 iota
package main
import "fmt"
func main() {
const (
a = iota // iota 0
b = iota // iota 1
c = iota // iota 2
d = "hahaha" // "hahaha" iota 3
e // "hahaha" iota 4
f = 100 // 100 iota 5
g // 100 iota 6
h = iota // iota 7
i // iota 8
)
fmt.Println(a, b, c, d, e, f, g, h, i)
}
package main
import "fmt"
func main() {
const (
a = iota // 0
b = iota
c = iota
d = "hahaha"
e
f = 100
g
h = iota
i
)
const (
j = iota // 0
k // 1
)
fmt.Println(a, b, c, d, e, f, g, h, i)
fmt.Println(j, k)
}
3 基本数据类型
3.1 布尔类型
package main
import "fmt"
func main() {
// var 变量名 数据类型
// bool : true false
// bool 默认值为 false
var isFlag bool = true
var isFlag2 bool
fmt.Println(isFlag) // true
fmt.Println(isFlag2) // false
fmt.Printf("%T, %t\n", isFlag, isFlag) // bool, true
}
3.2 数字型
package main
import "fmt"
func main() {
// 定义一个整形
var age int = 18
// 定义一个浮点型
// 默认是6位小数打印 3.140000
// 丢失精度时“五舍六入”
var money float64 = 3.16
fmt.Printf("%T, %d\n", age, age)
fmt.Printf("%T, %.1f\n", money, money)
}
// float运算尾数部分可能丢失, 造成精度损失, -123.00000901
// 尽量使用float64 不要使用 float32
// 数据类型:内存空间分配的大小是不同的
// float64 尽量使用float64 来定义浮点类型的小数
var num1 float32 = -123.0000901 // num1 = -123.00009
var num2 float64 = -123.0000901 // num2 = -123.0000901
fmt.Println("num1 = ", num1, "num2 = ", num2)
3.3 字符串
package main
import "fmt"
func main() {
var str string
str = "Hello, zhangleyao"
fmt.Printf("%T, %s\n", str, str)
// 单引号
v1 := 'A'
v2 := "A"
// 编码表 ASCII字符吗
// 扩展:
// 所有的中国字的编码表:GBK
// 全世界的编码表:Unicode编码表
fmt.Printf("%T, %c\n", v1, v1)
fmt.Printf("%T, %s\n", v2, v2)
// 字符串连接 +
fmt.Println("hello" + ",xuexiangban")
// 转义字符 \
fmt.Println("hello\"xuexiangban")
}
3.4 数据类型转换
package main
import "fmt"
// 类型转换
// 转换后的变量 := 要转换的类型(变量)
// 备注:整型是不能转化为bool类型的
func main() {
a := 3 // int
b := 5.0 // float64
// 需求: 将int类型的a转换为 float64 类型 类型转换
c := float64(a)
d := int(a)
// bool 整型是不能转化为bool类型的
//e := bool(a)
fmt.Printf("%T\n", a)
fmt.Printf("%T\n", b)
fmt.Printf("%T\n", c)
fmt.Printf("%T\n", d)
//fmt.Printf("%T\n", e)
}
3.5 补充:数组(array)和切片(slice)
声明数组
var arr [5]int // 一维
var arr2 [5][5]int // 二维
声明时初始化
var arr = [5]int{1, 2, 3, 4, 5}
// 或 arr := [5]int{1, 2, 3, 4, 5}
使用[]
索引/修改数组
arr := [5]int{1, 2, 3, 4, 5}
for i:= 0;i < len(arr); i++{
arr[i] += 100
}
fmt.Println(arr) // [101 102 103 104 105]
数组的长度不能改变,如果想拼接2个数组,或是获取子数组,需要使用切片。切片是数组的抽象。 切片使用数组作为底层结构。切片包含三个组件:容量,长度和指向底层数组的指针,切片可以随时进行扩展
声明切片:
slice := make([]float32, 0) // 长度为0的切片
slice2 := make([]float32, 3, 5) // [0 0 0] 长度为3容量为5的切片
fmt.Println(len(slice2), cap(slice2)) // 3 5
// 声明切片的另一种方式 [] 中不写任何值
slice3 := []float32{1, 2, 3}
使用切片:
// 添加元素,切片容量可以根据需要自动扩展
slice2 = append(slice2, 1, 2, 3, 4) // [0, 0, 0, 1, 2, 3, 4]
fmt.Println(len(slice2), cap(slice2)) // 7 12
// 子切片 [start, end) 左闭右开
sub1 := slice2[3:] // [1 2 3 4]
sub2 := slice2[:3] // [0 0 0]
sub3 := slice2[1:4] // [0 0 1]
// 合并切片
combined := append(sub1, sub2...) // [1, 2, 3, 4, 0, 0, 0]
- 声明切片时可以为切片设置容量大小,为切片预分配空间。在实际使用的过程中,如果容量不够,切片容量会自动扩展。
sub2...
是切片解构的写法,将切片解构为 N 个独立的元素。
3.6 补充:字典(键值对, map)
map 类似于 java 的 HashMap,Python的字典(dict),是一种存储键值对(Key-Value)的数据结构。使用方式和其他语言几乎没有区别
// 仅声明
m1 := make(map[string]int)
// 声明时初始化
m2 := map[string]string{
"Sam": "Male",
"Alice": "Female",
}
// key - value
m3 := map[int]string{
1: "zhangleyao",
}
// 赋值/修改
m1["Tom"] = 18
3.7 指针(pointer)
指针即某个值的地址,类型定义时使用符号*
,对一个已经存在的变量,使用 &
获取该变量的地址。
str := "Golang"
var p *string = &str // p 是指向 str 的指针
*p = "Hello"
fmt.Println(str) // Hello 修改了 p,str 的值也发生了改变
一般来说,指针通常在函数传递参数,或者给某个类型定义新的方法时使用。Go 语言中,参数是按值传递的,如果不使用指针,函数内部将会拷贝一份参数的副本,对参数的修改并不会影响到外部变量的值。如果参数使用指针,对参数的传递将会影响到外部变量。
例如:(同C++)
func add(num int) {
num += 1
}
func realAdd(num *int) {
*num += 1
}
func main() {
num := 100
add(num)
fmt.Println(num) // 100,num 没有变化
realAdd(&num)
fmt.Println(num) // 101,指针传递,num 被修改
}
3.8 结构体
Go 语言中数组可以存储同一类型的数据,但在结构体中我们可以为不同项定义不同的数据类型。
结构体是由一系列具有相同类型或不同类型的数据构成的数据集合。
结构体表示一项记录,比如保存图书馆的书籍记录,每本书有以下属性:
- Title :标题
- Author : 作者
- Subject:学科
- ID:书籍ID
定义结构体
结构体定义需要使用 type 和 struct 语句。struct 语句定义一个新的数据类型,结构体中有一个或多个成员。type 语句设定了结构体的名称。结构体的格式如下:
type struct_variable_type struct {
member definition
member definition
...
member definition
}
一旦定义了结构体类型,它就能用于变量的声明,语法格式如下:
variable_name := structure_variable_type {value1, value2...valuen}
或
variable_name := structure_variable_type { key1: value1, key2: value2..., keyn: valuen}
package main
import "fmt"
type Books struct {
title string
author string
subject string
book_id int
}
func main() {
// 创建一个新的结构体
fmt.Println(Books{"Go 语言", "www.runoob.com", "Go 语言教程", 6495407})
// 也可以使用 key => value 格式
fmt.Println(Books{title: "Go 语言", author: "www.runoob.com", subject: "Go 语言教程", book_id: 6495407})
// 忽略的字段为 0 或 空
fmt.Println(Books{title: "Go 语言", author: "www.runoob.com"})
}
访问结构体成员
如果要访问结构体成员,需要使用点号 . 操作符,格式为:
结构体.成员名"
package main
import "fmt"
type Books struct {
title string
author string
subject string
book_id int
}
func main() {
var Book1 Books /* 声明 Book1 为 Books 类型 */
var Book2 Books /* 声明 Book2 为 Books 类型 */
/* book 1 描述 */
Book1.title = "Go 语言"
Book1.author = "www.runoob.com"
Book1.subject = "Go 语言教程"
Book1.book_id = 6495407
/* book 2 描述 */
Book2.title = "Python 教程"
Book2.author = "www.runoob.com"
Book2.subject = "Python 语言教程"
Book2.book_id = 6495700
/* 打印 Book1 信息 */
fmt.Printf( "Book 1 title : %s\n", Book1.title)
fmt.Printf( "Book 1 author : %s\n", Book1.author)
fmt.Printf( "Book 1 subject : %s\n", Book1.subject)
fmt.Printf( "Book 1 book_id : %d\n", Book1.book_id)
/* 打印 Book2 信息 */
fmt.Printf( "Book 2 title : %s\n", Book2.title)
fmt.Printf( "Book 2 author : %s\n", Book2.author)
fmt.Printf( "Book 2 subject : %s\n", Book2.subject)
fmt.Printf( "Book 2 book_id : %d\n", Book2.book_id)
}
结构体作为函数参数
package main
import "fmt"
type Books struct {
title string
author string
subject string
book_id int
}
func main() {
var Book1 Books /* 声明 Book1 为 Books 类型 */
var Book2 Books /* 声明 Book2 为 Books 类型 */
/* book 1 描述 */
Book1.title = "Go 语言"
Book1.author = "www.runoob.com"
Book1.subject = "Go 语言教程"
Book1.book_id = 6495407
/* book 2 描述 */
Book2.title = "Python 教程"
Book2.author = "www.runoob.com"
Book2.subject = "Python 语言教程"
Book2.book_id = 6495700
/* 打印 Book1 信息 */
printBook(Book1)
/* 打印 Book2 信息 */
printBook(Book2)
}
func printBook( book Books ) {
fmt.Printf( "Book title : %s\n", book.title)
fmt.Printf( "Book author : %s\n", book.author)
fmt.Printf( "Book subject : %s\n", book.subject)
fmt.Printf( "Book book_id : %d\n", book.book_id)
}
结构体指针
你可以定义指向结构体的指针类似于其他指针变量,格式如下:
var struct_pointer *Books
以上定义的指针变量可以存储结构体变量的地址。查看结构体变量地址,可以将 & 符号放置于结构体变量前:
struct_pointer = &Book1
使用结构体指针访问结构体成员,使用 “.” 操作符:
struct_pointer.title
package main
import "fmt"
type Books struct {
title string
author string
subject string
book_id int
}
func main() {
var Book1 Books /* 声明 Book1 为 Books 类型 */
var Book2 Books /* 声明 Book2 为 Books 类型 */
/* book 1 描述 */
Book1.title = "Go 语言"
Book1.author = "www.runoob.com"
Book1.subject = "Go 语言教程"
Book1.book_id = 6495407
/* book 2 描述 */
Book2.title = "Python 教程"
Book2.author = "www.runoob.com"
Book2.subject = "Python 语言教程"
Book2.book_id = 6495700
/* 打印 Book1 信息 */
printBook(&Book1)
/* 打印 Book2 信息 */
printBook(&Book2)
}
func printBook( book *Books ) {
fmt.Printf( "Book title : %s\n", book.title)
fmt.Printf( "Book author : %s\n", book.author)
fmt.Printf( "Book subject : %s\n", book.subject)
fmt.Printf( "Book book_id : %d\n", book.book_id)
}
4 运算符
4.1 算术运算符
package main
import "fmt"
func main() {
var a int = 10
var b int = 3
//var c int
// + - * / % ++ --
fmt.Println(a + b)
fmt.Println(a - b)
fmt.Println(a * b)
fmt.Println(a % b)
a++ // a = a + 1
fmt.Println(a)
a-- // a = a - 1
fmt.Println(a)
}
4.2 关系运算符
package main
import "fmt"
func main() {
var a int = 11
var b int = 10
// == 等于 = 赋值
// 类型运算符 结果都是 bool
fmt.Println(a == b) // 等于
fmt.Println(a != b) // 不等于
fmt.Println(a > b)
fmt.Println(a < b)
fmt.Println(a >= b)
fmt.Println(a <= b)
// 判断 if 如果 。。。 结果
if a > b {
// 执行一些 a>b 的操作
} else { // else 否则
}
}
4.3 逻辑运算符
4.4 位运算符(二进制)
4.5 赋值运算符
4.6 其他运算符
4.7 拓展:键盘输入输出
package main
import "fmt"
func main() {
var x int
var y float64
// 定义了两个变量,想用键盘来录入这两个变量
//fmt.Println() // 打印并换行
//fmt.Printf() // 格式化输出
//fmt.Print() // 打印输出
fmt.Scanln(&x)
//fmt.Scanln(&x)// 接收输入 Scanl
//fmt.Scanf() // 接收输入 格式化输入 作业
//fmt.Scan() // 接收输入 作业
fmt.Println(x)
}
5 流程控制
5.1 if语句
if a > 20 {
} // 和python一样不需要括号
else if a > 10{
}
else {
}
5.2 switch
//和C语言一样
package main
import "fmt"
func main() {
var a int = 90
// 匹配 case
switch a {
case 90:
fmt.Println("A")
case 80:
fmt.Println("B")
case 50, 60, 70:
fmt.Println("C")
default:
fmt.Println("D")
}
// switch 默认的条件 bool = true
switch {
case false:
fmt.Println("false")
case true:
fmt.Println("true")
default:
fmt.Println("其他")
}
}
5.3 fallthrough穿透
package main
import "fmt"
func main() {
a := false
switch a {
case false:
fmt.Println("1. case条件为false")
fallthrough // case 穿透的,不管下一个条件满不满足, 都会执行
case true:
if a == false {
break // 终止case穿透
}
fmt.Println("2. case条件为true")
}
}
5.4 for
package main
import "fmt"
func main() {
// for 条件的起始值:循环条件:控制变量自增或者自减
// for : 循环条件
// for {} 无限循环
//循环10次
i := 0
for i < 10 {
i++
fmt.Println(i)
}
for {
fmt.Println(i)
i++
}
// 计算 1 到 10 的数字之和:
//sum := 0
//for i := 0; i < 1000; i++ {
// sum += i
// fmt.Println(sum)
//}
}
5.5 string
package main
import "fmt"
func main() {
// utf-8编码
var str string = "hello, zhangleyao"
fmt.Println(str)
// 获取字符串的长度 len
fmt.Println("字符串的长度为: ", len(str))
// 获取指定的字节
fmt.Println("字节打印: ", str[0])
fmt.Printf("%c\n", str[0])
// for
//for i := 0; i < len(str); i++ {
// fmt.Printf("%c\n", str[i])
//}
//for range 循环 , 遍历数组、切片
// 返回下标和对应的值,使用这个值就可以了
for i, v := range str {
fmt.Print(i)
fmt.Printf(" %c ", v)
}
}
6 函数
6.1 什么是函数
package main
import "fmt"
/*
- 函数是基本的代码块,用于执行一个任务。
- G0 语言最少有个 main() 函数。
- 你可以通过函数来划分不同功能。逻辑上每个函数执行的是指定的任务。
- 函数声明告诉了编译器函数的名称,返回类型,参数。
*/
func main() {
fmt.Println("hello, world")
// 调用函数 函数名()
fmt.Println(add(1, 2))
}
// func 函数名 (参数, 参数 ...) 函数调用后的返回值{
// 函数体 : 执行一段代码
// return 返回结果
func add(a, b int) int {
c := a + b
return c
}
6.2 函数的声明
- 无参无返回值的函数
- 有一个参数的函数
- 有两个参数的函数
- 有一个返回值的函数
- 有多个返回值的函数
package main
import (
"fmt"
"strconv"
)
func main() {
printinfo()
myprint("printinfo")
// 有返回值函数,我们就需要接收函数的返回值
c := add2(1, 2)
myprint(strconv.Itoa(c))
x, y := swap("123", "321")
fmt.Println(x, y)
}
func printinfo() {
fmt.Println("printinfo")
}
func myprint(msg string) {
fmt.Println(msg)
}
// 有两个参数的函数
// 有一个返回值的函数
func add2(a, b int) int {
c := a + b
return c
}
// 有多个返回值的函数
func swap(x, y string) (string, string) {
return y, x
}
6.3 形式参数和实际参数
package main
func main() {
max(1, 2)
}
// max 两个数字比大小
// 形式参数: 定义函数时,用来接收外部传入数据的参数,就是形式参数
// 实际参数: 调用函数时,传给形参的实际数据叫做实际参数
func max(num1, num2 int) int {
var result int
if num1 > num2 {
result = num1
} else {
result = num2
}
return result
}
6.4 值传递
package main
import "fmt"
func main() {
getSum("haha", 2, 3, 4, 5, 6)
}
// ...可变参数
func getSum(msg string, nums ...int) {
sum := 0
for i := 0; i < len(nums); i++ {
fmt.Println(nums[i])
sum += nums[i]
}
fmt.Println("sum:", sum)
}
6.5 可变参数
package main
import "fmt"
func main() {
getSum("haha", 2, 3, 4, 5, 6)
}
// ...可变参数
func getSum(msg string, nums ...int) {
sum := 0
for i := 0; i < len(nums); i++ {
fmt.Println(nums[i])
sum += nums[i]
}
fmt.Println("sum:", sum)
}
6.6 值传递
package main
import "fmt"
func main() {
// 值传递
// arr2的数据是从arr1 复制来的,所以是不同的空间
// 修改 arr2 并不会影响 arr1
// 值传递,传递的是数据的副本,修改数据,对于原始的数据没有影响
// 值类型的数据,默认的是指传递:基础类型,array、struct
// 定义一个数组 [个数]类型
arr := [4]int{1, 2, 3, 4}
fmt.Println(arr)
// 传递, 拷贝arr
update(arr)
fmt.Println("调用修改后的数据: ", arr)
// 引用传递
}
func update(arr2 [4]int) {
fmt.Println("arr2接收的数据:", arr2)
arr2[0] = 100
fmt.Println("arr2修改后的数据", arr2)
}
6.7 引用传递
package main
import "fmt"
// 引用传递
func main() {
// 切片,可以扩容的数组
s1 := []int{1, 2, 3, 4}
fmt.Println("默认的数据", s1)
// 传入的是引用类型的数据,地址
update2(s1)
fmt.Println("调用后的数据", s1)
}
func update2(s2 []int) {
fmt.Println("传递的数据", s2)
s2[0] = 100
fmt.Println("修改后的数据", s2)
}
6.8 函数变量的作用域
同C++和Python
package main
import "fmt"
// 全局变量
var name int = 100
func main() {
// 函数体内的局部变量
temp := 100
// if语句、 for语句定义的一次性变量局部变量
if b := 1; b <= 10 {
// 语句内的局部变量
temp := 50
fmt.Println(temp) // 局部变量,就近原则
fmt.Println(b)
}
fmt.Println(temp)
//fmt.Println(b)
f1()
f2()
}
func f1() {
a := 1
fmt.Println(a)
fmt.Println(name)
}
func f2() {
//
fmt.Println(name)
}
6.9 递归函数
同C++、python
package main
import "fmt"
func main() {
fmt.Println(sum(100))
}
func sum(a int) int {
if a != 0 {
return a + sum(a-1)
}
return 0
}
6.10 defer延迟函数执行
主要用来进行关闭操作
package main
import "fmt"
// defer 关闭操作
func main() {
a := 10
fmt.Println("a=", a)
defer f(a) // 参数就已经传递进去了,在最后执行
a++
fmt.Println("end a=", a)
}
func f(s int) {
fmt.Println("函数里面的a=", s)
}
6.11 高级:函数的本质和数据类型
函数是个地址,可以像Python一样引用函数
package main
import "fmt"
// func() 本身就是一个数据类型
func main() {
// f1 如果不加括号,函数就是一个变量
// f1() 如果加了括号那就成了函数的调用
fmt.Printf("%T\n", f3) // func() | func(int, int) | func(int, int) int
//fmt.Printf("%T", 10) // int
//fmt.Printf("%T", "hello") // string
var f5 func(int, int)
f5 = f3
f5(1, 2)
}
func f3(a, b int) {
fmt.Println(a, b)
}
6.12 高级:匿名函数推导
package main
import (
"fmt"
)
func main() {
f5()
f2 := f5 // 函数本身也是一个变量
f2()
f3 := func() {
fmt.Println("我是f3函数")
}
f3()
func() {
fmt.Println("我是匿名函数")
}()
func(a, b int) {
fmt.Println(a, b)
fmt.Println("我是匿名函数2")
}(1, 2)
r1 := func(a, b int) int {
fmt.Println(a, b)
fmt.Println("我是匿名函数2")
return a + b
}(1, 2)
fmt.Println(r1)
}
func f5() {
fmt.Println("我是f5函数")
}
// go语言可以传入func()
func f6(f func()) {
}
Go语言是支持函数式编程:
1、 将匿名桉树作为另一个函数的参数,回调函数
2、 将匿名函数作为另外一个函数的返回值,可以形成闭包结构
6.13 高级:函数式编程
高阶函数:根据go语言的数据类型的特点,可以将一个函数作为另外一个函数的参数
fun1(),fun2()
将 fun1 函数作为 fun2 这个函数的参数
fun2函数:就叫做高阶函数,接收了一个函数作为参数的函数
fun1函数:就叫做回调函数,作为另外个函数的参数
package main
import "fmt"
func main() {
r1 := add3(1, 2)
fmt.Println(r1)
r2 := oper(3, 4, add3)
fmt.Println(r2)
r3 := oper(8, 4, sub)
fmt.Println(r3)
// 可以直接传入匿名函数
r4 := oper(8, 4, func(a, b int) int {
if b == 0 {
fmt.Println("除数不能为0")
return 0
} else {
return a / b
}
})
fmt.Println(r4)
}
// 高阶函数
func oper(a, b int, fun func(int, int) int) int {
r := fun(a, b)
return r
}
func add3(a, b int) int {
return a + b
}
func sub(a, b int) int {
return a - b
}
6.14 闭包结构的理解
package main
import "fmt"
/*
一个外层函数中,有内层函数,该内层函数中,会操作外层函数的局部变量
并且该外层函数的返回值就是这个内层函数。
这个内层函数和外层函数的局部变量,统称为闭包结构 在本例中指的是i这个变量
局部变量的生命周期就会发生改变,正常的局部变量会随着函数的调用而创建,随着函数的结束而销毁
但是闭包结构中的外层函数的局部变量并不会随着外层函数的结束而销毁,因为内层函数还在继续使用
*/
func main() {
r1 := increment()
fmt.Printf("%p", &r1)
v1 := r1()
fmt.Println(v1)
v2 := r1()
fmt.Println(v2)
fmt.Println(r1())
fmt.Println(r1())
fmt.Println(r1())
//
r2 := increment()
v3 := r2()
fmt.Println(v3)
fmt.Println(r1())
fmt.Println(r2())
}
// 自增
func increment() func() int {
// 局部变量i
i := 0
// 定义一个匿名函数,给变量自增并返回
fun := func() int { // 内层函数,没有执行的
i++
return i
}
return fun
}
6.15 补充:错误处理
如果函数实现过程中,如果出现不能处理的错误,可以返回给调用者处理。比如我们调用标准库函数os.Open
读取文件,os.Open
有2个返回值,第一个是 *File
,第二个是 error
, 如果调用成功,error 的值是 nil,如果调用失败,例如文件不存在,我们可以通过 error 知道具体的错误信息。
package main
import (
"fmt"
"os"
)
func main() {
_, err := os.Open("filename.txt")
if err != nil {
fmt.Println(err)
}
}
// open filename.txt: no such file or directory
// open filename.txt: The system cannot find the file specified.
可以通过 errorw.New
返回自定义的错误
package main
import (
"errors"
"fmt"
)
func hello(name string) error {
if len(name) == 0 {
return errors.New("error: name is null")
}
fmt.Println("Hello,", name)
return nil
}
func main() {
if err := hello(""); err != nil {
fmt.Println(err)
}
}
// error: name is null
error 往往是能预知的错误,但是也可能出现一些不可预知的错误,例如数组越界,这种错误可能会导致程序非正常退出,在 Go 语言中称之为 panic。
func get(index int) int {
arr := [3]int{2, 3, 4}
return arr[index]
}
func main() {
fmt.Println(get(5))
fmt.Println("finished")
}
$ go run .
panic: runtime error: index out of range [5] with length 3
goroutine 1 [running]:
exit status 2
在 Python、Java 等语言中有 try...catch
机制,在 try
中捕获各种类型的异常,在 catch
中定义异常处理的行为。Go 语言也提供了类似的机制 defer
和 recover
。
func get(index int) (ret int) {
defer func() {
if r := recover(); r != nil {
fmt.Println("Some error happened!", r)
ret = -1
}
}()
arr := [3]int{2, 3, 4}
return arr[index]
}
func main() {
fmt.Println(get(5))
fmt.Println("finished")
}
- 在 get 函数中,使用 defer 定义了异常处理的函数,在协程退出前,会执行完 defer 挂载的任务。因此如果触发了 panic,控制权就交给了 defer。
- 在 defer 的处理逻辑中,使用 recover,使程序恢复正常,并且将返回值设置为 -1,在这里也可以不处理返回值,如果不处理返回值,返回值将被置为默认值 0。
6.16 补充:方法
可以理解为结构体的方法,在Java中就是把结构体看成一个类的成员变量,然后方法就是这个类的方法,可以调用这个类中的成员变量。
Go 语言中同时有函数和方法。
一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。所有给定类型的方法属于该类型的方法集。语法格式如下:
func (variable_name variable_data_type) function_name() [return_type]{
/* 函数体*/
}
下面定义一个结构体类型和该类型的一个方法:
package main
import (
"fmt"
)
/* 定义结构体 */
type Circle struct {
radius float64
}
func main() {
var c1 Circle
c1.radius = 10.00
fmt.Println("圆的面积 = ", c1.getArea())
}
//该 method 属于 Circle 类型对象中的方法
func (c Circle) getArea() float64 {
//c.radius 即为 Circle 类型对象中的属性
return 3.14 * c.radius * c.radius
}
6.17 补充:Go并发
Go 语言支持并发,我们只需要通过 go 关键字来开启 goroutine 即可。
goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。
goroutine 语法格式:
go 函数名( 参数列表 )
例如:
go f(x, y, z)
开启一个新的 goroutine:
f(x, y, z)
Go 允许使用 go 语句开启一个新的运行期线程, 即 goroutine,以一个不同的、新创建的 goroutine 来执行一个函数。 同一个程序中的所有 goroutine 共享同一个地址空间。
package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
}
6.18 补充:通道channel
通道(channel)是用来传递数据的一个数据结构。
通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 <-
用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。
ch <- v // 把 v 发送到通道 ch
v := <-ch // 从 ch 接收数据
// 并把值赋给 v
声明一个通道很简单,我们使用chan关键字即可,通道在使用前必须先创建:
ch := make(chan int)
注意:默认情况下,通道是不带缓冲区的。发送端发送数据,同时必须有接收端相应的接收数据。
以下实例通过两个 goroutine 来计算数字之和,在 goroutine 完成计算后,它会计算两个结果的和:
package main
import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // 把 sum 发送到通道 c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // 从通道 c 中接收
fmt.Println(x, y, x+y)
}
通道缓冲区
通道可以设置缓冲区,通过 make 的第二个参数指定缓冲区大小:
ch := make(chan int, 100)
带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。
不过由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送端就无法再发送数据了。
注意:如果通道不带缓冲,发送方会阻塞直到接收方从通道中接收了值。如果通道带缓冲,发送方则会阻塞直到发送的值被拷贝到缓冲区内;如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方在有值可以接收之前会一直阻塞。
package main
import "fmt"
func main() {
// 这里我们定义了一个可以存储整数类型的带缓冲通道
// 缓冲区大小为2
ch := make(chan int, 2)
// 因为 ch 是带缓冲的通道,我们可以同时发送两个数据
// 而不用立刻需要去同步读取数据
ch <- 1
ch <- 2
// 获取这两个数据
fmt.Println(<-ch)
fmt.Println(<-ch)
}
Go 遍历通道与关闭通道
Go 通过 range 关键字来实现遍历读取到的数据,类似于与数组或切片。格式如下:
v, ok := <-ch
如果通道接收不到数据后 ok 就为 false,这时通道就可以使用 close() 函数来关闭。
package main
import (
"fmt"
)
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}
func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
// range 函数遍历每个从通道接收到的数据,因为 c 在发送完 10 个
// 数据之后就关闭了通道,所以这里我们 range 函数在接收到 10 个数据
// 之后就结束了。如果上面的 c 通道不关闭,那么 range 函数就不
// 会结束,从而在接收第 11 个数据的时候就阻塞了。
for i := range c {
fmt.Println(i)
}
}
7 结构体、方法和接口
7.1 结构体(struct)和方法(methods)
结构体类似于其他语言中的 class,可以在结构体中定义多个字段,为结构体实现方法,实例化等。接下来我们定义一个结构体 Student,并为 Student 添加 name,age 字段,并实现 hello()
方法
type Student struct {
name string
age int
}
func (stu *Student) hello(person string) string {
return fmt.Sprintf("hello %s, I am %s", person, stu.name)
}
func main() {
stu := &Student{
name: "Tom",
}
msg := stu.hello("Jack")
fmt.Println(msg) // hello Jack, I am Tom
}
- 使用
Student{field: value, ...}
的形式创建 Student 的实例,字段不需要每个都赋值,没有显性赋值的变量将被赋予默认值,例如 age 将被赋予默认值 0。 - 实现方法与实现函数的区别在于,
func
和函数名hello
之间,加上该方法对应的实例名stu
及其类型*Student
,可以通过实例名访问该实例的字段name
和其他方法了。 - 调用方法通过
实例名.方法名(参数)
的方式。
除此之外,还可以使用 new
实例化:
func main() {
stu2 := new(Student)
fmt.Println(stu2.hello("Alice")) // hello Alice, I am , name 被赋予默认值""
}
7.2 接口(interface)
一般而言,接口定义了一组方法的集合,接口不能被实例化,一个类型可以实现多个接口。
举一个简单的例子,定义一个接口 Person
和对应的方法 getName()
和 getAge()
:
type Person interface {
getName() string
}
type Student struct {
name string
age int
}
func (stu *Student) getName() string {
return stu.name
}
type Worker struct {
name string
gender string
}
func (w *Worker) getName() string {
return w.name
}
func main() {
var p Person = &Student{
name: "Tom",
age: 18,
}
fmt.Println(p.getName()) // Tom
}
- Go 语言中,并不需要显式地声明实现了哪一个接口,只需要直接实现该接口对应的方法即可。
- 实例化
Student
后,强制类型转换为接口类型 Person。
在上面的例子中,我们在 main 函数中尝试将 Student 实例类型转换为 Person,如果 Student 没有完全实现 Person 的方法,比如我们将 (*Student).getName()
删掉,编译时会出现如下报错信息。
*Student does not implement Person (missing getName method)
但是删除 (*Worker).getName()
程序并不会报错,因为我们并没有在 main 函数中使用。这种情况下我们如何确保某个类型实现了某个接口的所有方法呢?一般可以使用下面的方法进行检测,如果实现不完整,编译期将会报错。
var _ Person = (*Student)(nil)
var _ Person = (*Worker)(nil)
- 将空值 nil 转换为 *Student 类型,再转换为 Person 接口,如果转换失败,说明 Student 并没有实现 Person 接口的所有方法。
- Worker 同上。
实例可以强制类型转换为接口,接口也可以强制类型转换为实例。
func main() {
var p Person = &Student{
name: "Tom",
age: 18,
}
stu := p.(*Student) // 接口转为实例
fmt.Println(stu.getAge())
}
7.3 空接口
如果定义了一个没有任何方法的空接口,那么这个接口可以表示任意类型。例如
func main() {
m := make(map[string]interface{})
m["name"] = "Tom"
m["age"] = 18
m["scores"] = [3]int{98, 99, 85}
fmt.Println(m) // map[age:18 name:Tom scores:[98 99 85]]
}
8 并发编程
8.1 sync
Go 语言提供了 sync 和 channel 两种方式支持协程(goroutine)的并发。
例如我们希望并发下载 N 个资源,多个并发协程之间不需要通信,那么就可以使用 sync.WaitGroup,等待所有并发协程执行结束。
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func download(url string) {
fmt.Println("start to download", url)
time.Sleep(3 * time.Second) // 模拟耗时操作
wg.Done()
}
func main() {
for i := 0; i < 3; i++ {
wg.Add(1)
go download("a.com/" + string(i+'0'))
}
wg.Wait()
fmt.Println("Done!")
}
- wg.Add(1):为 wg 添加一个计数,wg.Done(),减去一个计数。
- go download():启动新的协程并发执行 download 函数。
- wg.Wait():等待所有的协程执行结束。
可以看到串行需要 3s 的下载操作,并发后,只需要 1s。
8.2 channel
var ch = make(chan string, 10) // 创建大小为 10 的缓冲信道
func download(url string) {
fmt.Println("start to download", url)
time.Sleep(time.Second)
ch <- url // 将 url 发送给信道
}
func main() {
for i := 0; i < 3; i++ {
go download("a.com/" + string(i+'0'))
}
for i := 0; i < 3; i++ {
msg := <-ch // 等待信道返回消息。
fmt.Println("finish", msg)
}
fmt.Println("Done!")
}
9 单元测试(unit test)
假设我们希望测试 package main 下 calc.go
中的函数,要只需要新建 calc_test.go
文件,在calc_test.go
中新建测试用例即可。
// calc.go
package main
func add(num1 int, num2 int) int {
return num1 + num2
}
// calc_test.go
package main
import "testing"
func TestAdd(t *testing.T) {
if ans := add(1, 2); ans != 3 {
t.Error("add(1, 2) should be equal to 3")
}
}
运行 go test
,将自动运行当前 package 下的所有测试用例,如果需要查看详细的信息,可以添加-v
参数。
$ go test -v
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok example 0.040s
10 包(Package)和模块(Modules)
10.1 Package
一般来说,一个文件夹可以作为 package,同一个 package 内部变量、类型、方法等定义可以相互看到。
比如我们新建一个文件 calc.go
, main.go
平级,分别定义 add 和 main 方法
// calc.go
package main
func add(num1 int, num2 int) int {
return num1 + num2
}
// main.go
package main
import "fmt"
func main() {
fmt.Println(add(3, 5)) // 8
}
运行 go run main.go
,会报错,add 未定义:
./main.go:6:14: undefined: add
或
$ go run .
8
Go 语言也有 Public 和 Private 的概念,粒度是包。如果类型/接口/方法/函数/字段的首字母大写,则是 Public 的,对其他 package 可见,如果首字母小写,则是 Private 的,对其他 package 不可见。
10.2 Modules
Go Modules 是 Go 1.11 版本之后引入的,Go 1.11 之前使用 $GOPATH 机制。Go Modules 可以算作是较为完善的包管理工具。同时支持代理,国内也能享受高速的第三方包镜像服务。接下来简单介绍 go mod
的使用。Go Modules 在 1.13 版本仍是可选使用的,环境变量 GO111MODULE 的值默认为 AUTO,强制使用 Go Modules 进行依赖管理,可以将 GO111MODULE 设置为 ON。
在一个空文件夹下,初始化一个 Module
$ go mod init example
go: creating new go.mod: module example
此时,在当前文件夹下生成了go.mod
,这个文件记录当前模块的模块名以及所有依赖包的版本。
接着,我们在当前目录下新建文件 main.go
,添加如下代码:
package main
import (
"fmt"
"rsc.io/quote"
)
func main() {
fmt.Println(quote.Hello()) // Ahoy, world!
}
运行 go run .
,将会自动触发第三方包 rsc.io/quote
的下载,具体的版本信息也记录在了go.mod
中:
module example
go 1.13
require rsc.io/quote v3.1.0+incompatible
我们在当前目录,添加一个子 package calc,代码目录如下:
demo/
|--calc/
|--calc.go
|--main.go
在 calc.go
中写入
package calc
func Add(num1 int, num2 int) int {
return num1 + num2
}
在 package main 中如何使用 package cal 中的 Add 函数呢?import 模块名/子目录名
即可,修改后的 main 函数如下:
package main
import (
"fmt"
"example/calc"
"rsc.io/quote"
)
func main() {
fmt.Println(quote.Hello())
fmt.Println(calc.Add(10, 3))
}
$ go run .
Ahoy, world!
13
func add(num1 int, num2 int) int {
return num1 + num2
}
// calc_test.go
package main
import "testing"
func TestAdd(t *testing.T) {
if ans := add(1, 2); ans != 3 {
t.Error("add(1, 2) should be equal to 3")
}
}
运行 go test
,将自动运行当前 package 下的所有测试用例,如果需要查看详细的信息,可以添加-v
参数。
$ go test -v
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok example 0.040s
10 包(Package)和模块(Modules)
10.1 Package
一般来说,一个文件夹可以作为 package,同一个 package 内部变量、类型、方法等定义可以相互看到。
比如我们新建一个文件 calc.go
, main.go
平级,分别定义 add 和 main 方法
// calc.go
package main
func add(num1 int, num2 int) int {
return num1 + num2
}
// main.go
package main
import "fmt"
func main() {
fmt.Println(add(3, 5)) // 8
}
运行 go run main.go
,会报错,add 未定义:
./main.go:6:14: undefined: add
或
$ go run .
8
Go 语言也有 Public 和 Private 的概念,粒度是包。如果类型/接口/方法/函数/字段的首字母大写,则是 Public 的,对其他 package 可见,如果首字母小写,则是 Private 的,对其他 package 不可见。
10.2 Modules
Go Modules 是 Go 1.11 版本之后引入的,Go 1.11 之前使用 $GOPATH 机制。Go Modules 可以算作是较为完善的包管理工具。同时支持代理,国内也能享受高速的第三方包镜像服务。接下来简单介绍 go mod
的使用。Go Modules 在 1.13 版本仍是可选使用的,环境变量 GO111MODULE 的值默认为 AUTO,强制使用 Go Modules 进行依赖管理,可以将 GO111MODULE 设置为 ON。
在一个空文件夹下,初始化一个 Module
$ go mod init example
go: creating new go.mod: module example
此时,在当前文件夹下生成了go.mod
,这个文件记录当前模块的模块名以及所有依赖包的版本。
接着,我们在当前目录下新建文件 main.go
,添加如下代码:
package main
import (
"fmt"
"rsc.io/quote"
)
func main() {
fmt.Println(quote.Hello()) // Ahoy, world!
}
运行 go run .
,将会自动触发第三方包 rsc.io/quote
的下载,具体的版本信息也记录在了go.mod
中:
module example
go 1.13
require rsc.io/quote v3.1.0+incompatible
我们在当前目录,添加一个子 package calc,代码目录如下:
demo/
|--calc/
|--calc.go
|--main.go
在 calc.go
中写入
package calc
func Add(num1 int, num2 int) int {
return num1 + num2
}
在 package main 中如何使用 package cal 中的 Add 函数呢?import 模块名/子目录名
即可,修改后的 main 函数如下:
package main
import (
"fmt"
"example/calc"
"rsc.io/quote"
)
func main() {
fmt.Println(quote.Hello())
fmt.Println(calc.Add(10, 3))
}
$ go run .
Ahoy, world!
13