# Go 语言基础

# 参考文档

# 简介

Go 是一门编译型语言。

即,运行程序之前,Go 首先使用编译器将代码转换成机器能读懂的 1 和 0。它会把所有代码编译成一个可执行文件,在编译的过程中,Go 编译器能够捕获一些错误。

# 安装

安装完之后,控制台打出 go 然后回车,判断是否已经存在了 go 命令。

# 配置 VSCode

  1. 安装扩展 Go。
  2. Ctrl + Shift + P,搜索 Go: Install/Update Tools
  3. 全选之后,进行安装。

由于源在国外,所以速度可能会很慢,这时候可以:

go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct
1
2

# 包和函数

#

package <package_name> 标识代码所属的包。

import 导入包:

// 第一种
import "fmt"
// 第二种
import (
    "fmt"
)
1
2
3
4
5
6

# 函数

// 左括号必须和 func 处于同一行
// 右括号必须单独占一行
func main() {
    fmt.println("hello world")
}
1
2
3
4
5

# 变量/常量

使用 var 来声明一个变量,使用 const 来声明一个变量。

var a = 1

var a, b = 1, 2

var (
    a = 1
    b = 2
)
1
2
3
4
5
6
7
8

# 短声明

var a = 1
// 可以替换为
a := 1
1
2
3

短声明语句更加简短,并且可以在无法使用 var 的地方使用。

// i 只能声明在外面
var i = 0
for i = 5; i > 0; i-- {
    ...
}
// i 可以在 for 中声明
for i := 5; i > 0; i-- {
    ...
}
1
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)
1
2
3

# 循环和分支

# if

if boolean {
    ...
}
if statement; var boolean {
    ...
}
1
2
3
4
5
6

# switch

Go 中的 switch 不需要使用 break 跳出,默认就会跳出,如果需要让它继续向下走,则需要使用关键字 fallthrough

# for

break 跳出循环。

# range

  • 遍历 array*arraystring,返回两个值分别是数据的索引和值。
  • 遍历 map 时返回的两个值分别是 keyvalue
  • 遍历 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
1
2
3
4
5
6
7
8
9
10
11

# Boolean 类型

Go 只有 truefalse,不会出现 1 也是 true,只有 true 代表 true,也只有 false 代表 fasle

# 实数

  • float32 单精度
  • float64 双精度
// 只要数字含有小数部分,那么它的类型就是 float64
num := 2.98
1
2

# 整数类型

共 10 中整数类型,注意 int 并不是其它 int * 的别名,它们是不一样的:

  • int
  • uint,unsigned int
  • int8
  • uint8
  • int16
  • uint16
  • int32
  • uint32
  • int64
  • uint64

intuint 在不同的设备上表示的位数不同。老设备上等同于 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
1
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
}
1
2
3
4
5
6
7
8
9
10

# 函数

go function

  • 形参 parameter
  • 实参 argument
func func_name(param_name param_type) result_type {
    ...
}
// 多个返回值的话用 (), 返回值可以省略参数名,只写参数类型
func func_name(param_name param_type) (result_name result_type, ...) {
    ...
}
1
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
}
1
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
}
1
2
3
4
5
6
7

# 可变参数

... 标识可变参数

# 方法

go method

方法和函数区别?

函数就是一个独立的函数,方法是指的和某个类型关联的一个函数。

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
}
1
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)
1
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]
// 第三个值表示数组容量
1
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 往切片中添加值,返回值仍是个切片。


1

# map

map := map[string]int{
    "a": 1
    "b": 2
}
// map["c"] 不会报错,会返回 int 的默认值 0
1
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}
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

# 将 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}
1
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}
1
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
}
1
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()
}
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

如果子类型有两个同名方法,那么,需要写一个方法接收 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())
}
1
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{})
}
1
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
}
1
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
}
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

示例 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 这里是因为地址不一样,即使内容相等了
}
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
  • 一个结构体指针可以直接访问属性,可以不用解引用。
  • 数组指针也会自动解引用。
  • 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
    // 可以直接使用 指针操作数组操作,因为数组指针有自动解引用
}
1
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++
}
1
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)
}
1
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)
}
1
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
}
1
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
}
1
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
1
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 开辟的线程执行完再结束
1
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
1
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

# 讨论区

由于评论过多会影响页面最下方的导航,故将评论区做默认折叠处理。

点击查看评论区内容,渴望您的宝贵建议~
Last Updated: 12/2/2022, 11:30:26 AM