1、Go Module:
go mod init:为当前 Go 项目创建一个新的 module
go mod tidy:自动分析 Go 源码的依赖变更
go mod vendor:可以支持 vendor 机制
2、Go 包的初始化次序:
- 依赖包按“深度优先”的次序进行初始化;
- 每个包内按以“常量 -> 变量 -> init() -> main()”的顺序进行初始化;
- 包内的多个 init 函数按出现次序进行自动调用(顺序执行)。
3、init 函数的用途:
- 重置包级变量值;
- 实现对包级变量的复杂初始化;
- 在 init 函数中实现“注册模式”。
4、变量声明规则:
包级变量:
- 只能使用带有 var 关键字的变量声明形式,不能使用短变量声明形式,但在形式细节上可以有一定灵活度。
var (
a = 13
b = int32(17)
f = float32(3.14)
)
- 可以将延迟初始化的变量声明放在一个 var 声明块 (比如上面的第一个 var 声明块),然后将声明且显式初始化的变量放在另一个 var 块中(比如上面的第二个 var 声明块)
var (
netGo bool
netCgo bool
)
var (
aLongTimeAgo = time.Unix(1, 0)
noDeadline = time.Time{}
noCancel = (chan struct{})(nil)
)
- iota常量计数器
1、iota在const关键字出现时将被重置为0。
2、const中每新增一行常量声明将使iota计数一次(iota可理解为const语句块中的行索引)。
const (
n1 = iota //0
n2 //1
n3 //2
n4 //3
)
const (
n1 = iota //0
n2 //1
_ //丢弃该值,常用在错误处理中
n4 //3
)
const (
n1 = iota //0
n2 = 100 //100
n3 = iota //2
n4 //3
)
局部变量:
- 延迟初始化的局部变量声明采用通用的变量声明形式
var err error
- 声明且显式初始化的局部变量使用短变量声明形式
a := 17
f := 3.14
s := "hello, gopher!"
5、条件判断语句的特殊写法
- 正常写法:
a := 10
if a == 1 {
fmt.Println("a == 1")
} else if a == 2 {
fmt.Println("a == 2")
} else if a == 3 {
fmt.Println("a == 3")
} else {
fmt.Println(a)
}
- 简略写法:
if a := 10; a == 1 {
fmt.Println("a == 1")
} else if a == 2 {
fmt.Println("a == 2")
} else if a == 3 {
fmt.Println("a == 3")
} else {
fmt.Println(a)
}
6、for循环语句的特殊写法
- 正常写法:
for i := 0; i < 10; i++ {
fmt.Println(i)
}
- 简略写法:
//省略初始语句和结束语句
var i = 10
for i > 0 {
fmt.Println(i)
i--
}
- 死循环:
for {
fmt.Println("wuxian")
}
- for range循环:
for index, value := range list {
fmt.Println("index:", index, ",", "value:", value)
}
7、数组、切片、map
- 数组的拷贝是深拷贝,不会影响原数组:
list1 := [3]int{1, 2, 3}
list2 := list1
list2[0] = 100
fmt.Println(list1) //[1 2 3]
- 切片的拷贝是浅拷贝,会影响原切片:
list1 := []int{1, 2, 3}
list2 := list1
list2[0] = 100
fmt.Println(list1) //[100 2 3]
- 深拷贝切片:
a := []int{1, 2, 3, 4}
b := make([]int, 5)
copy(b, a)
b[0] = 100
fmt.Println(a) //[1 2 3 4]
- 切片删除索引index元素:append(a[:index], a[index+1:]...)
// 切片删除下标为2的元素
a := []int{1, 2, 3, 4}
a = append(a[0:2], a[3:]...)
fmt.Println(a)
切片删除多个元素
a = append(a[:i], a[i+1:]...) // 删除中间1个元素
a = append(a[:i], a[i+N:]...) // 删除中间N个元素map基本使用
直接在声明的时候填充元素:
userInfo := map[string]string{
"username": "沙河小王子",
"password": "123456",
}
- map的遍历
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
scoreMap["娜扎"] = 60
for k, v := range scoreMap {
fmt.Println(k, v)
}
}
8、结构体struct
- 1、定义
type person struct {
name, city string
gender string
age int8
}
var p person
p.name = "panda"
p.age = 21
p.gender = "女"
p.city = "BeiJing"
// p2 为结构体指针
var p2 = new(person)
fmt.Println(p2)
// 取结构体的地址进行实例化
p3 := &person{}
fmt.Println(p3)
// 结构体初始化
p4 := person{ //键值对初始化
name: "Panda",
city: "BeiJing",
gender: "女",
age: 21,
}
fmt.Println(p4)
p5 := person{ //列表初始化(按定义时的顺序)
"Panda", "BeiJing", "女", 21,
}
fmt.Printf("%#v\n", p5)
- 2、构造函数
//定义
func newPerson(name, city string, age int8) *person {
return &person{
name: name,
city: city,
age: age,
}
}
//调用构造函数
p9 := newPerson("张三", "沙河", 90)
fmt.Printf("%#v\n", p9) //&main.person{name:"张三", city:"沙河", age:90}
- 3、方法和接收者(相当于结构体的成员函数)
Go语言中的方法(Method)是一种作用于特定类型变量的函数。
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
函数体
}
例如:
//Person 结构体
type Person struct {
name string
age int8
}
//NewPerson 构造函数
func NewPerson(name string, age int8) *Person {
return &Person{
name: name,
age: age,
}
}
//Dream Person做梦的方法
func (p Person) Dream() {
fmt.Printf("%s的梦想是学好Go语言!\n", p.name)
}
func main() {
p1 := NewPerson("小王子", 25)
p1.Dream()
}
- 4、结构体-JSON 序列化、反序列化
type student struct {
ID int
Gender string
Name string
}
type class struct {
Title string
Students []student
}
c := &class{
Title: "101",
Students: make([]student, 0, 20),
}
for i := 0; i < 10; i++ {
stu := student{
Name: "stu" + string(i+48),
Gender: "男",
ID: i,
}
c.Students = append(c.Students, stu)
}
data, err := json.Marshal(c) // 序列化
if err != nil {
fmt.Println("json marshal failed!")
return
}
fmt.Printf("json:%s\n", data)
str := `{"Title":"101","Students":[{"ID":0,"Gender":"男","Name":"stu0"},{"ID":1,"Gender":"男","Name":"stu1"},{"ID":2,"Gender":"男","Name":"stu2"},{"ID":3,"Gender":"男","Name":"stu3"},{"ID":4,"Gender":"男","Name":"stu4"},{"ID":5,"Gender":"男","Name":"stu5"},{"ID":6,"Gender":"男","Name":"stu6"},{"ID":7,"Gender":"男","Name":"stu7"},{"ID":8,"Gender":"男","Name":"stu8"},{"ID":9,"Gender":"男","Name":"stu9"}]}`
c1 := &class{}
err = json.Unmarshal([]byte(str), c1) // 反序列化
if err != nil {
fmt.Println("json unmarshal failed!")
return
}
fmt.Printf("%#v\n", c1)
9、接口interface
- 1、语法
接口的出现是为了支持多态,例如一个函数可能会接收多种结构体作为参数,如果此时参数类型还是传统的结构体,那么就只能支持传入一种结构体类型,这时可以使用接口作为参数类型。
只要一个类型实现了接口中规定的所有方法,那么它就实现了这个接口。
package main
import "fmt"
type dog struct {
name string
}
func (d dog) say() {
fmt.Println(d.name + ":汪汪汪~")
}
type cat struct {
name string
}
func (c cat) say() {
fmt.Println(c.name + ":喵喵喵~")
}
// 接口为了实现多态,只要实现了say方法的结构体都可以被视为sayer类型
type sayer interface {
say()
}
// 这里传入的参数因为既有狗也有猫,所以必须使用接口类型进行多态传入
func hit(someone sayer) {
someone.say()
}
func main() {
dog1 := dog{
"旺财",
}
cat1 := cat{
"加菲",
}
hit(dog1)
hit(cat1)
}
- 2、接口组合
接口与接口之间可以通过互相嵌套形成新的接口类型
// src/io/io.go
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
// ReadWriter 是组合Reader接口和Writer接口形成的新接口类型
type ReadWriter interface {
Reader
Writer
}
// ReadCloser 是组合Reader接口和Closer接口形成的新接口类型
type ReadCloser interface {
Reader
Closer
}
// WriteCloser 是组合Writer接口和Closer接口形成的新接口类型
type WriteCloser interface {
Writer
Closer
}
只需要实现新接口类型中规定的所有方法就算实现了该接口类型
- 3、空接口
空接口是指没有定义任何方法的接口类型。因此任何类型都可以视为实现了空接口。也正是因为空接口类型的这个特性,空接口类型的变量可以存储任意类型的值。
使用空接口实现可以接收任意类型的函数参数:
// 空接口作为函数参数
func show(a interface{}) {
fmt.Printf("type:%T value:%v\n", a, a)
}
使用空接口实现可以保存任意值的字典:
// 空接口作为map值
var studentInfo = make(map[string]interface{})
studentInfo["name"] = "沙河娜扎"
studentInfo["age"] = 18
studentInfo["married"] = false
fmt.Println(studentInfo)
10、reflect 反射
reflect包提供了reflect.TypeOf和reflect.ValueOf两个函数来获取任意对象的Value和Type。
11、并发 goroutine
区别于操作系统线程由系统内核进行调度, goroutine 是由Go运行时(runtime)负责调度。例如Go运行时会智能地将 m个goroutine 合理地分配给n个操作系统线程,实现类似m:n的调度机制,不再需要Go开发者自行在代码层面维护一个线程池。
Goroutine 是 Go 程序中最基本的并发执行单元。每一个 Go 程序都至少包含一个 goroutine——main goroutine,当 Go 程序启动时它会自动创建。
启动 goroutine 的方式非常简单,只需要在调用函数(普通函数和匿名函数)前加上一个go关键字:
go func(){
// ...
}()
为了防止main函数主线程结束时创建的goroutine 还没有运行完,就需要使用sync.WaitGroup全局等待组变量,让main函数等待其他线程运行结束
package main
import (
"fmt"
"sync"
)
// 声明全局等待组变量
var wg sync.WaitGroup
func hello() {
fmt.Println("hello")
wg.Done() // 告知当前goroutine完成
}
func main() {
wg.Add(1) // 登记1个goroutine
go hello()
fmt.Println("你好")
wg.Wait() // 阻塞等待登记的goroutine完成
}
启动多个goroutine:
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func hello(i int) {
defer wg.Done() // goroutine结束就登记-1
fmt.Println("hello", i)
}
func main() {
for i := 0; i < 10; i++ {
wg.Add(1) // 启动一个goroutine就登记+1
go hello(i)
}
wg.Wait() // 等待所有登记的goroutine都结束
}
匿名函数版本:
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1) // 启动一个goroutine就登记+1
go func(i int) {
fmt.Println("hello", i)
wg.Done() // goroutine结束就登记-1
}(i) //必须要显式的传入i,不然闭包访问到的 i 可能跟循环次数不对应
}
wg.Wait() // 等待所有登记的goroutine都结束
}
12、channel通信
-
1、初始化
通道是一种消息队列,总是遵循先进先出的原则;
channel是一种类型,一种引用类型(空值为nil),声明格式如下:
var x1 chan int //声明一个传递整型的通道
var x2 chan bool //声明一个传递布尔型的通道
var x3 chan []int //声明一个传递int切片的通道
声明的通道需要使用make函数初始化之后才能使用,格式如下:
make(chan 元素类型, [缓冲大小])
x1 := make(chan int)
x2 := make(chan bool)
x3 := make(chan []int)
ch5 := make(chan bool, 1) // 声明一个缓冲区大小为1的通道
-
2、channel操作
通道共有发送(send)、接收(receive)和关闭(close)三种操作,发送和接收操作都使用<-符号。
先使用以下语句定义一个通道:
ch := make(chan int)
①发送
将一个值发送到通道中。
ch <- 10 // 把10发送到ch中
②接收
从一个通道中接收值。
x := <- ch // 从ch中接收值并赋值给变量x
<-ch // 从ch中接收值,忽略结果
③关闭
我们通过调用内置的close函数来关闭通道。
close(ch)
-
3、无缓冲的通道
无缓冲的通道又称为阻塞的通道。
ch := make(chan int)没有分配缓冲区大小,信息必须要手把手的同步传输 -
4、有缓冲的通道
ch := make(chan int, 1) - 5、对通道的操作
len(ch1) //取通道中的元素数量
cap(ch1) //获取通道的容量
- 6、从通道中取值的两种方式
for res := range ch2 {
fmt.Println(res)
}
for {
val, ok := <-ch1
if !ok {
break
}
ch2 <- val * val
}
-
7、单向通道
一般用于函数传参时规定通道的方向
ch1 chan<- int 只能接收值
ch2 <-chan int 只能发送值
image.png
例如:
package main
import (
"fmt"
)
func f1(ch1 chan<- int) {
for i := 0; i < 100; i++ {
ch1 <- i
}
close(ch1)
}
func f2(ch1 <-chan int, ch2 chan<- int) {
for {
val, ok := <-ch1
if !ok {
break
}
ch2 <- val * val
}
close(ch2)
}
func main() {
ch1 := make(chan int, 100)
ch2 := make(chan int, 100)
go f1(ch1)
go f2(ch1, ch2)
for ret := range ch2 {
fmt.Println(ret)
}
}
-
8、select多路复用:同时从多个通道接收数据
类似于之前学到的 switch 语句,但是如果有多个case符合条件则会随机选取其中一条执行而不是顺序执行
select {
case <-ch1:
//...
case data := <-ch2:
//...
case ch3 <- 10:
//...
default:
//默认操作
}
Select 语句具有以下特点。
- 可处理一个或多个 channel 的发送/接收操作。
- 如果多个 case 同时满足,select 会随机选择一个执行。
- 对于没有 case 的 select 会一直阻塞,可用于阻塞 main 函数,防止退出。
13、并发安全和锁
-
1、互斥锁
互斥锁是完全互斥的
var lock sync.Mutex //定义一个互斥锁类型
lock.Lock() //加锁
lock.Unlock() //释放锁
-
2、读写互斥锁:适用于读的操作数量远远大于写操作
读取时可以不加锁,写的时候才有必要加锁
加了读锁之后都可以读,不可以写;
加了写锁之后其他的线程都不能读写
var rwlock sync.RWMutex
rwlock .Lock() //加写锁
rwlock .Unlock() //释放写锁
rwlock .RLock() //加读锁
rwlock .RUnlock() //释放读锁
-
3、sync.WaitGroup
进程同步器
var wg sync.WaitGroup
wg.Add(x) //计数器+x
wg.Done() //计数器-1
wg.Wait() //阻塞直到计数器变为0
-
4、sync.Once:只执行一次
确保某些操作即使在高并发的场景下也只会被执行一次,例如只加载一次配置文件等
func test(){
...
}
var once sync.Once
once.Do(test)
-
5、sync.Map:并发安全版map
go 语言中内置的 map 不是并发安全的,当过多的线程并发访问map时会报错:
fatal error: concurrent map writes
// 并发安全的map
var m = sync.Map{} //不需要指定key、value类型(都是空接口类型)
m.Store(key, value) // 存储key-value
m.Load(key) // 根据key取值
-
6、原子操作
通常直接使用原子操作比使用锁操作效率更高,但是只针对整数数据类型(int32、uint32、int64、uint64)
方法:
LoadInt32、StoreInt32、AddInt32、SwapInt32、CompareAndSwapInt32
import "sync/atomic"
type AtomicCounter struct {
counter int64
}
func (a *AtomicCounter) Inc() {
atomic.AddInt64(&a.counter, 1)
}
func (a *AtomicCounter) Load() int64 {
return atomic.LoadInt64(&a.counter)
}
14、网络编程
-
1、TCP:
服务端:
package main
import (
"bufio"
"fmt"
"net"
)
func process1(conn net.Conn) {
defer conn.Close() //处理完毕之后要关闭该连接
// 针对当前连接做数据发送和接收操作
for {
reader := bufio.NewReader(conn)
var buf [128]byte
n, err := reader.Read(buf[:])
if err != nil {
fmt.Printf("read from conn failed, err:%v\n", err)
break
}
recv := string(buf[:n])
fmt.Println("接收到的数据:", recv)
conn.Write([]byte("ok")) //把收到的数据返回到客户端
}
}
func main() {
// 1、开启服务
listen, err := net.Listen("tcp", "127.0.0.1:20000")
if err != nil {
fmt.Printf("Listen failed, err:%v\n", err)
return
}
for {
// 2、等待客户端建立连接
conn, err := listen.Accept()
if err != nil {
fmt.Printf("Accept failed, err:%v\n", err)
continue
}
// 3、启动一个单独的goroutine去处理连接
go process1(conn)
}
}
客户端:
package main
import (
"bufio"
"fmt"
"net"
"os"
"strings"
)
func main() {
// 1、与服务端建立连接
conn, err := net.Dial("tcp", "127.0.0.1:20000")
if err != nil {
fmt.Printf("Dial failed, err:%v\n", err)
return
}
// 2、利用该连接进行数据发送和接收
input := bufio.NewReader(os.Stdin)
for {
s, _ := input.ReadString('\n')
s = strings.TrimSpace(s)
if strings.ToUpper(s) == "Q" {
return
}
// 给服务端发消息
_, err := conn.Write([]byte(s))
if err != nil {
fmt.Printf("Send failed, err:%v\n", err)
return
}
// 从服务端接收回复的消息
var buf [1024]byte
n, err := conn.Read(buf[:])
if err != nil {
fmt.Printf("Read failed, err:%v\n", err)
return
}
fmt.Println("收到服务端回复:", string(buf[:n]))
}
}
-
2、TCP黏包:
tcp流模式传递数据,当数据量较多时,多条数据混在一起接收时不知道是哪一条
①由Nagle算法造成的发送端的粘包:Nagle算法是一种改善网络传输效率的算法。简单来说就是当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去。
②接收端接收不及时造成的接收端粘包:TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据。当应用层由于某些原因不能及时的把TCP的数据取出来,就会造成TCP缓冲区中存放了几段数据。
解决办法:对数据包进行封包、拆包操作
// socket_stick/proto/proto.go
package proto
import (
"bufio"
"bytes"
"encoding/binary"
)
// Encode 将消息编码
func Encode(message string) ([]byte, error) {
// 读取消息的长度,转换成int32类型(占4个字节)
var length = int32(len(message))
var pkg = new(bytes.Buffer)
// 写入消息头
err := binary.Write(pkg, binary.LittleEndian, length)
if err != nil {
return nil, err
}
// 写入消息实体
err = binary.Write(pkg, binary.LittleEndian, []byte(message))
if err != nil {
return nil, err
}
return pkg.Bytes(), nil
}
// Decode 解码消息
func Decode(reader *bufio.Reader) (string, error) {
// 读取消息的长度
lengthByte, _ := reader.Peek(4) // 读取前4个字节的数据
lengthBuff := bytes.NewBuffer(lengthByte)
var length int32
err := binary.Read(lengthBuff, binary.LittleEndian, &length)
if err != nil {
return "", err
}
// Buffered返回缓冲中现有的可读取的字节数。
if int32(reader.Buffered()) < length+4 {
return "", err
}
// 读取真正的消息数据
pack := make([]byte, int(4+length))
_, err = reader.Read(pack)
if err != nil {
return "", err
}
return string(pack[4:]), nil
}
-
3、UDP:
UDP服务端:
// UDP/server/main.go
// UDP server端
func main() {
listen, err := net.ListenUDP("udp", &net.UDPAddr{
IP: net.IPv4(0, 0, 0, 0),
Port: 30000,
})
if err != nil {
fmt.Println("listen failed, err:", err)
return
}
defer listen.Close()
for {
var data [1024]byte
n, addr, err := listen.ReadFromUDP(data[:]) // 接收数据
if err != nil {
fmt.Println("read udp failed, err:", err)
continue
}
fmt.Printf("data:%v addr:%v count:%v\n", string(data[:n]), addr, n)
_, err = listen.WriteToUDP(data[:n], addr) // 发送数据
if err != nil {
fmt.Println("write to udp failed, err:", err)
continue
}
}
}
UDP客户端
// UDP 客户端
func main() {
socket, err := net.DialUDP("udp", nil, &net.UDPAddr{
IP: net.IPv4(0, 0, 0, 0),
Port: 30000,
})
if err != nil {
fmt.Println("连接服务端失败,err:", err)
return
}
defer socket.Close()
sendData := []byte("Hello server")
_, err = socket.Write(sendData) // 发送数据
if err != nil {
fmt.Println("发送数据失败,err:", err)
return
}
data := make([]byte, 4096)
n, remoteAddr, err := socket.ReadFromUDP(data) // 接收数据
if err != nil {
fmt.Println("接收数据失败,err:", err)
return
}
fmt.Printf("recv:%v addr:%v count:%v\n", string(data[:n]), remoteAddr, n)
}
15、单元测试
go test命令是一个按照一定约定和组织的测试代码的驱动程序。在包目录内,所有以_test.go为后缀名的源代码文件都是go test测试的一部分,不会被go build编译到最终的可执行文件中。
在_test.go文件中有三种类型的函数,单元测试函数、基准测试函数和示例函数。
go test命令会遍历所有的_test.go文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。
-
1、测试函数:
每个测试函数必须导入testing包,测试函数的基本格式(签名)如下:
func TestName(t *testing.T){
// ...
}
其中参数t用于报告测试失败和附加的日志信息。 testing.T的拥有的方法如下:
func (c *T) Error(args ...interface{})
func (c *T) Errorf(format string, args ...interface{})
func (c *T) Fail()
func (c *T) FailNow()
func (c *T) Failed() bool
func (c *T) Fatal(args ...interface{})
func (c *T) Fatalf(format string, args ...interface{})
func (c *T) Log(args ...interface{})
func (c *T) Logf(format string, args ...interface{})
func (c *T) Name() string
func (t *T) Parallel()
func (t *T) Run(name string, f func(t *T)) bool
func (c *T) Skip(args ...interface{})
func (c *T) SkipNow()
func (c *T) Skipf(format string, args ...interface{})
func (c *T) Skipped() bool
16、泛型
如果多个函数仅仅是参数类型不同,可以考虑使用泛型
观看以下两个函数:
func reverseInt(s []int) []int {
l := len(s)
r := make([]int, l)
for i, e := range s {
r[l-i-1] = e
}
return r
}
func reverseFloat64(s []float64) []float64 {
l := len(s)
r := make([]float64, l)
for i, e := range s {
r[l-i-1] = e
}
return r
}
仅仅是处理的参数类型不同,过程完全一样,那么可以使用泛型语法:
func min[T int | float64](a, b T) T {
if a <= b {
return a
}
return b
}
这次定义的min函数就同时支持int和float64两种类型:
m1 := min[int](1, 2) // 1
m2 := min[float64](-0.1, -0.2) // -0.2