视频教程:https://www.bilibili.com/video/BV1gf4y1r79E
Golang 下载与安装
Golang 安装包
Windows 安装教程:https://juejin.cn/post/6982916943183347742
在下方两个网站中任选其一,下载对应操作系统的 Golang 安装包: > 谷歌官方网站:https://go.dev/dl/ > 国内镜像网站:https://studygolang.com/dl
安装包下载之后直接安装,如有修改安装路径的需求外,其余一路 next
即可。安装完毕之后,在命令行窗口输入 go version
查看 Go
的版本,如果显示版本号,则表示安装成功
Golang 环境变量
Golang 需要配置三个环境变量,分别是: - GOROOT
:Golang
的安装目录 - PATH
:Golang 的安装目录下的 bin 目录 -
GOPATH
:Golang 的项目目录
如果使用的是 msi
安装包,一般都会在安装的时候自动配置GOROOT
和PATH
,只要go version
能够查看版本信息,就说明
Golang 配置成功,这两个环境变量无需再配置了;如果使用的是 zip
解压缩的方式,那么就需要手动配置这些环境变量
注意:本人 Go 版本为 1.21,文章编辑于 2023年12月。新版本的 Go
会在用户变量中自动创建这三个环境变量,其中GOROOT
和PATH
保持不变即可,只需要修改GOPATH
接下来配置GOPATH
环境变量,我们需要提前新建一个我们想要开发
Go
项目的文件夹,然后在系统变量中修改名为GOPATH
的环境变量,值为该文件夹路径
在该文件夹中新建三个文件夹,分别是bin
(二进制可执行文件)、pkg
(用于存放依赖包)、src
(用于存放我们自己写的代码)
使用 VSCode 开发 Go 项目
我们接下来使用 VSCode 开发 Go 项目,如果想使用 JetBrains 专门用于Go语言程序编写的 Goland 进行开发也是可以的
但不管使用什么 IDE 开发 Go
项目,建议将默认的GOPROXY
代理地址修改为国内镜像地址,否则在下载外部依赖包时容易出现超时问题。在命令行窗口中输入该命令进行修改:
1
go env -w GOPROXY=https://goproxy.cn,direct
最后使用go env
命令查看各项环境配置参数
- 使用 VSCode 开发 Go 项目需要下载相关插件,首先下载 Go 插件
- 先新建一个
main.go
文件,右下角会自动弹出提示,让你下载其他依赖,点击"安装全部"
- 在“输出”中查看安装情况,如下图表示全部安装成功
Golang 语言基础
初窥 Golang 语法:Hello World
1 | package main // 当前程序的包名 |
注意: 1. Go 加不加分号都可以,建议不加 2.
函数的{
必须和函数名在同一行,否则编译错误
声明变量与常量
- 声明变量
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// 四种变量的声明方式
package main
import "fmt"
// 方式一二三支持全局变量,而方式四不支持
var gA int
var gB = 100
var gC = "Hello, World!"
// gD := 100
func main() {
// 方式一:声明一个变量,默认值为0
var a int
fmt.Println("a = ", a)
// 方式二:声明一个变量,并赋初始值
var b int = 100
fmt.Println("b = ", b)
// 方式三:赋初始值时,可以省略数据类型,根据值自动匹配当前变量的数据类型
var c = "Hello, World!"
fmt.Printf("c = %s, c 的类型是 %T\n", c, c)
// 方式四:(常见)省去var关键字,直接自动匹配数据类型
d := 100
fmt.Printf("d = %d, d 的类型是 %T\n", d, d)
fmt.Println("gA = ", gA, "gB = ", gB, "gC = ", gC)
// 声明多个变量
var ee, ff int = 100, 200
fmt.Println("ee = ", ee, ", ff = ", ff)
var gg, hh = 100, "abc"
fmt.Println("gg = ", gg, ", hh = ", hh)
// 多行的多变量声明
var (
ii int = 100
jj bool = true
)
fmt.Println("ii = ", ii, ", jj = ", jj)
} - 声明常量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24package main
import "fmt"
// 定义枚举类型
const (
// BEIJING = 0
// SHANGHAI = 1
// GUANGZHOU = 2
// 在const()添加关键字 iota,每行的iota都会自增1,第一行iota默认为0
BEIJING = iota // iota = 0
SHANGHAI // iota = 1
GUANGZHOU // iota = 2
)
func main() {
// 常量(只读属性)
const length int = 10
fmt.Println("length = ", length)
// length = 100 // 常量不允许被修改
fmt.Println("BEIJING = ", BEIJING, "SHANGHAI = ", SHANGHAI, "GUANGZHOU =", GUANGZHOU)
}
小技巧:VSCode 代码片段
我们可以类似 Java
中通过sout
快捷输入System.out.println
一样,为
Go 语言设置自己的代码片段 1. 创建 Go
的代码片段,按如下步骤弹出窗口后,输入“go”
- 在
go.json
中配置main
方法和fmt.println()
的代码片段,比如:这样,只要输入1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20{
"func main": {
"prefix": "fm",
"body": [
"package main",
"",
"func main() {",
"\t$0",
"}"
],
"description": "定义 main 方法"
},
"fmt Println": {
"prefix": "fp",
"body": [
"fmt.Println($0)"
],
"description": "输出语句"
}
}fm
就可以自动编写main
方法的一些代码,输入fp
就可以自动编写fmt.Println()
函数的多返回值
1 | package main |
如果有func fool(a int, b int) (c int, d int)
,不但a
和b
属于形参,c
和d
也属于形参,同样只能作为局部变量在函数体里使用,但a
和b
的默认值由调用者传参决定,而c
和d
的默认值是0
导包
建立两个目录lib1
和lib2
,并在两个目录中分别新建两个
go
文件lib1.go
和lib2.go
。lib1.go
这么写,lib2.go
同理:
1
2
3
4
5
6
7
8
9
10
11
12package lib1
import "fmt"
// lib1提供的API
func Lib1Test() {
fmt.Println("lib1开始了...")
}
func init() {
fmt.Println("lib1的init方法执行了...")
}
然后新建main.go
,通过导入上面两个包,调用其提供的API
1
2
3
4
5
6
7
8
9
10
11package main
import (
"go_study/05_init/lib1"
"go_study/05_init/lib2"
)
func main() {
lib1.Lib1Test()
lib2.Lib2Test()
}go env -w GO111MODULE=off
才可以导包成功,后续还需要改回来
2. 导包路径是%GOPATH%/项目目录名/子目录/.../程序名
3.
对外提供的函数名首字母必须大写,否则只能该函数只能在内部使用
运行结果如下所示: 1
2
3
4lib1的init方法执行了...
lib2的init方法执行了...
lib1开始了...
lib2开始了...init
方法最早开始执行,并且其他方法的执行顺序如下图所示:
另外,导包还有三个注意点: 1.
import _ "lib1"
:给lib1
起别名(匿名),表示无法使用lib1
的方法,但是会执行lib1
的init
方法
2.
import myLib "lib1"
:给lib1
起别名,表示可以使用myLib.Lib1Test()
调用lib1
的方法
3.
import . "lib1"
:将lib1
包中的所有方法导入到当前程序中,即lib1
的方法可以直接使用,比如Lib1Test()
,而无需lib1.Lib1Test()
。但是当导入多个包中有重名的方法会发生歧义,所以建议避免重名
defer
关键字
defer
关键字用于修饰被调用的函数,使该函数在调用者函数生命周期结束后再执行
比如下方这个代码示例,则展示了defer
的执行顺序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20package main
import "fmt"
func deferFunc(i int) int {
fmt.Println("传参后的 i =", i)
defer func() {
i += 5
fmt.Println("defer 函数被执行后的 i =", i)
}()
return i
}
func main() {
i := 5
fmt.Println("初始化的 i =", i)
fmt.Println("return 回来的 i =", deferFunc(i))
}1
2
3
4初始化的 i = 5
传参后的 i = 5
defer 函数被执行后的 i = 10
return 回来的 i = 5defer
关键字修饰的函数会比return
更晚执行,所以在本例中主函数得到的结果依然是初始化时候的5
另外,defer
还有个特点,使用它修饰多个函数时,这些函数的执行顺序遵循LIFO(先进后出)的栈顺序,比如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21package main
import "fmt"
func a() {
fmt.Println("a方法执行了...")
}
func b() {
fmt.Println("b方法执行了...")
}
func c() {
fmt.Println("c方法执行了...")
}
func main() {
defer a()
defer b()
defer c()
}1
2
3c方法执行了...
b方法执行了...
a方法执行了...
数组
固定数组
下方代码展示了如何声明固定长度的数组,以及如何循环遍历数组
其中我们通过range
关键字指定某个数组,返回当前元素的下标与值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22package main
import "fmt"
func main() {
// 固定长度的数组
var myArray1 [10]int
for i := 0; i < len(myArray1); i++ {
fmt.Println(myArray1[i])
}
myArray2 := [6]int{1, 2, 3, 4}
for index, value := range myArray2 {
fmt.Printf("Index: %d, Value: %d\n", index, value)
}
//查看数组的数据类型
fmt.Printf("myArray1 的数据类型是 %T\n", myArray1) // [10]int
fmt.Printf("myArray2 的数据类型是 %T\n", myArray2) // [6]int
}[长度]int
,所以当我们使用固定长度的数组进行传参时,是严格匹配数组类型的,即某个函数定义的形参类型是[10]int
,那么我们就无法传入[6]int
的数组
切片 slice
下方代码展示了如何声明动态数组: 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
27package main
import "fmt"
func printArray(arr []int) {
//引用传递
// _ 表示匿名变量
for _, value := range arr {
fmt.Println("value = ", value)
}
arr[0] = 100 // 修改 myArray 的值
}
func main() {
myArray := []int{1, 2, 3, 4, 5} //动态数组,切片slice
// 打印 myArray 的数据类型
fmt.Printf("myArray 的数据类型是 %T\n", myArray) // []int
printArray(myArray)
fmt.Println("----- 修改后 -----")
for _, value := range myArray {
fmt.Println("value = ", value)
}
}[]int
,因此在传参时,我们也只需要将形参的数据类型定义为[]int
。
而且动态数组在传参时是引用传递,因此可以修改到原数组的元素。
其中for _, value := range arr
,由于range
固定返回下标和元素值,如果我们不想使用下标,可以使用匿名变量_
代替
声明 slice
下方代码展示了两种方式声明slice
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
27package main
import "fmt"
func main() {
// 方式一:声明slice,并且初始化值
slice1 := []int{1, 2, 3, 4, 5}
fmt.Printf("len(slice1) = %d, slice1 = %d\n", len(slice1), slice1)
// 方式二:声明slice,并且分配空间,默认初始化值为0
slice2 := make([]int, 4)
slice2[0] = 1
fmt.Printf("len(slice2) = %d, slice2 = %d\n", len(slice2), slice2)
// 定义一个空切片,即没有分配空间
var slice3 []int
fmt.Printf("len(slice3) = %d, slice3 = %d\n", len(slice3), slice3)
if slice3 == nil {
fmt.Println("slice3 is nil")
} else {
fmt.Println("slice3 is not nil")
}
}make
方法先为slice
分配空间,其默认初始化值为0;
另外我们也可以定义空切片,表示没有空间被分配的切片
slice 的追加与截取
下方代码展示了
slice
的另一个属性:容量,以及如何追加元素,并且当超过容量时如何扩容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16package main
import "fmt"
func main() {
var numbers = make([]int, 3, 5)
fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers), cap(numbers), numbers) // 3 5 [0 0 0]
// 给 numbers 追加一个元素1
numbers = append(numbers, 1)
fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers), cap(numbers), numbers) // 4 5 [0 0 0 1]
// 给 numbers 追加两个元素2 3
numbers = append(numbers, 2, 3)
fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers), cap(numbers), numbers) // 6 10 [0 0 0 1 2 3]
}make
方法中有三个参数,第一个表示切片的数据类型、第二个表示切片的长度、第三个表示切片的容量,如果没有指定容量,那么也会默认使容量等于长度
如果把切片比作酒店,那么容量就是酒店当前所有的房间数,长度就是已经有客人的房间,当添加元素时,长度变长,容量不变,直到长度超过容量,那么就需要扩容了。如何扩容?就是将当前容量翻倍。
比如当前容量是5,当长度超过容量时,容量翻倍为10。如果再次超过,那么翻倍为20,以此类推
切片的截取规则和 Python 类似,下方代码展示了如何截取切片
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21package main
import "fmt"
func main() {
slice1 := []int{1, 2, 3} // len = 3,cap = 3
// 左闭右开
s1 := slice1[0:2]
fmt.Println("s1 = ", s1) // [1 2]
// 原切片和截取后的切片指向同一个内存
s1[0] = 100
fmt.Println("slice1 = ", slice1)
fmt.Println("s1 = ", s1)
// 将 slice 的元素依次拷贝到 s2 中
s2 := make([]int, 3)
copy(s2, slice1)
fmt.Println("s2 = ", s2)
}len(slice)
另外需要注意的是原切片和截取后的切片指向的是同一个内存地址,所以如果修改某个切片,另外一个切片也会随之修改。截取后的切片实际上是拷贝了原切片,我们可以使用copy
方法进行拷贝
键值对集合 map
下方代码展示了如何声明 map: 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
28package main
import "fmt"
func main() {
// 声明 map 类型,key 是 string,value 是 string
var myMap1 map[string]string
if myMap1 == nil {
fmt.Println("myMap1 is nil")
}
// 为 myMap1 分配空间
myMap2 := make(map[string]string)
// 添加键值对
myMap2["one"] = "Java"
myMap2["two"] = "C++"
myMap2["three"] = "Python"
fmt.Println(myMap2)
// 声明 map 类型,并且分配空间,同时赋初始化值
myMap3 := map[int]string{
1: "Java",
2: "C++",
3: "Python",
}
fmt.Println(myMap3)
}
下方代码展示了如何遍历、添加、删除、修改键值对集合map
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
31package main
import "fmt"
func main() {
cityMap := make(map[string]string)
// 添加城市
cityMap["China"] = "Shanghai"
cityMap["USA"] = "New York"
cityMap["Japan"] = "Tokyo"
// 遍历
for key, value := range cityMap {
fmt.Println("key = ", key)
fmt.Println("value = ", value)
}
// 删除
delete(cityMap, "USA")
// 修改
cityMap["Japan"] = "Osaka"
fmt.Println("------------------")
// 遍历
for key, value := range cityMap {
fmt.Println("key = ", key)
fmt.Println("value = ", value)
}
}
结构体 struct
下方代码展示了如何定义一个结构体,并且如何正确地传递参数才能修改成员变量
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
38package main
import "fmt"
// 声明一种新的数据类型 myInt,实际是 int 的别名
type myInt int
// 声明一种新的数据类型 Book,实际是 title string 和 author string 结合的结构体
type Book struct {
title string
author string
}
// 值传递
func changeBook(book Book) {
book.title = "Java语言编程实战"
}
// 引用传递
func changeBook1(book *Book) {
book.title = "Java语言编程实战"
}
func main() {
var number myInt = 10
fmt.Printf("type of number = %T,number = %d\n", number, number) // main.myInt 10
book := Book{title: "Go语言编程", author: "ts"}
fmt.Printf("type of book = %T,number = %v\n", book, book)
// 值传递,变量值无法被修改
changeBook(book)
fmt.Printf("type of book = %T,number = %v\n", book, book)
// 引用传递,变量值被修改
changeBook1(&book)
fmt.Printf("type of book = %T,number = %v\n", book, book)
}
封装
下方代码展示了如何打印成员变量,以及实现 Get 和 Set 方法
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
38package main
import "fmt"
// 如果类名首字母大写,表示其他包能够访问
type Human struct {
// 如果成员变量首字母大写,表示该成员变量对外能够访问,否则只能在类的内部访问
Name string
Age int
Phone string
}
func (this *Human) Show() {
fmt.Println("Name = ", this.Name)
fmt.Println("Age = ", this.Age)
fmt.Println("Phone = ", this.Phone)
}
func (this *Human) SetPhone(newPhone string) {
this.Phone = newPhone
}
func (this *Human) GetPhone() string {
return this.Phone
}
func main() {
human := Human{Name: "Alice", Age: 30, Phone: "123456789"}
human.Show()
fmt.Println("--------------------")
fmt.Println(human.GetPhone())
fmt.Println("--------------------")
human.SetPhone("987654321")
human.Show()
}(this *Human)
,其中this
只是代表它是一个Human指针
类型的变量,实际上
Go
中并没有this
关键字。另外这是固定写法,表示该函数会被视为对象的
Get 和 Set 方法
继承
在 Go
中如何实现子类对父类的继承,实际上直接在定义子类时添加父类名即可继承父类的成员变量和方法
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
49package main
import "fmt"
type Human struct {
Name string
Age int
}
func (this *Human) Eat() {
fmt.Println("Human eat...")
}
func (this *Human) Walk() {
fmt.Println("Human walk...")
}
type SuperMan struct {
Human // SuperMan 继承了 Human 类
level int
}
// 重写父类方法
func (this *SuperMan) Walk() {
fmt.Println("SuperMan is walking...")
}
// 定义子类的新方法
func (this *SuperMan) Fly() {
fmt.Println("SuperMan is flying...")
}
func main() {
human := Human{Name: "Alice", Age: 20}
human.Eat()
human.Walk()
fmt.Println("----------------")
// superMan := SuperMan{Human{Name: "Bob", Age: 30}, 1}
var superMan SuperMan
superMan.Name = "Bob"
superMan.Age = 30
superMan.level = 1
superMan.Eat()
superMan.Walk()
superMan.Fly()
}
而在实例子类对象时,有一种写法看上去是将父类对象和子类属性一起传了进去。但可读性较差,推荐使用下面一个个定义属性的方法
多态
接口 interface
在 Go
中实现多态需要使用接口interface
来定义父类,注意接口数据类型实际上是个指针,而子类需要全部实现该接口的所有方法才可被视为实现了该接口,此时父类接口类型才指向(或者说引用)子类的具体数据变量
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
63
64package main
import "fmt"
// 接口实质上是**指针类型**的
type Animal interface {
Sleep()
GetColor() string // 获取动物的颜色
GetType() string // 获取动物的类型
}
// 具体的类
type Cat struct {
color string
}
func (this *Cat) Sleep() {
fmt.Println("Cat is sleeping")
}
func (this *Cat) GetColor() string {
return this.color
}
func (this *Cat) GetType() string {
return "This is a Cat"
}
// 具体的类
type Dog struct {
color string
}
func (this *Dog) Sleep() {
fmt.Println("Dog is sleeping")
}
func (this *Dog) GetColor() string {
return this.color
}
func (this *Dog) GetType() string {
return "This is a Dog"
}
func showAnimal(animal Animal) {
animal.Sleep()
fmt.Println(animal.GetColor())
fmt.Println(animal.GetType())
}
func main() {
// var animal Animal
// animal = &Cat{"Orange"}
// animal.Sleep()
// animal = &Dog{"Black"}
// animal.Sleep()
cat := Cat{"Orange"}
dog := Dog{"Black"}
showAnimal(&cat)
showAnimal(&dog)
}Animal
接口,其中有三个成员方法。另外定义了两个类,只有当子类实现了接口的所有方法,接口指针才指向这个类。
当子类实现了接口,那么我们通过调用父类接口的方法也相当于调用了子类方法。比如示例代码中的showAnimal
方法,方法体里虽然是animal.Sleep()
,但实际上是在调传进来的对象的Sleep()
方法
空接口 interface
interface{}
是空接口,是一种通用万能类型,int
、string
、float32
、float64
、struct
都实现了interface{}
。实际上因为空接口什么方法都有,所以任何数据类型都实现了空接口
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
29package main
import "fmt"
func myFunc(arg interface{}) {
fmt.Println("myFunc is called...")
fmt.Println(arg)
// interface{} 该如何区分此时引用的数据类型是什么?
// 答案:给 interface{} 提供类型断言的机制
value, ok := arg.(string)
if !ok {
fmt.Println("arg is not string type")
} else {
fmt.Printf("arg is %T type, and arg = %v\n", value, value)
}
}
type Book struct {
auth string
}
func main() {
book := Book{auth: "Go语言编程"}
myFunc(book)
myFunc("Go语言编程")
myFunc(123)
myFunc(3.14)
}value, ok := arg.(string)
,其中arg.(string)
有两个返回值,value
就是空接口当前引用的变量值,ok
是类型断言后的布尔值结果