Go 语言从入门到精通(超全超详细整理)

Go 语言入门基础

由于 GolangClang 有高度的相似性,本文假设你已经掌握了 C 语言,文中未提及的部分可以参考 Clang

文章目录

1. Get Start

package main

import "fmt"

func main() {
    fmt.Println("Hello World!")
}
  • package main 定义包名,如果包名为 main ,其应该包含 func main() {} 否则不该包含。

  • import 有两种用法:

    import "fmt"
    
    import (
        "fmt"
        "time"
    )
    

2. 变量与常量

2.1 定义

2.1.1 var 定义变量
var a int  // var 定义变量
var a int = 10  // var 设置初值
var a = 10  // var 自动解析类型 (但不能解析为 int8 等)
var a, b int = 1, 2  // var 定义同类型多变量
var (
	a int
    s string
)  // var 定义多变量
var (
	a, b int = 1, 2
    s, ss string
    pi = 3.1415926535  // float64
)
2.1.2 const 定义常量
const a int = 10  // 必须初始化
const a = 10  // 自动解析常量类型
const a, b int = 1, 2  // 定义同类型多常量
const (
	a int = 10
    s string = "Hello World!"
    pi float64 = 3.1415926535
)
2.1.3 := 自动变量
a := 1  // var a int = 1
s := "Hello World!"  // var s string = "Hello World!"
b, ss := 2, "No Way Home."
  • **注意:**自动变量不可用于全局变量的定义。
2.1.4 匿名变量
  • 匿名变量 _ 伪接受数值,数值将直接被丢弃,不可访问其数值。
  • 匿名变量 _ 可以使用 =:=。(其他变量重定义将出错)
func getData() (int, int) {
	return 10, 20
}
func main() {
    a, _ := getData()  // 10, _
    _, a = getData()  // _, 20
}

2.2 作用域

  • 局部变量:
    • 每一个 {} 可以被视为一个作用域,它包括函数、if 语句、for 语句等。
    • 在局部作用域定义的变量将在所定义的作用域结束时消亡。
  • 全局变量:
    • 详见 7.3 全局作用域

2.3 基本数据类型

2.3.1 概览
  • 概览中的 complex / uintptr / byte/rune 将作补充说明,其他不作介绍。
