# Go 语言基础
# 参考文档
# 简介
Go 是一门编译型语言。
即,运行程序之前,Go 首先使用编译器将代码转换成机器能读懂的 1 和 0。它会把所有代码编译成一个可执行文件,在编译的过程中,Go 编译器能够捕获一些错误。
# 安装
安装完之后,控制台打出 go
然后回车,判断是否已经存在了 go
命令。
# 配置 VSCode
- 安装扩展 Go。
Ctrl + Shift + P
,搜索Go: Install/Update Tools
。- 全选之后,进行安装。
由于源在国外,所以速度可能会很慢,这时候可以:
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct
2
# 包和函数
# 包
package <package_name>
标识代码所属的包。
import 导入包:
// 第一种
import "fmt"
// 第二种
import (
"fmt"
)
2
3
4
5
6
# 函数
// 左括号必须和 func 处于同一行
// 右括号必须单独占一行
func main() {
fmt.println("hello world")
}
2
3
4
5
# 变量/常量
使用 var
来声明一个变量,使用 const
来声明一个变量。
var a = 1
var a, b = 1, 2
var (
a = 1
b = 2
)
2
3
4
5
6
7
8
# 短声明
var a = 1
// 可以替换为
a := 1
2
3
短声明语句更加简短,并且可以在无法使用 var
的地方使用。
// i 只能声明在外面
var i = 0
for i = 5; i > 0; i-- {
...
}
// i 可以在 for 中声明
for i := 5; i > 0; i-- {
...
}
2
3
4
5
6
7
8
9
短声明不可以声明 package 级别变量,即全局变量。
# 运算符
Go 中没有 ++a
这种写法,只有 a++
。
# 随机数
使用 rand
from math/rand
rand.Seed(time.Now().UnixNano())
a := rand.Intn(10)
fmt.Print(a)
2
3
# 循环和分支
# if
if boolean {
...
}
if statement; var boolean {
...
}
2
3
4
5
6
# switch
Go 中的 switch
不需要使用 break
跳出,默认就会跳出,如果需要让它继续向下走,则需要使用关键字 fallthrough
。
# for
break
跳出循环。
# range
- 遍历
array
、*array
、string
,返回两个值分别是数据的索引和值。 - 遍历
map
时返回的两个值分别是key
和value
。 - 遍历
channel
时,则只有一个返回数据。
str := "abcdef"
for i, v := range str {
fmt.Println(i, "--", v)
}
--- out ---
0 -- 97
1 -- 98
2 -- 99
3 -- 100
4 -- 101
5 -- 102
2
3
4
5
6
7
8
9
10
11
# Boolean 类型
Go 只有 true
和 false
,不会出现 1 也是 true
,只有 true
代表 true
,也只有 false
代表 fasle
。
# 实数
float32
单精度float64
双精度
// 只要数字含有小数部分,那么它的类型就是 float64
num := 2.98
2
# 整数类型
共 10 中整数类型,注意 int
并不是其它 int *
的别名,它们是不一样的:
int
uint
,unsigned intint8
uint8
int16
uint16
int32
uint32
int64
uint64
int
和 uint
在不同的设备上表示的位数不同。老设备上等同于 int32
,新设备上等同于 int64
。
big
package
# 注意
不用的引用会报错
fira code font style
# 格式化输出
%T
,输出数据类型。%b
,输出二进制。0x[十六进制值]
,使用%x
输出十六进制值。%c
输出数值对应的字符。
# 字符串
使用增强符号 ` 来显示字符串的原值。
// 字符串字面值
str := "aa\nbb"
// 原始字符串字面值
str2 := `aa\nbb`
fmt.Println(str)
fmt.Println(str2)
--- out ---
aa
bb
aa\nbb
2
3
4
5
6
7
8
9
10
# 类型别名
注意 类型别名
和 类型定义
是不同的。
# byte
byte = uint8
# rune
rune = int32
# 自定义类型别名
type new_name = int32
# 类型断言
value, ok := param.(type_name)
func main() {
var a interface{}
a = "sss"
val, ok := a.(string)
val1, ok1 := a.(int)
fmt.Println(val, ok) // sss true
fmt.Println(val1, ok1) // 0 false
}
2
3
4
5
6
7
8
9
10
# 函数
- 形参
parameter
- 实参
argument
func func_name(param_name param_type) result_type {
...
}
// 多个返回值的话用 (), 返回值可以省略参数名,只写参数类型
func func_name(param_name param_type) (result_name result_type, ...) {
...
}
2
3
4
5
6
7
例如:
// 1, 3 是实参,a, b 是形参
func main() {
fmt.Println(add(1, 3))
}
func add(a int, b int) int {
return a + b
}
2
3
4
5
6
7
调用当前包内的函数,不需要加包名。如果要调用其他包的函数,则需要使用 package_name.method_name
形式调用。
方法名是大写开头表示可以在其他包内调用,如果是小写,则标识私有方法。
...interface{}
接收任意类型,任意个参数。
# 参数类型一致时可以省略成一个
func add(a int, b int) int {
return a + b
}
// 可以写成
func add(a, b int) int {
return a + b
}
2
3
4
5
6
7
# 可变参数
...
标识可变参数
# 方法
方法和函数区别?
函数就是一个独立的函数,方法是指的和某个类型关联的一个函数。
package main
import "fmt"
type Num int
func main() {
var n Num = 5
fmt.Println(add(1, 2))
fmt.Println(n.add(2))
}
// 函数
func add(a int, b int) int {
return a + b
}
// 方法
func (num Num) add(b int) int {
return int(num) + b
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 函数是一等公民
# 匿名函数
# 数组
// 1
var arr [2]string
arr[0] = "a"
arr[1] = "b"
// 2
arr2 := [2]string{"c", "d"}
// 3
arr3 := [...]string{
"e",
"f"
}
length := len(arr)
2
3
4
5
6
7
8
9
10
11
12
13
# 切片 slice
arr := [...]string{
"a","b","c"
}
arr0 := []string{...}
arr1 := arr[0:1]
arr2 := arr[1:2]
arr3 := arr[2:3]
arr4 := arr[0:2:2]
// 第三个值表示数组容量
2
3
4
5
6
7
8
9
10
11
[]
中没有数字或者 ...
,代表声明的是一个切片,实质也是先声明一个数组,再生成的切片。
切片展开 slice_name ...
# make
- 两个参数
make([]string, length && capacity)
- 三个参数
make([]string, length, capacity)
# cap
len() 获取长度,cap() 获取容量。
即 length 和 capacity
# append
切片使用 append 往切片中添加值,返回值仍是个切片。
# map
map := map[string]int{
"a": 1
"b": 2
}
// map["c"] 不会报错,会返回 int 的默认值 0
2
3
4
5
delete(map,key)
,删除 map 中的值。
# struct
struct 可以和函数关联形成方法
struct 深拷贝
func main() {
// 声明了一个 struct 类型的变量 user
// 本质是变量
var user struct {
name string
age int32
}
user.age = 12
user.name = "ming"
fmt.Println(user)
// 声明了一个名为 user2 类型的 struct
// 本质是类型
// 实质是别名
type user2 struct {
name string
age int32
}
// 声明一个 user2 类型的变量 a1
var a1 user2
a1.age = 12
a1.name = "qqq"
fmt.Println(a1)
// 初始化
u2 := user2{"aa", 23}
u3 := user2{name: "bb", age: 23}
fmt.Println(u2)
fmt.Println(u3)
// 格式化输出,带 + 号的话会将属性名打印出来
fmt.Printf("%v", u2)
fmt.Printf("%+v", u3)
}
--- out ---
{ming 12}
{qqq 12}
{aa 23}
{bb 23}
{aa 23}
{name:bb age:23}
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
# 将 struct 编码为 JSON
type user2 struct {
// 这里的变量名首字母必须大写
Name string
Age int32
}
func main() {
u2 := user2{"aa", 23}
bytes, err := json.Marshal(u2)
if err != nil {
fmt.Println(err.Error())
}
fmt.Println(string(bytes))
}
--- out ---
{"Name":"aa","Age":23}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
注意: struct 的变量名必须是首字母大写,才能被导出为 JSON,因为在 Go 中,字母的首字母大小写代表是否私有。如果是小写,则表示私有,所以打不出来。
或者使用 JSON 标签:
type user2 struct {
Name string `json:"name"`
Age int32 `json:"ageee"`
}
func main() {
u2 := user2{"aa", 23}
bytes, err := json.Marshal(u2)
if err != nil {
fmt.Println(err.Error())
}
fmt.Println(string(bytes))
}
--- out ---
{"name":"aa","ageee":23}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 构造函数
构造方法的函数名使用 new + <结构体名称>
组成。
# 组合
组合就是结构体内的属性的类型是结构体类型。
type Address struct {
Province string
City string
District string
}
func main() {
add := Address{"pro", "city", "dis"}
b := add.newAddress()
bytes, err := json.Marshal(b)
if err != nil {
fmt.Println(err.Error())
}
fmt.Println(string(bytes))
}
func (a Address) newAddress() Address {
return a
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 转发
# struct 嵌入
组合的时候只给定类型,不给变量名
// 1 这种方式
type User struct {
Name string `json:"name"`
Age int32 `json:"ageee"`
Address Address `json:"address"`
}
func (a Address) newAddress() Address {
return a
}
func main() {
user := User{Age: 18, Name: "ming"}
// 这里是错误的,newAddress 只能接收 Address 类型
user.newAddress()
}
// 2 这种方式
type User struct {
Name string `json:"name"`
Age int32 `json:"ageee"`
// struct 嵌入
Address `json:"address"`
}
func (a Address) newAddress() Address {
return a
}
func main() {
user := User{Age: 18, Name: "ming"}
// 这里的 user 是可以调用 newAddress 的
user.newAddress()
}
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
如果子类型有两个同名方法,那么,需要写一个方法接收 user
,然后在新方法中指定执行哪个方法。
# 接口
Go 的接口是隐式实现的。
var inf interface {
talk() string
}
type user struct{}
// user 实现了接口
func (u user) talk() string {
return "i am a user"
}
func main() {
// 将具体实现赋值给接口变量
inf = user{}
fmt.Println(inf.talk())
}
2
3
4
5
6
7
8
9
10
11
12
13
- 接口类型的名称一般是
-er
结尾。 - 为方便服用,一般将接口声明为类型 struct。
type talker interface {
talk() string
}
type user struct{}
func (u user) talk() string {
return "i am a user"
}
// 函数入参是一个接口类型,所以只要实现了这个接口的类型都可以传进来
func aa(t talker) {
fmt.Println(t.talk())
}
func main() {
// user 实现了接口,所以可以传进去
aa(user{})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 接口与 struct 嵌入
# 指针
与 C 系列语言一样, Go 中 所有 内容都是 按值传递。
&
地址操作符,&a
即变量 a
的地址。
- 无法获得 字符串、数值、布尔字面量的地址。
*
解引用,即取内容。
func main() {
// 这里定义了一个变量,它的值为 3,address 代表变量存储的内存地址,而 content 则是该地址值对应的内容
a := 3
address := &a
fmt.Println(address) // 0xc00018c000
content := *address
fmt.Println(content) // 3
// *& 抵消等于空
fmt.Println(*&a) // 3
}
2
3
4
5
6
7
8
9
10
11
12
Go 中不允许像 C 中的操作指针加减的操作。
两个指针变量判断是否相等使用 ==
,它们比较的是指针变量中存放的地址值是否一样。
示例1:
func main() {
name := "xiaoming"
add := "sd_qd"
var pn *string = &name
pa := &add
fmt.Println(pa) // 0xc00009e230
fmt.Println(*pa) // sd_qd
fmt.Println(pn) // 0xc00009e220
fmt.Println(*pn) // xiaoming
*pn = "ming"
fmt.Println(name) // ming
fmt.Println(pn) // 0xc00009e220
fmt.Println(*pn) // ming
po := pa
fmt.Println(po) // 0xc00009e230
fmt.Println(*po) // sd_qd
add = "qdddd"
fmt.Println(*pa) // qdddd
fmt.Println(*po) // qdddd
fmt.Println(po == pa) // true
}
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
示例 2:
func main() {
name := "xiaoming"
pa := &name
fmt.Println(pa) // 0xc000010240
fmt.Println(*pa) // xiaoming
pb := pa
fmt.Println(pb) // 0xc000010240
fmt.Println(*pb) // xiaoming
fmt.Println(pa == pb) // true
add := "qingdao"
pa = &add
fmt.Println(pa == pb) // false
// *pb 赋值给一个变量的时候,是赋值了一个副本,所以 pbcontent 和 pb 指针指向的内容不是同一个。
pbcontent := *pb
*pb = "hahaha"
fmt.Println(pbcontent) // xiaoming
fmt.Println(name) // hahaha
fmt.Println(*pb) // hahaha
name = "xiaoming"
pp := &pbcontent
fmt.Println(pbcontent == name) // true 这里是内容相等
fmt.Println(pb == pp) // false 这里是因为地址不一样,即使内容相等了
}
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
- 一个结构体指针可以直接访问属性,可以不用解引用。
- 数组指针也会自动解引用。
- Slice 和 map 没有自动解引用。
示例 3:
func main() {
arr := [2]string{"a", "b"}
fmt.Println(arr) // [a b]
fmt.Println(arr[1]) // b
parr := &[2]string{"a", "b"}
fmt.Println(parr) // &[a b]
fmt.Println(parr[1]) // b
// 可以直接使用 指针操作数组操作,因为数组指针有自动解引用
}
2
3
4
5
6
7
8
9
10
Go 的函数和方法是按值传递的,这意味着函数总是操作于被传递参数的副本。
当指针被传递到函数时,函数接收的是传入的内存地址(指针存储的值)的副本,之后函数通过解引用内存地址来修改指针指向的值。
示例 4:
type user struct {
name string
age int32
}
func main() {
u1 := user{"xiaoming", 18}
m1(&u1)
fmt.Println(u1.age) // 19 直接通过地址修改的值
u2 := user{"xiaoming", 18}
m2(u2)
fmt.Println(u2.age) // 18 修改的是副本的值
}
func m1(p *user) {
p.age++
}
func m2(u user) {
u.age++
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
示例 5:
type user struct {
name string
age int32
}
func main() {
u1 := user{"xiaoming", 18}
m1(&u1) // 0xc00000c040
m2(u1) // 0xc00000c058
}
func m1(p *user) {
fmt.Println(&p.age)
}
func m2(u user) {
fmt.Println(&u.age)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
示例 6:
type user struct {
name string
age int32
}
func main() {
u1 := user{"xiaoming", 18}
u2 := &u1
// 指针和结构体变量都可以调用方法 m1,因为这里有自动取地址
u1.m1() // 0xc00000c040, 这里等同于 (&u1).m1()
u2.m1() // 0xc00000c040
}
func (p *user) m1() {
fmt.Println(&p.age)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
map 本身就是一种隐式指针,所以在方法中使用的时候不需要 func a(param *map) {...}
。
map 的键和值都可以值指针类型。
slice 内部表示为 3 个元素:
- 指向数组的指针
- 切片容量
- 切片长度
当 slice 作为参数传递给函数或方法时,就可以使用slice 内部指针修改底层数据。
指向 slice 的显示指针的唯一作用就是修改 slice 本身:
- slice 长度
- slice 容量
- 起始偏移量
# 指针和接口
示例1:
talk() string
}
func aa(t talker) {
fmt.Println(t.talk())
}
type User struct{}
func (u User) talk() string {
return "a"
}
func main() {
u1 := User{}
aa(u1) // a
aa(&u1) // a
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 方法接收者无论是类型还是指针类型,都可以通过类型或者指针直接调用。这里 Go 会做自动解引用处理。前提是你使用的指针可以得到一个对应类型的对象。
示例 2:
type talker interface {
talk() string
}
func aa(t talker) {
fmt.Println(t.talk())
}
type User struct{}
func (u *User) talk() string {
return "a"
}
func main() {
u1 := User{}
aa(u1) // wrong,这里是赋值的副本,所以会报错
aa(&u1) // a
p := &u1 // a
u1.talk() // a
p.talk() // a
(&u1).talk() // a
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Nil
如果一个指针没有明确指向,默认为 Nil。
Nil 可以看做是几个类型的零值。
除了指针,nil 还是 slice、map 和接口的零值。
nil 会导致 panic(恐慌),继而引起程序崩溃。
如果指针没有明确指向,程序也就无法对其解引用。
尝试解引用一个 nil 指针将导致程序崩溃。类似 NPE。
# 异常处理
# defer
defer
延迟,相当于 finally
。
defer
会在 return
之后,赋值给返回值之前执行。
错误变量名应该以 Err
开头。
# goroutine
在 Go 中,独立的任务叫做 goroutine。
# 创建 goroutine
在调用前加个关键字 go
,就可以并发执行。
示例 1:
func main() {
aa()
fmt.Println("world")
}
func aa() {
fmt.Println("hello")
time.Sleep(3 * time.Second)
}
--- out ---
hello
// 等 3 秒
world
2
3
4
5
6
7
8
9
10
11
12
示例 2:
func main() {
// 使用 go 开辟一个新线程
go aa()
fmt.Println("world")
}
func aa() {
fmt.Println("hello")
time.Sleep(3 * time.Second)
}
--- out ---
world
// 这里只会显示 world,因为主线程并不会等 go 开辟的线程执行完再结束
2
3
4
5
6
7
8
9
10
11
12
# goroutine 执行顺序
func main() {
for i := 0; i < 5; i++ {
go aa(i)
}
time.Sleep(4 * time.Second)
}
func aa(i int) {
fmt.Println("hello", i)
time.Sleep(3 * time.Second)
}
--- out ---
hello 4
hello 0
hello 1
hello 2
hello 3
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
多线程的执行是无序的。
# channel
channel 通道,通道可以在多个 goroutine 之间安全的传值。
使用 make
创建一个通道,并指定其传输数据的类型。
c := make(chan int)
- 向通道发送值
c <- 99
- 从通道接收值
a := <- c
# 讨论区
由于评论过多会影响页面最下方的导航,故将评论区做默认折叠处理。