类型默认值描述
int / uint0默认位数的整型和无符号整型,unsafe.Sizeof() 查看字节数
int8 / int16 / int32 / int6408 / 16 / 32 / 64 位整型
uint8 / uint16 / uint32 / uint6408 / 16 / 32 / 64 位无符号整型
float32 / float640.032 / 64 位浮点数(IEEE754
complex64 / complex128(0.0+0.0i)32 / 64 位复数
uintptrnil无符号整型,指针类型
byte‘\000’类似 uint8,表示 ASCII 编码
rune‘\000’类似 int32,表示 UTF-8 编码
  • Golang 中,int / uint 不是 int8 / int16 / int32 / int64 / uint8 / uint16 / uint32 / uint64 的别名。
  • 后面还将介绍非基本数据类型,如 stringslicechannel
2.3.2 复数类型 complex
  • 可以使用 a+bi 表示复数,如 0.5+1.5i

  • 可以使用内置函数 func complex(r, i float64) complex128 构造复数类型。

2.3.3 指针类型 uintptr / unsafe.Pointer
  • 任何指针类型不可直接转换为 uintptruintptr 可以与 unsafe.Pointer 相互转换,unsafe.Pointer 可以实现对任意数据类型的强制转换,一般直接使用 unsafe.Pointer 即可。注意:这是危险的操作。
a := "Hello World!"  // string: "Hello World!"
b := []byte(a)  // []byte: [72 101 108 108 111 32 87 111 114 108 100 33]
c := **(**int)(unsafe.Pointer(&a))  // int: 8022916924116329800
2.3.4 字符类型 byte / rune
  • 字符类型使用单引号表示,默认为 rune 类型,当为 ASCII 编码范围时,可以转换为 byte 类型。字符有多种表达方式,与 Clang 有所不同。
符号描述
'A'字符表示
'\n'转义字符,与 Clang 基本相同
'\101'八进制编码,'\000' ~ '\177'0 ~ 127
'\x41'十六进制,'\x00' ~ '\xff'0 ~ 255
'\u4f60'Unicode 编码,'\u0000' ~ '\uffff'0 ~ 65535

3. 内置数据类型

3.1 指针类型

3.1.1 定义指针
var a int = 10
var p *int = &a
pp := &a
3.1.2 空指针
  • 默认值,空指针的值为 nil
3.1.3 访问指针
  • 使用 *ptr 访问值,地址无法访问将产生运行错误。
  • 结构体使用 name.attr 访问元素,在 Golang 中可以使用 ptr.attr 通过指针访问元素(Clangptr->attr)。

3.2 结构体类型

3.2.1 定义结构体
  • 具名结构体
type Book struct {
	name string
    author string
}
var a Book  // 定义默认结构体
var b = Book{"Golang 入门经典", "Unknown"}  // 全部初始化
var c = Book{name: "C++ 入门指南"}  // 局部初始化
d := Book{name: "鸟哥的 Linux 私房菜"}  // 使用 :=
var e *Book = new(Book)  // 使用指针
  • 字段匿名
type Book struct {
	string
    string
}
var a Book = Book{"Golang 入门经典", "Unknown"}
  • 结构体匿名
var a = struct{
    name string
    author string
}{"Golang 入门经典", "Unknown"}
  • 两种匿名不可同时使用。
3.2.2 访问结构体
var a Book = Book{"Golang 入门经典", "Unknown"}
var p *Book = &a
fmt.Println(a.name, a.author)
fmt.Println(p.name, p.author)  // 结构体指针直接使用 ptr.attr 访问
3.2.4 类型转换
  • 如果两个结构体具有完全相同的定义,它们可以进行显式类型转换。
3.2.5 嵌套与继承
  • 结构体嵌套将一个结构体作为新结构体的一个字段。
type Library struct {
    name string
	books []Book
}
  • 结构体继承将一个结构体的字段和方法引入新结构体。结构体的方法后面会提及。
type ChildBook struct {
	fitAge int
    Book
}

3.3 数组类型

3.3.1 定义数组
var a [3]int
var b = [3]int{1, 2, 3}
var c = [...]int{1, 2, 3}  // 注意 [...]int 是数组, []int 是切片 (下一节会讲)
d := [...]int{1, 2, 3}
3.3.2 传递数组
  • Golang 中数组名表示整个数组。这与 Clang 表示数组头指针不同,但与其 std::array 类似。传递数组有时会产生很大的开销,可以使用指针数组。
var a [3]int = [3]int{1, 2, 3}
var p *[3]int = &a

3.4 切片类型

3.4.1 定义切片
var a []int  // [], 注意 [3]int / [...]int 都是数组不是切片
var a = []int{1, 2, 3}  // [1 2 3]
a := []int{1, 2, 3}     // [1 2 3]
var a = make([]int, 3)  // [0 0 0]
// make: make 只用于构造 slice / map / channel, 第一个参数为类型, 第二个参数为大小
3.4.2 切片原型
  • slice 是引用数据类型,对切片的赋值不会拷贝数据,切片的切片也是引用而非拷贝。
  • 后面介绍的所有切片操作请注意区别引用与拷贝
type SliceHeader struct {
    Data uintptr  // 数据存储地址
    Len int       // 数据长度
    Cap int       // 数据申请长度
}
3.4.3 切片操作
3.4.3.1 切片

​ * 切片是一种数据类型也是一种操作

  • 进行切片的对象可以是数组或切片,将返回 SliceHeader ,是对数据的引用而非拷贝。
  • Golang 中的切片应与 Python 区分的是:Golang 中索引不可使用负数表示,并且不可设置步长。
var a = []int{1, 2, 3}
b := a[:]    // [1 2 3] 如果 a 是一个数组, 此方法将创建一个切片
c := a[1:]   // [2 3]
c := a[:2]   // [1 2]
c := a[1:2]  // [2]
3.4.3.2 append() 与 copy()
  • append()copy() 都是对数据的拷贝,会对引用到的所有数据进行拷贝。
var a = []int{1, 2}
var b = []int{4, 5, 6}
var c []int = append(a, 4)        // [1 2 4]     传入切片类型与元素类型, 返回切片类型
var d []int = append(a, 4, 5, 6)  // [1 2 4 5 6] 传入切片类型与多个元素类型,返回切片类型
var e []int = append(a, b...)     // [1 2 4 5 6] 传入两个切片类型, 返回切片类型, 是将 b 追加到 a 尾后的新切片
var i int = copy(a, b)            // 2 [4 5] [4 5 6]  将 b 拷贝到 a , 返回拷贝量, 拷贝量为 min(len(a), len(b))
  • Golangargs... 表示解包,对应 Python 中的 *args
3.4.3.3 常见组合操作
3.4.3.3.1 拷贝切片
b := append([]int{}, a...)
b := make([]int, len(a))
copy(b, a)
3.4.3.3.2 原地删除
  • 原地删除首部
a = a[st:]  // 删除前 st 个元素
  • 原地删除尾部
a = a[:ed]  // 保留前 ed 个元素
  • 原地截取中部
a = a[st:ed]  // 截取索引 [st, ed) 元素
  • 原地删除中部
a = append(a[:st], a[ed:]...)  // 删除索引 [st, ed) 元素
3.4.3.3.3 原地插入
  • 原地末尾追加
a = append(a, b...)
  • 原地首部插入
a = append(b, a...)
  • 原地中间插入
a = append(append(a[:st], b...), a[st:]...)  // 从第 st 个元素开始插入元素

3.5 字符串类型

3.5.1 定义字符串
  • Golang 字符串默认使用 UTF-8 编码。
var a string = "Hello World!"
var b = "Golang 入门经典"
c := "abcABC123"
3.5.2 字符串原型
  • 字符串是引用数据类型,底层使用 [...]byte 存储,而使用 UTF-8 解析。与 Java 相似字符串作常量使用,不直接修改其值。
type StringHeader struct {
    Data uintptr
    Len int
}
3.5.3 字符串操作
  • 许多字符串操作位于库文件 strings 中,许多字符串类型转换位于库文件 strconv 中。
  • 这里假设已经定义了两个变量:
var a = "你好"
var b = "Hello"
3.5.3.1 字符串切片
  • 字符串可以认为是一个静态切片类型,可以进行相应的切片操作。不可使用 appendcopy 函数。
3.5.3.1 字符串拼接
c := a + b  // 运算符 两字符串拼接
c = strings.Join([]string{a, b}, "")  // 函数 多字符串拼接
// 官方推荐的 最高效率
var builder strings.Builder  // 或 builder := strings.Builder{}
builder.WriteString(a)
builder.WriteString(b)
var s string = builder.String()  // 或 s := builder.String()
3.5.3.2 字符串编码
c := []byte(a)  // 转换为 ASCII 编码, 一个 Unicode 字符占用 3 Bytes
d := len(c)     // 6
c := []rune(a)  // 转换为 UTF-8 编码
d := len(c)     // 2
  • string 类型直接使用 len() ,返回值为 ASCII 编码长度。
3.5.3.3 其他操作
func strings.Contains(s string, substr string) bool   // s 是否包含 substr
func strings.HasPrefix(s string, prefix string) bool  // s 是否以 substr 开头
func strings.HasSuffix(s string, suffix string) bool  // s 是否以 substr 结尾
func strings.Index(s string, substr string) int       // substr 在 s 中第一次出现的索引
func strings.LastIndex(s string, substr string) int   // substr 在 s 中最后次出现的索引
func strings.ToLower(s string) string            // 返回 s 的小写形式
func strings.ToUpper(s string) string            // 返回 s 的大写形式
func strings.EqualFold(s string, t string) bool  // 不区分大小写的情况下, s 与 t 是否相等
func strings.Split(s string, sep string) []string   // 以 sep 分割字符串 s
func strings.SplitN(s string, sep string, n int) []string  
// 以 sep 分割字符串 s. 最多将字符串分割为 n 份, -1 表示无穷
func strings.SplitAfter(s string, sep string) []string  // 以 sep 分割字符串 s, sep 会保留在前一个字符串尾
func strings.SplitAfterN(s string, sep string, n int) []string  
// 以 sep 分割字符串 s, sep 会保留在前一个字符串尾. 最多将字符串分割为 n 份, -1 表示无穷
// ??? 为什么 go 没有 rsplit
func strings.Join(elem []string, sep string) string  // 以 sep 衔接 elem 中元素, Split 的逆过程
func strings.Replace(s string, old string, new string, n int) string  
// 将 s 中的 old 替换为 new 至多 n 次, n 为 -1 表示无穷
func strings.ReplaceAll(s string, old string, new string) string      // 替换全部
func string.Repeat(s string, n int) string  // s 重复 n 次
func strings.TrimSpace(s string) string                  // 去除前后空字符
func strings.Trim(s string, chars string) string         // 去除前后 chars 中的字符
func strings.TrimLeft(s string, chars string) string     // 去除前导 chars 中的字符
func strings.TrimRight(s string, chars string) string    // 去除后导 chars 中的字符
func strings.TrimPrerix(s string, prefix string) string  // 去除前导 prefix 字符串至多一次
// func strings.CutPrefix(s string, prefix string) (string, bool)  // 功能相似, 返回是否去除
func strings.TrimSuffix(s string, suffix string) string  // 去除后导 suffix 字符串至多一次
// func strings.CutSuffix(s string, suffix string) (string, bool)  // 功能相似, 返回是否去除
3.5.4 字符串转换
3.5.4.1 转换为字符串类型
func fmt.Sprint(a ...any) string  // 将任意类型转换为字符串, 只要类型满足 fmt.Stringer 接口
// fmt.Sprint(Complex(1, 5))
3.5.4.2 转换自字符串类型
func strconv.ParseBool(s string) (bool, error)  
// 将 string 转为 bool 类型 (true/false), 如果错误存在同时返回
func strconv.ParseComplex(s string, bitSize int) (complex128, error)
// 将 string 转为 complex 类型, 如果错误存在同时返回
// bitSize 为 64/128 表示 complex64/complex128, 一般使用 128
func strconv.ParseFloat(s string, bitSize int) (float64, error)
// 将 string 转为 complex 类型, 如果错误存在同时返回
// bitSize 为 32/64 表示 float32/float64, 一般使用 64
func strconv.ParseInt(s string, base int, bitSize int) (int64, error)
// 将 string 转为 complex 类型, 如果错误存在同时返回
// base 为进制 0/2-36, 特别地, 0 表示自动进制
//     0b 开头被识别为二进制
//     0o 开头被识别为八进制
//     0x 开头被识别为十六进制
//     否则被识别为十进制
// bitSize 为 8/16/32/64 表示 int8/int16/int32/int64, 一般使用 64
func strconv.ParseUint(s string, base int, bitSize int) (uint64, error)  // 与 strconv.ParseInt 相似

3.6 channel 类型

  • channel 用于进行多线程管道通信与同步。
3.6.1 定义 channel
a := make(chan <type>, [<size>])
a := make(chan int)  // 无缓冲区的 int 通道
b := make(chan int, 2)  // 缓冲区大小为 2 的 int 通道
  • 注意:channel也是引用数据类型,直接使用 var a chan int 是错误的,其没有构造 chan 结构。
3.6.2 channel 读写
  • channel 具有缓冲区大小,读写会对 channel 的缓冲区进行操作。当缓冲区为空时,读操作阻塞,知道新的写操作完成;当缓冲区为满时,写操作阻塞。

  • 特殊地,当缓冲区大小为 0 时,所有读写操作都会阻塞等待直到 channel 的读写操作配对。

  • channel

<channel> <- <data>
a <- 1
  • channel
<data> := <-<channel>
x := <-a
  • channel 非阻塞读
// 详见 4.1.3 select-case 语句
select {
case x := <-a:
    <block1>
default:  // 读取失败
    <block0>
}
3.6.3 channel 应用
  • 详见 5.多线程开发

3.7 map 类型

3.7.1 定义 map
a := make(map[<keyType>]<valueType>)
b := map[<keyType>]<valueType>{<key>: <value>...}
a := make(map[string]int)
b := map[string]int{"abc": 1, "dfe": 9}
  • 注意:map也是引用数据类型,直接使用 var a map[<keyType>]<valueType> 是错误的,其没有构造 map 结构。
3.7.2 操作 map
  • 插入或修改元素
a[<key>] = <value>  // 如果元素存在, 插入值, 否则修改值
  • 访问元素
x := a[<key>]  // 如果元素存在, 获取元素值, 否则返回 0 值
  • 删除元素
delete(<mapName>, <key>)  // 如果元素存在, 删除元素, 否则不进行操作
  • 遍历元素
for key, value := range <mapName> {
	// ...
}
for key := range <mapName> {
    // ...
}
3.7.3 map 原理
  • map 底层为优化的拉链式哈希表,因此 map 是无序的。关于拉链式哈希表不多作介绍。
  • map 对拉链式哈希表作的优化包括:
    • 链表内存访问较慢,map 使用数组链表加速访问。数组链表是指每次申请一定数量的连续内存,连续内存溢出时再由连续内存链接到下一个申请的连续内存,一块连续内存可以存储多个键值对,这样一块连续内存称为 overflow(不包括第一块连续内存)。
    • 键比对可能较慢(如以字符串作为键),map 使用二级哈希值进行比对,在链表中仅当二级指针相同时才进行键比对。
  • map 查找工作流程:
    • 使用 key 类型对应的哈希函数计算足够大的哈希值 hashCode,取 hashCodeB 位作为一级哈希值 firstlyHashCode,取 hashCode 的高 8 位作为二级哈希值 secondaryHashCode,其中 B 的值取决于哈希表扩容后的大小 AB=log2(A)
    • firstlyHashCode 作为索引,索引到一个数组链表。
    • secodaryHashCode 作为键依次于数组链表的第一块连续内存中的 secodaryHashCode 比较,如果相同则继续比较 key,如果相同则认为找到,否则继续向后找。
    • 如果当前连续内存中没有找到,那么前往当前内存的下一个 overflow 以相同方式查找,如果没有更多 overflow,认为不存在。
  • map 扩容策略:
    • 如果满足以下条件之一,map 将申请一块 2 倍的连续内存,并将原表数据逐一转储到新表。
      • 负载因子 = 键数量 / 桶数量 A > 6.5 负载因子 = 键数量 / 桶数量A > 6.5 负载因子=键数量/桶数量A>6.5
      • 溢出桶 o v e r f l o w 数量 > 32768 溢出桶overflow数量 > 32768 溢出桶overflow数量>32768
    • 扩容后,负载因子折半,溢出桶数量均摊折半。

3.8 函数

3.8.1 具名函数与匿名函数
  • 函数按分类可以分为具名函数与匿名函数,函数是一个对象即一种数据类型,具名函数是一种特殊的匿名函数。
  • 具名函数
func <functionName>([parameterList...]) [returnTypes...] {
	<block>
}
func DivMod(x int, y int) (int, int) {
	return x / y, x % y
}
  • 匿名函数
<functionName>([parameterList...]) [returnTypes...] {
    <block>
}
DivMod := func(x int, y int) (int, int) {
    return x / y, x % y
}
3.8.2 函数传参
  • 每项参数使用 parameterName parameterType 的格式,见 3.8.1 的例子。

  • Golang 中没有引用类型,避免参数拷贝可使用指针类型 *parameterType

  • Golangargs... 表示解包,...args 表示封包,对应 Python 中的 *args ,其实际构造一个切片。

Sum := func(a ...int) int {
    s := 0
    for _, i := range a {
        s += i
	}
    return s
}
fmt.Println(Sum([]int{1, 2, 3, 4, 5}...))
fmt.Println(Sum(1, 2, 3, 4, 5))
  • Golang 可以进行批量参数类型定义。
func demo(a, b int, s, ss string) {}
// func demo(a int, b int, s string, ss string) {}
  • Golang 中没有函数默认值、函数重载。
3.8.3 函数返回
  • 如果未指定函数返回值,需用 return 返回相同的返回值列表。
  • 如果指定函数返回值名,直接使用 return 返回即可。此时函数返回值已定义,无需在函数体内定义。
Sum := func(a ...int) (s int) {
    s = 0  // 无需重复定义
    for _, i := range a {
        s += i
	}
    return  // 无需重复指定返回值
}
fmt.Println(Sum([]int{1, 2, 3, 4, 5}...))
fmt.Println(Sum(1, 2, 3, 4, 5))

3.9 方法

  • Golang 实际没有类与对象的概念,方法是 Golang 的类面向对象实现,它实现了对象与函数的静态绑定。其调用方式与面向对象相同。
// 函数实现
type File struct {
    fd int
    flag uint
}

func open(file *File, fp string, mode string) {
    // ...
}

func close(file *File) {
    // ...
}

func main() {
    file := File{}
    open(&file, "data.txt", "w")
    close(&file)
}
// 方法实现
type File struct {
	fd int
    flag uint
}

func (file *File) open(fp string, mode string) error {
    // ...
}

func (file *File) close(fp string, mode string) error {
	// ...
}

func main() {
    file := File{}
    file.open("data.txt", "w")
    file.close()
}
  • 方法的对象必须是本包结构体。例如 func (builder strings.Builder) <functionName>([parameterList...]) [returnTypes...] 是错误的,func (x int) <functionName>([parameterList...]) [returnTypes...] 是错误的。
  • 值对象与指针对象是不同的,但它们的调用方式是相同的。以值对象传递,将对对象的拷贝操作;以指针对象传递,将直接操作对象。

3.10 接口

  • 接口也是 Golang 中类面向对象的实现,接口的存在类似于面向对象中的虚基类。接口是方法的集合,任何包含所有接口定义的方法的结构体都满足接口。接口可以被赋值为任何满足接口的结构体(隐式类型转换),但不能使用非接口定义的字段、方法。
3.10.1 定义接口
  • 定义接口。任何满足接口所有方法的结构体都可以赋值到接口。
type Human interface {
    who() string
    say(data interface{})  // 参数为匿名空接口类型
    gender() bool
}
  • 定义空接口。任何结构体都可以赋值到空接口,空接口在底层原理上与一般接口不同。
type Any interface {
}
  • 叠加接口
type Writer interface {
    write(data []byte) (int, error)
}

type Reader interface {
	read(n int) ([]byte, error)
}

type WriteReader interface {
    Writer
    Reader
}
  • 匿名接口。在第一个示例中,interface{} 即为匿名接口。
var i interface{} = interface{}("Hello")
3.10.2 动态类型
  • 接口可以接收一切符合接口的类型,因此可以用于动态类型。
package main

import (
	"fmt"
	"unsafe"
)

type Human interface {
	who() string
	say(data interface{}) // 参数为匿名空接口类型
	gender() bool
}

type Man struct {
	name string
}

func (man Man) who() string {
	fmt.Println(unsafe.Pointer(&man), unsafe.Sizeof(man))
	return man.name
}

func (man Man) say(data interface{}) {
	fmt.Println("He is", man.name, data)
}

func (man Man) gender() bool {
	return true
}

type Woman struct {
	name string
}

func (woman Woman) who() string {
	return woman.name
}

func (woman Woman) say(data interface{}) {
	fmt.Println("She is", woman.name, data)
}

func (woman Woman) gender() bool {
	return false
}

type Me struct {
	name string
}

func (me *Me) who() string {
	return me.name
}

func (me *Me) say(data interface{}) {
	fmt.Println("He is", me.name, data)
}

func (me *Me) gender() bool {
	return true
}

func (me *Me) sleeping() bool {
	return true
}

func main() {
	var human Human

	human = Man{"Mike"}
	fmt.Println(human.who())
	human.say("123")
	fmt.Println("Gender:", human.gender())
	fmt.Println()

	human = Woman{"Jane"}
	fmt.Println(human.who())
	human.say("456")
	fmt.Println("Gender:", human.gender())
	fmt.Println()

	human = &Me{"Jamhus"}  // 指针类型, 因为其方法全部为指针类型
	fmt.Println(human.who())
	human.say("789")
	fmt.Println("Gender:", human.gender())
	fmt.Println()
}
3.10.3 类型断言
  • Golang 中接口支持返回到原始类型,判断接口原始类型的方法称为类型断言。类型断言使 Golang 中的动态类型更加灵活。
3.10.3.1 类型判断
  • value 为接收原始类型的变量,其类型为 originalTypeok 为判断 interfaceVariable 的原始类型是否为 originalType,判断为否,value 将没有意义。
<value>, <ok> := <interfaceVariable>.(<originalType>)
// 类型断言还有一种写法 <value> := <interfaceVariable>.(<originalType>) 此写法判断为否将抛出异常, 详见 4.4 异常处理
human := Human(Me("Jamhus"))

value1, ok := human.(Man)
if ok {
    fmt.Println("It's Man.", value1)
} else {
    fmt.Println("It's not Man.")
}

value3, ok := human.(Me)
if ok {
    fmt.Println("It's Me.", value3)
} else {
    fmt.Println("It's not Me.")
}
It's not Man.
It's Me. {Jamhus}
3.10.3.2 类型分支
switch human.(type) {
case Man:
    fmt.Println("It's Man", human.(Man))
case Woman:
    fmt.Println("It's Woman", human.(Woman))
case Me:
    fmt.Println("It's Man", human.(Me))
}

4. 控制语句

  • Golang 中的条件控制语句与 Clang 相似,但省去了许多不必要的附加符号,提供必要的精简方式。

4.1 条件控制语句

4.1.1 if-else 语句
  • Clang 不同的是 Golang 中的表达式都省去了 ()
if <condition1> {
    <block1>
} else if <condition2> {
    <block2>
} else {
    <block0>
}

Example:

if returnValue == -1 {
    fmt.Println("Something Error.")
}
if weight < 240 {
    fmt.Println("Weight OK!")
} else {
    fmt.Println("Too weight.")
}
if score < 60 {
    fmt.Println("F")
} else if score < 70 {
    fmt.Println("P")
} else if (score < 80) {
    fmt.Println("C")
} else if (score < 90) {
    fmt.Println("B")
} else {
    fmt.Println("A")
}
4.1.2 switch-case 语句

Usage 1:

switch <variable> {
case <value1>:
    <block1>
case <value2>:
    <block2>
...
default:
    <block0>
}

Example:

switch level {
case 1:
    fmt.Println("P")
case 2:
    fmt.Println("C")
case 3:
    fmt.Println("B")
case 4:
    fmt.Println("A")
default:
    fmt.Println("F")
}

Usage 2:

switch {
case <condition1>:
	<block1>
case <condition2>:
	<block2>
...
default:
    <block0>
}

Example:

switch {
case score < 60:
    fmt.Println("F")
case score < 70:
    fmt.Println("P")
case score < 80:
    fmt.Println("C")
case score < 90:
    fmt.Println("B")
default:
    fmt.Println("A")
}
4.1.3 select-case 语句
  • expression 必须是一个通信操作,如 x := <-c
  • select 语句将随机抽取一个 case,如果通信成功将运行 block;如果通信失败并且存在 default 将运行 block0
select {
case <expression1>:
    <block1>
case <expression2>:
    <block2>
default:
    <block0>
}

Example:

var c1 = make(chan string)
var c2 = make(chan string)

func Thread1() {
	time.Sleep(time.Millisecond * 2100)  // A1
	c1 <- "Thread1 is ready."
}

func Thread2() {
	time.Sleep(time.Millisecond * 2100)  // A1
	c2 <- "Thread2 is ready."
}

func ThreadMain() {
	for i := 0; i < 10; i++ {
		select {
		case x := <-c1:
			fmt.Println("Get:", x)
		case x := <-c2:
			fmt.Println("Get:", x)
		default:
			fmt.Println("Get Nothing.")
			time.Sleep(time.Millisecond * 500)  // A2
		}
	}
}

func main() {
	go Thread1()
	go Thread2()
	ThreadMain()
}
Get Nothing.
Get Nothing.
Get Nothing.
Get Nothing.
Get Nothing.
Get: Thread1 is ready.
Get: Thread2 is ready.
Get Nothing.
Get Nothing.
Get Nothing.
  • 注意在案例输出中,Thread1 is ready.Thread2 is ready. 先后是随机的,因为到第五次循环(2500msThread1Thread2 都准备就绪。
  • 如果将 A1 处参数修改为 2000,则 Thread1 is ready. 在先概率更高,因为 Thread1Thread2 理论上恰好同时在第四次循环(2000ms)准备就绪,而线程启动需要其他准备时间且 Thread1 先于 Thread2 启动。

4.2 循环控制语句

4.2.1 for 语句
GolangClang
for <init>; <condition>; <post> { <block> }for (<init>; <condition>; <post>) { <block>; }
for <condition> { <block> }while (<condition>) { <block>; }
for { <block> }while (true) { <block>; }
  • Golang 中的 for 语句 ; 间的语句也可以是空语句

Example:

for i := 0; i < 10; i++ {
    fmt.Println(i)
}
var i int = 0
// for ; i < 10; {
//     fmt.Println(i)
//     i++
// }
for i < 10 {
    fmt.Println(i)
    i++
}
// for ;; {
//     fmt.Println("Running...")
//     time.Sleep(time.Millisecond * 500)
// }
for {
    fmt.Println("Running...")
    time.Sleep(time.Millisecond * 500)
}
4.2.2 跳转语句
  • continue:跳转到下一个 for 循环。

    for i := 0; i < 5; i++ {
        if i == 3 {
            continue
    	}
        fmt.Println(i)
    }
    
  • break:跳转到当前 for 循环外。

    for i := 0; i < 5; i++ {
        if i == 3 {
            break
    	}
        fmt.Println(i)
    }
    
  • goto:跳转到指定标签

    for i := 0; i < 5; i++ {
        for j := 0; j < 5; j++ {
            if i == 3 and j == 3 {
                goto tag
    		}
            fmt.Println(i, j)
    	}
    }
    tag:
    
4.2.3 for-range 语句
slice := []int{1, 2, 3, 4, 5}
for key, value := range slice {
    fmt.Println(key, value)
}
  • 数组、stringslicemapchannel 等都可以使用 for-range 语句遍历。其中 channel 没有 key 且阻塞。
channel := make(chan int, 10)
for value := range channel {
    fmt.Println(value)
}

4.3 特殊控制语句

  • Golang 从语法上支持并发,它有其特殊的控制语句。
4.3.1 defer 语句
4.3.1.1 defer 语句用法
  • defer 语句用于将操作延迟到函数结束执行,其操作甚至迟于 return 操作。
  • defer 语句将延迟操作压栈,在结束时逆序执行。
  • defer 语句必须使用函数。可以使用匿名函数。
defer <functionCall>
func Echo() {
    defer fmt.Println("Function Exited.")
    defer fmt.Println("Echoed.")
    fmt.Println("Hello World!")
}
Hello World!
Echoed.
Function Exited.
4.3.1.2 defer 语句怪用
  • 谈及 defer 原理,defer 语句将延迟操作压栈,压栈数据包括:函数名、函数参数(临时)地址。后面的所有怪用都基于这一原理,defer 语句可以修改和访问本该已卸载的内存。defer 压栈后实际对地址又进行了一次引用,因此 Golang 的垃圾回收机制(GC)实际没有卸载这些内存。

  • 修改函数返回值。该案例中使用匿名返回值得不到相同效果,个人猜测是因为匿名返回值返回时进行了拷贝。

func Demo() (s string) {
    s = "Edit by Function"
    defer func() { s = "Edit by Defer" }
    return s
}
Edit by Defer
  • 循环延迟返回值相同。该案例中,Demo1defer 访问 for 循环定义的 i,在程序结束时访问其值为 3Demo2defer 访问每次循环体内定义的 i,可以认为是循环变量 i 的快照版本。
func Demo1() {
    for i := 0; i < 3; i++ {
        defer fmt.Println(i)
    }
}
3
3
3
func Demo2() {
    for i := 0; i < 3; i++ {
        i := i
        defer fmt.Println(i)
    }
}
2
1
0
4.3.2 go 语句
  • go 语句将创建一个 gorountine,可以简单认为是一个线程或协程。go 语句的对象是一个函数,将函数作为一个新的线程运行。
  • 关于什么是线程,不作赘述。
func thread() {
	for i := 0; i < 5; i++ {
		fmt.Println("Thread", i)
		time.Sleep(time.Microsecond * 50)
	}
}

func main() {
	go thread()
	for i := 0; i < 5; i++ {
		fmt.Println("Main", i)
		time.Sleep(time.Microsecond * 50)
	}
}
Main 0
Thread 0
Thread 1
Main 1
Main 2
Thread 2
Main 3
Thread 3
Thread 4
Main 4

4.4 异常处理

4.4.1 error 接口
4.4.1.1 接口原型
type error interface {
    Error() string
}
4.4.1.2 构造异常
err := errors.New("异常信息")  // error: "异常信息"
err := fmt.Errorf("错误信息: %v", "异常信息")  // errors.New(fmt.Sprintf(...))
4.4.1.3 捕捉异常
func tryInt(s string) {
	i, err := strconv.ParseInt(s, 0, 64)
	if err == nil {
		fmt.Println("Value:", i)
	} else {
		fmt.Println("Error:", err)
	}
}

func main() {
	tryInt("123123")
	tryInt("123a123")
}
Value: 123123
Error: strconv.ParseInt: parsing "123a123": invalid syntax
4.4.2 panic() 与 recover()
  • Golang 中没有 try-catch 语句,而使用 panic-recover 进行异常控制,两者具有一定区别。

  • func panic(v any)(崩溃)将产生异常,如果异常不被捕捉,将抛出命令行错误并终止程序。

  • func recover() any(恢复)用于捕捉异常,只在 defer 函数中生效。

4.4.2.1 造成 panic 的场景
  • 索引或指针无效或越界
  • 向关闭的 channel 发送消息
  • 类型断言(不获取 ok
  • 用户发送 panic
4.4.2.2 捕捉与处理异常
defer func() {
    err := recover()
    if err != nil {
        fmt.Printf("捕捉异常: %T %v\n", err, err)
    }
}()
// defer recover() 无效
// defer func() { recover() }() 有效
panic("抛出异常")
捕捉异常: string 抛出异常
  • 由于 recover() 只在 defer 函数中生效,只能在函数结束处理异常。

5. 多线程开发

  • Golang 原生支持多线程开发,因此多线程开发是学习 Golang 不可少的环节。

5.1 多线程相关语法

  • 详见 3.6 channel 类型
  • 详见 4.1.3 select-case 语句
  • 详见 4.3.2 go 语句

5.2 多线程 Golang 解决方案

5.2.1 等待子线程结束
  • 众所周知,主线程结束将结束所有子线程。其他语言提供了 waitjoin 等方法等待子线程正常结束,在 Golang 中可以使用 channel 等待线程结束。
5.2.1.1 未等待结果
  • 在下面这个案例中,你将会看到未等待子线程结束时,子线程未执行任何代码。因为在此情况下,主线程结束将立即杀死子线程。
func thread() {
	for i := 0; i < 5; i++ {
		fmt.Println("Thread", i)
		time.Sleep(time.Microsecond * 50)
	}
}

func main() {
	go thread()
	fmt.Println("Main ended.")
}
Main ended.
5.2.1.2 等待的结果
  • 通过管道阻塞,我们可以做到使主线程等待子线程结束。
  • 需要注意的是,这个案例中多线程复用无缓冲通道,如果情况更复杂可能需要考虑使用多通道或者有缓冲。通常此案例的方法是足够的。
var wait = make(chan any)

func thread() {
	defer func() { wait <- nil }()  
    // 使用 defer 可以避免 忘记发出信号 / 线程提前返回 / 线程异常终止 导致的信号未发出, 信号未发出将导致主线程阻塞
	for i := 0; i < 5; i++ {
		fmt.Println("Thread", i)
		time.Sleep(time.Microsecond * 50)
	}
}

func main() {
	go thread()
	time.Sleep(time.Second)
	go thread()
	<-wait
	<-wait
	fmt.Println("Main ended.")
}
Thread 0
Thread 1
Thread 2
Thread 3
Thread 4
Thread 0
Thread 1
Thread 2
Thread 3
Thread 4
Main ended.
5.2.2 无缓冲线程同步
  • 无缓冲 channel 可以保证两个线程高度同步,但频繁使用该操作会降低多线程的效率,甚至劣于单线程。
5.2.2.1 事务递交
var wait = make(chan any)
var chan1 = make(chan int)
var chan2 = make(chan int)

func thread1() {
	defer func() { wait <- nil }()
	issue := <-chan1
	fmt.Println("Jamhus: Ok. Mr.Smith.")
	fmt.Println("Jamhus: Hello, Mike. Help me slove the issue", issue)
	chan2 <- issue
}

func thread2() {
	defer func() { wait <- nil }()
	<-chan2
	fmt.Println("Mike: Ok. Mr.Jamhus. I will do it...")
}

func main() {
	issue := 114514
	go thread1()
	go thread2()
	fmt.Println("Smith: Hello, Jamhus. Help me slove the issue", issue)
	chan1 <- issue
	<-wait
	<-wait
}
Smith: Hello, Jamhus. Help me slove the issue 114514
Jamhus: Ok. Mr.Smith.
Jamhus: Hello, Mike. Help me slove the issue 114514
Mike: Ok. Mr.Jamhus. I will do it...
5.2.2.2 线程对话
var wait = make(chan any)

func thread1(info []string, from chan string, to chan string) {
	defer func() { wait <- nil }()
	for _, send := range info {
		recv := <-from
		fmt.Println("Thread 1 Receive", recv)

		fmt.Println("Thread 1 Send   ", send)
		to <- send

		time.Sleep(time.Millisecond * 500)
	}
	fmt.Println("Thread 1 Finished.")
}

func thread2(info []string, from chan string, to chan string) {
	defer func() { wait <- nil }()
	for _, send := range info {
		fmt.Println("Thread 2 Send   ", send)
		to <- send

		recv := <-from
		fmt.Println("Thread 2 Receive", recv)

		time.Sleep(time.Millisecond * 500)
	}
	fmt.Println("Thread 2 Finished.")
}

func main() {
	chan1 := make(chan string)
	chan2 := make(chan string)
	go thread1([]string{
		"I'm doing well, thank you! How about you, Jamhus?",
		"Well, first I need to finish up some reports for the meeting this afternoon...",
		"Thanks for offering, but I think I can handle it.",
	}, chan1, chan2)
	go thread2([]string{
		"Good morning, Mr.Smith! How are you today?",
		"I'm great, thanks for asking. So, what's on the agenda for today?",
		"Sounds busy! Do you need any help with the reports?",
	}, chan2, chan1)
	<-wait
	<-wait
}
Thread 2 Send    Good morning, Mr.Smith! How are you today?
Thread 1 Receive Good morning, Mr.Smith! How are you today?
Thread 1 Send    I'm doing well, thank you! How about you, Jamhus?
Thread 2 Receive I'm doing well, thank you! How about you, Jamhus?
Thread 2 Send    I'm great, thanks for asking. So, what's on the agenda for today?
Thread 1 Receive I'm great, thanks for asking. So, what's on the agenda for today?
Thread 1 Send    Well, first I need to finish up some reports for the meeting this afternoon...
Thread 2 Receive Well, first I need to finish up some reports for the meeting this afternoon...
Thread 2 Send    Sounds busy! Do you need any help with the reports?
Thread 1 Receive Sounds busy! Do you need any help with the reports?
Thread 1 Send    Thanks for offering, but I think I can handle it.
Thread 2 Receive Thanks for offering, but I think I can handle it.
Thread 2 Finished.
Thread 1 Finished.
5.2.3 缓冲任务分发
  • 使用 channel 进行任务分发可以避免资源的重复占用或任务的重复执行。
5.2.3.1 景区售票模型
  • 在这个案例中,我们实现了两个功能,并且发现了两个疑点:
    • 两个功能:
      • 每张票只销售一次。程序通过 manager 线程向子线程发送 ,从而保证了每张票只销售一次。
      • 发出售罄信号。manager 线程在完成 的生产任务后会设置 noTicketRest 值,如果通道为空且该值为真,salesman 线程可以认为票售罄。
    • 两个疑点:
      • 票的销售不是连续的。我们理想的情况应该是 salesman 线程交替销售连续的票号。我们可以将售票的过程抽象为 取票交易报告 三个过程,而打印结果显示的是 报告 的顺序。由于在此案例中售票过程高速进行,可能会出现 salesman1 取票但尚未完成 交易报告 环节时,salesman2 完成了售票的全过程。至于 salesman2 为何可以晚于 salesman1 开始而提前完成全过程,需用时间片轮转原理解释。
      • 票的断供情况。很显然票的断供是由 manager 引起的,salesman 销售完了缓冲区全部的票而 manager 没有及时补充,解决这个问题可以扩大缓冲区。在凉皮的 Laptop 上,将缓冲区设置为 20 基本上解决了这个问题。
var wait = make(chan any)         // 等待线程结束
var tickets = make(chan int, 10)  // 售票缓存
var noTicketsRest = false         // 票已全入缓存

func manager() {
	defer func() { wait <- nil }()
	for i := 1; i <= 100; i++ {
		tickets <- i  // 如果缓存满了此处会阻塞
	}
	noTicketsRest = true
}

func salesman(id int) {
	defer func() { wait <- nil }()  // 使用 defer 可以避免忘记发送信号或提前 return
	for {
		select {
		case ticketId := <-tickets:
			fmt.Printf("Salesman%v sales ticket No.%v.\n", id, ticketId)
		default:
			if noTicketsRest {
				return  // 售票缓存为空且票已全入缓存
			}
			fmt.Printf("Salesman%v waits for the next ticket.\n", id)
		}
		// time.Sleep(time.Millisecond)
	}
}

func main() {
	go salesman(1)
	go salesman(2)
	go manager()
	<-wait
	<-wait
	<-wait
}
Salesman2 waits for the next ticket.
Salesman1 waits for the next ticket.
Salesman1 sales ticket No.2.
Salesman1 sales ticket No.3.
Salesman1 sales ticket No.4.
Salesman1 sales ticket No.5.
Salesman1 sales ticket No.6.
Salesman1 sales ticket No.7.
Salesman1 sales ticket No.8.
Salesman1 sales ticket No.9.
Salesman2 sales ticket No.1.
Salesman2 sales ticket No.11.
Salesman2 sales ticket No.12.
Salesman2 sales ticket No.13.
Salesman2 sales ticket No.14.
Salesman2 sales ticket No.15.
Salesman1 sales ticket No.10.
Salesman2 sales ticket No.16.
Salesman2 sales ticket No.18.
Salesman1 sales ticket No.17.
Salesman1 sales ticket No.20.
Salesman1 sales ticket No.21.
Salesman1 sales ticket No.22.
Salesman1 sales ticket No.23.
Salesman1 sales ticket No.24.
Salesman1 sales ticket No.25.
Salesman1 sales ticket No.26.
Salesman1 sales ticket No.27.
Salesman1 sales ticket No.28.
Salesman1 sales ticket No.29.
Salesman1 sales ticket No.30.
Salesman1 waits for the next ticket.
Salesman1 waits for the next ticket.
Salesman1 waits for the next ticket.
Salesman1 waits for the next ticket.
Salesman1 waits for the next ticket.
Salesman1 waits for the next ticket.
Salesman1 waits for the next ticket.
Salesman2 sales ticket No.19.
Salesman2 sales ticket No.32.
Salesman1 sales ticket No.31.
Salesman1 sales ticket No.34.
Salesman1 sales ticket No.35.
Salesman2 sales ticket No.33.
Salesman2 sales ticket No.37.
Salesman2 sales ticket No.38.
Salesman1 sales ticket No.36.
Salesman1 sales ticket No.40.
Salesman1 sales ticket No.41.
Salesman1 sales ticket No.42.
Salesman1 sales ticket No.43.
Salesman1 sales ticket No.44.
Salesman1 sales ticket No.45.
Salesman1 sales ticket No.46.
Salesman1 sales ticket No.47.
Salesman1 sales ticket No.48.
Salesman1 sales ticket No.49.
Salesman1 sales ticket No.50.
Salesman1 waits for the next ticket.
Salesman1 waits for the next ticket.
Salesman1 waits for the next ticket.
Salesman1 waits for the next ticket.
Salesman1 waits for the next ticket.
Salesman1 waits for the next ticket.
Salesman1 waits for the next ticket.
Salesman1 sales ticket No.51.
Salesman1 sales ticket No.52.
Salesman1 sales ticket No.53.
Salesman1 sales ticket No.54.
Salesman1 sales ticket No.55.
Salesman1 sales ticket No.56.
Salesman1 sales ticket No.57.
Salesman2 sales ticket No.39.
Salesman2 sales ticket No.59.
Salesman2 sales ticket No.60.
Salesman2 sales ticket No.61.
Salesman2 sales ticket No.62.
Salesman2 sales ticket No.63.
Salesman2 sales ticket No.64.
Salesman2 sales ticket No.65.
Salesman2 sales ticket No.66.
Salesman2 sales ticket No.67.
Salesman2 sales ticket No.68.
Salesman2 sales ticket No.69.
Salesman2 sales ticket No.70.
Salesman2 waits for the next ticket.
Salesman2 waits for the next ticket.
Salesman2 waits for the next ticket.
Salesman2 waits for the next ticket.
Salesman2 waits for the next ticket.
Salesman2 waits for the next ticket.
Salesman2 waits for the next ticket.
Salesman2 waits for the next ticket.
Salesman2 waits for the next ticket.
Salesman1 sales ticket No.58.
Salesman1 sales ticket No.71.
Salesman1 sales ticket No.72.
Salesman1 sales ticket No.73.
Salesman1 sales ticket No.74.
Salesman1 sales ticket No.75.
Salesman1 sales ticket No.76.
Salesman1 sales ticket No.77.
Salesman1 sales ticket No.78.
Salesman2 waits for the next ticket.
Salesman2 sales ticket No.80.
Salesman2 sales ticket No.81.
Salesman1 sales ticket No.79.
Salesman1 sales ticket No.83.
Salesman2 sales ticket No.82.
Salesman2 sales ticket No.85.
Salesman1 sales ticket No.84.
Salesman1 sales ticket No.87.
Salesman1 sales ticket No.88.
Salesman1 sales ticket No.89.
Salesman1 sales ticket No.90.
Salesman1 sales ticket No.91.
Salesman1 sales ticket No.92.
Salesman1 sales ticket No.93.
Salesman1 sales ticket No.94.
Salesman1 sales ticket No.95.
Salesman1 sales ticket No.96.
Salesman1 sales ticket No.97.
Salesman1 waits for the next ticket.
Salesman1 waits for the next ticket.
Salesman1 waits for the next ticket.
Salesman1 waits for the next ticket.
Salesman1 waits for the next ticket.
Salesman1 waits for the next ticket.
Salesman1 waits for the next ticket.
Salesman1 sales ticket No.98.
Salesman1 sales ticket No.99.
Salesman1 sales ticket No.100.
Salesman2 sales ticket No.86.
5.2.3.2 爬虫任务分发
  • 爬虫经常使用多进程、多线程等进行加速。将上面的景区售票模型中的 票Id 改为 url 即是一个爬虫任务分发模型。manager 负责解析并获取 urlsalesman 负责下载 url 并解析。
  • 由于 票Id 是连续的,可以使用互斥量实现,而爬虫任务分发使用通道实现更佳,除非 url 具有一定逻辑性。
5.2.4 消息广播
5.2.4.1 开放式广播
  • 开放式广播的特点是高效且节约内存,但无法指定广播对象。
  • 开放式广播有两种实现方式:
    • 使用全局变量存储状态,接收方定时查看变量状态。5.2.3.1 景区售票模型 中的 noTicketRest 即是这种方式。局限性是消息状态有限,消息状态可能未被接收方捕获又被更改,仅用于简单广播。
    • 使用链表存储消息序列,接收方持有指针向后查看链表。局限性是代码实现相对复杂,历史消息不处理会一直占用内存。
5.2.4.1.1 通过变量广播
5.2.4.1.2 通过链表广播
package main

import (
	"container/list"
	"fmt"
	"time"
)

var wait = make(chan any)
var msg = list.New()

func reciever(name string) {
	defer func() { wait <- nil }()
	msgIterator := msg.Front()
	for {
		if msgIterator != msg.Back() {
			msgIterator = msgIterator.Next()
			x := msgIterator.Value
			if x == nil {
				break
			}
			fmt.Println(name, x)
		}
	}
}

func sender() {
	defer func() { wait <- nil }()
	msg.PushBack("123")
	msg.PushBack("abc")
	msg.PushBack("XYZ")
	msg.PushBack(nil)
}

func main() {
	msg.PushBack(nil)
	go reciever("Thread1")
	go sender()
	go reciever("Thread2")
	time.Sleep(time.Second)
	go reciever("Thread3")
	<-wait
	<-wait
	<-wait
	<-wait
}
Thread2 123
Thread2 abc
Thread2 XYZ
Thread1 123
Thread1 abc
Thread1 XYZ
Thread3 123
Thread3 abc
Thread3 XYZ
5.2.4.2 一对一广播
  • 一对一广播为每个线程创建通道,所有发送方消息都由广播线程代理发送到所有或指定接收方,消息转发具有一定开销。
package main

import (
	"fmt"
	"time"
)

type aa struct {
	message chan any
	name    string
}

func (p *aa) Run() {
	defer close(p.message)
	for {
		select {
		case ss := <-p.message:
			fmt.Println(p.name, ss)
		case <-Exitp:
			fmt.Println("exit :", p.name)
			return
		}
	}
}

func broadcast() {
	for event := range Eventbus { //不建议此种方式接收chan数据,建议用select
		for _, v := range Reicevermap {
			v.message <- event
		}
	}
}

var Eventbus chan any
var Exitp chan any
var Reicevermap map[string]*aa

func main() {
	Exitp = make(chan any) //控制go程退出
	Eventbus = make(chan any, 1)
	Reicevermap = make(map[string]*aa)
	a := &aa{make(chan any, 3), "aa"} //aa接收者
	go a.Run()                        //aa接收广播线程
	Reicevermap[a.name] = a
	b := &aa{make(chan any, 3), "bb"} //bb接收者
	go b.Run()                        //bb接收广播线程
	Reicevermap[b.name] = b
	fmt.Println("111")
	go broadcast()
	time.Sleep(time.Duration(1) * time.Second)
	Eventbus <- "第一个广播"
	time.Sleep(time.Duration(3) * time.Second)
	close(Exitp)
	time.Sleep(time.Duration(3) * time.Second)
}
111
bb 第一个广播
aa 第一个广播
exit : bb
exit : aa
5.2.5 互斥量
  • 虽然 Golang 官方推荐尽量使用 channel 实现线程间通信,并且互斥量可以使用缓冲为 1 的 channel 实现。但互斥量一般使用 实现,没必要滥用 channel
5.2.6 单通道多线程读写
  • 单通道一读一写:5.2.2 无缓冲线程同步 中的两个案例即为这种模式。

  • 单通道多读一写:5.2.1 等待子线程结束 即为这种模式。

  • 单通道一读多写:5.2.3 缓冲任务分发 中的两个案例即为这种模式。

  • 单通道多读多写:最复杂且难以控制的模式,应该尽量避免。难以避免需要谨慎使用,也可考虑使用中转线程对通道内消息筛选后转发。此处不作案例。

6. 类面向对象设计

  • Golang 并不支持面向对象设计,但是它一些特性使许多面向对象的设计方法得以引入。

6.1 数据存储 - 结构体

  • 对象是变量与函数的集合,在 Golang 中,变量对应结构体的字段、函数对应结构体的方法。在 Golang 中结构体存储了对象的所有变量。

  • 详见 3.2 结构体类型

6.2 静态绑定 - 方法

  • 对象是变量与函数的集合,在 Golang 中,变量对应结构体的字段、函数对应结构体的方法。在 Golang 中方法实现了对象到函数的静态绑定。
  • 详见 3.9 方法

6.3 继承

  • Golang 中结构体支持继承,其继承同时继承其字段与方法。

  • 详见 3.2.5 结构体类型的嵌套与继承

package main

import "fmt"

type A struct {
	name string
}

func (a *A) getName() string {
	return a.name
}

type B struct {
	A
}

func main() {
	b := B{A{"abab"}}  // 注意构造方式
	fmt.Println(b.getName())
}

6.4 虚类与反射 - 接口

  • 虚类是变量与未实现的函数声明的集合。接口是未实现的函数声明的集合,可以接受一切满足接口方法的对象。反射是从基类到派生类的过程。
  • Golang 中虚类对应接口,反射对应类型断言。
  • 详见 3.10 接口 详见 3.10.3 类型断言

7. 包管理

  • Golang 提供了包管理方式,方便项目的分类管理。

7.1 项目结构

项目路径 - 项目名
包路径 - 包名
包路径 - 包名
...
包路径 src - 包名 main
源文件
源文件
...
源文件
源文件
源文件
...
...
源文件 main.go
  • 项目结构包括:
    • 项目路径:项目目录在此电脑上的位置。
    • 项目名go mod init 时为项目命的名,与项目路径呈对应关系,通常与项目路径最后一级命名相同。
    • 包路径:导包时使用包路径,包是最后一级目录,包路径为 标准库路径项目名/包相对于项目路径。其中 main 包 路径应为 项目名/src
    • 包名:打包时定义包名,程序内使用包名区别各个包,与包路径呈对应关系,通常与包路径最后一级命名相同。其中 main 包 包名应为 main
    • 源文件:写有代码的 .go 文件,一个包可以有多个源文件,一个包内所有源文件的包名必须相同,不同包间所有源文件的包名必须不同。

7.2 新建项目、打包、导包

7.2.1 新建项目
  • 如果一个项目涉及多个自定义包,应该新建项目。
  • 在执行 go mod init 命令后,项目目录下会生成 go.mod 文件,该目录下所有文件属于该项目。
cd {项目路径}
go mod init {项目名}
cd D:/Go/Project/Test
go mod init testProject
7.2.2 打包
  • 所有文件必须打包,打包语句必须位于程序第一行。
  • main 是特殊的包,main 包必须包含 main 函数(main 函数不能有任何参数和返回值),非 main 包不得包含 main 函数。
package <包名>  // 一般以小写字母开头, 打包时定义包名
package main
7.2.3 导包
  • 导包 import 应该紧跟在打包语句后,注意导包时使用包路径而非包名。
  • 如果导包使用别名,访问包内对象应使用别名;否则,访问包内对象应使用包名。
import "<包路径>"  // 单个包导入
import (
    "<包路径1>"
    "<包路径2>"
    ...
)  // 多个包导入
import <别名> "<包路径>"  // 使用别名
import (
	"fmt"  // 标准库包
    "testProject/packageDirectory1"  // 自定义包
)
7.2.4 运行项目
cd {项目路径}/src
go build
./src.exe

7.3 全局作用域

  • 直接定义在包中的函数、变量都是全局变量。

  • 全局变量一旦被定义将始终存在,直到程序终止。

  • 全局变量可以在包之间共享,它分为:

    • 受保护(Protected),所有内部包可以相互访问。所有小写字母开头的全局变量、函数是受保护的。
    • 公有(Public),允许外部包访问共有成员。所有大写字母开头的全局变量、函数是公有的。

7.4 项目案例

  • 注意:项目名通常与项目路径最后一级命名相同,包名通常与包路径最后一级命名相同。但在此案例中为了区分两者的区别,使用不同的命名。
7.4.1 新建项目
cd D:/Go/Project/TestProject
go mod init test

生成 go.mod 文件:

module test

go 1.20

7.4.2 构建项目结构
  • D:/Go/Project/TestProject
    • packageDirectory1
      • packageFile1.go
      • packageFile2.go
    • packageDirectory2
      • packageFile3.go
      • packageFile4.go
    • src
      • hello.go
      • main.go
7.4.3 编写源代码
  • ./packageDirectory1/packageFile1.go
package packageName1

func Add(x, y int) int {
	return x + y
}

func mul(x, y int) int {
	return x * y
}
  • ./packageDirectory1/packageFile2.go
package packageName1  // 同包不同源文件必须使用相同包名

func Mul(x, y int) int {
	return mul(x, y)  // 包内可直接访问受保护对象
}
  • ./packageDirectory2/packageFile3.go
package packageName2  // 不同包的源文件间必须使用不同包名

func Sub(x, y int) int {
	return x - y
}
  • ./packageDirectory2/packageFile4.go
package packageName2

func Div(x, y int) int {
	return x / y
}
  • ./src/hello.go
package main

import "fmt"

func hello() {
	fmt.Println("Hello World!")
}
  • ./src/main.go
package main

import (
	"fmt"  // 标准库包
	p1 "test/packageDirectory1"  // 项目名/包路径
	p2 "test/packageDirectory2"
)

func main() {
	fmt.Println(p1.Add(3, 7))
    fmt.Println(p1.Mul(3, 7))  // 包外只能访问公有对象, p1.mul(3, 7) 是错误的
	fmt.Println(p2.Sub(3, 7))
	fmt.Println(p2.Div(3, 7))
	hello()  // 包内访问不需要导包
}
7.4.4 编译并运行
cd D:/Go/Project/TestProject/src
go build
./src.exe
10
21
-4
0
Hello World!

Reference

凉皮的 Golang 也是在 CSDN 学的,这些是参考的资料。可能没记全…

Go语言超全详解(入门级)_go 语言_大家好,我是好同学的博客-CSDN博客

golang中函数,方法和接口的浅析(函数篇)_CoderTH的博客-CSDN博客

go语言基础 匿名结构体_go 匿名结构体_超级系的博客-CSDN博客

Go语言之接口(接口实现条件,使用,原理,类型断言)_go语言请求接口_Sheena爽的博客-CSDN博客

go 语言常量_go语言中的常量_岳来的博客-CSDN博客

【Go】Go 语言切片(Slice)_go slice_想变厉害的大白菜的博客-CSDN博客

Go 语言中的字符串拼接_go 字符串连接_Roc.Chang的博客-CSDN博客

golang中channel使用_golang channel 的使用_一二三的博客-CSDN博客

11. Go中的结构体_golang结构体类型强转_数哥的博客-CSDN博客

Go学习(十五):异常处理_猿码记的博客-CSDN博客

Go语言学习笔记—golang包管理_golang 包管理_PPPsych的博客-CSDN博客

  • 1
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值