参考链接:哔哩哔哩视频,在线文章。

1 Golang安装和环境变量
D:\Program Files\goD:\Program Files\go\GoWorks%GOROOT\bin
go version


  IDE如果是免费的选择VSCode,收费的选择Goland。也可以Vim+go插件。

2 Golang语言特性

2.1 优势

1、极简单的部署方式
  可直接编译成机器码,不依赖其他库,可以直接运行部署。
2、静态类型语言
  编译的时候可以检查出隐藏的大多数问题(一般静态语言的优势)。
3、语言层面的并发
  go语言是基因支持的并发,很多语言其实是“美容”的并发,一层包一层实现高并发。go的语法使得能够充分地利用多核,切换成本低,尽量提高CPU并发效率。
4、强大的标准库支撑
  有runtime的系统调度机制,高效的垃圾回收,丰富的标准库。
5、简单易学
  仅有25个关键字,语法从c语言过渡,内嵌c语法支持,具有面向对象特征(继承、封装、多态),跨平台。
6、大厂领军
  国内外大公司有在使用go语言,例如Google,Facebook,腾讯,百度,字节跳动,京东,小米,阿里巴巴,哔哩哔哩等等。

例子:斐波那契亚数列算法下不同语言效率对比
  就这个例子而言,可以看到不管是编译还是运行Go都是比较快的。

2.2 适合做什么

1、云计算基础设施领域
2、基础后端软件
3、微服务
4、互联网基础设施

2.3 明星作品

Docker和Kubernetes

2.4 缺点

1、包管理,大部分第三方库都在Github上,代码稳定性有风险
2、无泛化类型,目前在Go1.18已经加上了泛型
3、全部Exception都采用Error处理
Java是极端地把全部Error都用Exception处理,python是取了中间两种都可以,而Golang是极端地把全部Exception都用Error处理,没有谁对谁错之分
4、对C语言的降级处理并不是无缝的,没有降级到汇编那么完美,但目前只有go能够这样做
c语言是唯一能够和操作系统交流的语言,

3 Golang语法新奇

3.1 从main函数初见go的语法

package mainimport "fmt"func main() {fmt.Println("Hello Go!")
}
go run hello.gogo build hello.go.\hello
import "fmt"
import "time"

或者

import ("fmt""time"
)

3、方法的左花括号必须要和函数名同一行

3.2 变量

3.2.1 单变量声明

1、声明一个变量,不初始化,默认值是0

var a int

2、声明一个变量,并初始化

var b int = 100

3、初始化时省去类型声明,通过值的类型自动匹配(不推荐)

var c = 100
fmt.Printf("%T", c)

4、(常用)省去var关键字,直接匹配

d := 100

区别:声明全局变量(方法外)可以用前三种,不能用第四种

3.2.2 多变量声明

1、数据类型相同

var x, y int  = 100,  200

2、数据类型不同

var k, l = 100, "hello"

3、多行写法,类型可不声明

var (
i (int) = 100
j (bool) = true
)

3.3 常量与iota

常量命名

const length int = 10

const定义枚举

const (BEIJING  = 0SHANGHAI = 1SHENZHEN = 2
)

或者使用iota,只要第一个赋值iota,它默认是0,每行依次加1

	const (BEIJING = iotaSHANGHAISHENZHEN)
10*iota
const (a, b = 1 + iota, 2 + iotac, de, fg, h = 2 * iota, 3 * iotai, j
)
fmt.Println(a, b, c, d, e, f, g, h, i, j)

3.4 函数

函数声明,括号内是形参,右边是返回值

func f1(a string, b int) int {fmt.Println(a)fmt.Println(b)c := 100return c
}

多返回值,用括号括起来
1、匿名返回值

func f2(a string, b int) (int, int) {fmt.Println(a)fmt.Println(b)return 111, 222
}

2、给返回值命名

func f3(a string, b int) (r1 int, r2 int) {fmt.Println(a)fmt.Println(b)r1 = 111r2 = 222return 
}

3、如果返回值类型一样,可以只保留一个返回值类型

func f4(a string, b int) (r1, r2 int) {fmt.Println(a)fmt.Println(b)r1 = 111r2 = 222return
}

3.5 init函数与导包

  • init函数
      从图中可以看出,程序先从main包进入,再递归导包,然后执行包中的常量,变量,init函数,并依次返回,最后执行main方法。
    对外开放的方法首字母大写。
  • import导包
    go语言的包导入如果不调用会报错。

1、如果不想使用某一个包的API,但是要使用这个包的init函数,可以匿名导包

import _ "lib1"

2、可以给包起别名,并且可以调用它的方法

import mylib2 "lib2"

3、将包的全部方法导入本包,调用方法时可以不带包名(少使用,防止函数名冲突)

import . lib2

3.6 指针

默认情况下方法是值传递

package mainimport "fmt"func main() {a := 1changeValue(a)fmt.Println(a)//输出为20,说明a的值未改变
}func changeValue(p int) {p = 10
}

可以指针传递,*int表示是指向int类型的指针,p处存储的就是a的地址值,*p表示找到存的地址值对应的地址,然后改变值为10,&表示传入地址

package mainimport "fmt"func main() {a := 1changeValue(&a)fmt.Println(a)//输出10,说明a的值被改变
}func changeValue(p *int) {*p = 10
}


经典例子:交换数据
如果这样交换,并不能交换成功,因为是值传递

package mainimport "fmt"func main() {a := 10b := 20swap(a, b)fmt.Println(a, b)
}func swap(a int, b int) {tmp := aa = bb = tmp
}

需要使用指针

package mainimport "fmt"func main() {a := 10b := 20swap(&a, &b)fmt.Println(a, b)
}func swap(a *int, b *int) {tmp := *a*a = *b*b = tmp
}

二级指针:指针的指针

3.7 defer关键字

有点像c++的析构函数或者Java中的finally关键字
defer语句放在return之前,在当前函数结束,return返回后执行,defer可以有多个,但是按照栈的顺序,先写的后执行。

package mainimport "fmt"func main() {returnAndDefer()
}func returnFun() int {fmt.Println("return...")return 0
}func returnAndDefer() int {defer fmt.Println("defer...")return returnFun()
}

3.8 数组和动态数组slice

声明数组

var myArray1 [10]int
myArray2 := [10]int{}

数组如果作为形参,要声明长度,而且是值拷贝,方法内不改变数组的值

func printArray(myArray [10]int) {for i, value := range myArray {fmt.Println(i,value)}
}

动态数组声明,相比之下不指定长度

var myArray1 []int
myArray1 := []int{1,2,3,4}

方法如下,动态数组是指针传递,因此方法内会改变原有的值

func printArray(myArray []int) {for i, value := range myArray {fmt.Println(i, value)}
}
for _, value := range myArray

3.8.1 slice声明方式

四种声明slice方式如下:

slice1 := []int{1, 2, 3}
var slice2 []int
var slice3 []int = make([]int,3)//通过make分配空间
slice4 := make([]int,3)if slice2 == nil {fmt.Println("空切片")
} else {fmt.Println("有空间")
}

输出空切片,要注意else要和上一个右花括号放在同一行。

3.8.2 slice使用方式

1、切片容量的追加
长度小于等于容量

append方法追加一个元素并赋值,如果cap不够会追加cap容量

var slice = make([]int, 3, 5)
fmt.Println(len(slice), cap(slice), slice)
slice = append(slice, 1)
fmt.Println(len(slice), cap(slice), slice)
slice = append(slice, 2)
fmt.Println(len(slice), cap(slice), slice)
slice = append(slice, 3)
fmt.Println(len(slice), cap(slice), slice)


扩容机制:根据cap增加二倍,即每次翻一倍
2、切片的截取
这里和python有些类似,但是这里是指针传递,改变slice2会改变slice。

slice1 := slice[0:2]//取头不取尾
slice2 := slice[:5]
slice3 := slice[3:]
slice4 := slice[:]

copy函数可以深拷贝,前一个参数是destination,后一个参数是source

slice := []int{0, 1, 2, 3, 4, 5, 6}
slice5 := make([]int,7)
copy(slice5,slice)

3.9 map

三种声明方式:

var myMap1 map[string]int
myMap1 = make(map[string]string, 10)
myMap1["one"] = "java"
myMap1["two"] = "c"
myMap1["three"] = "python"
myMap2 := make(map[string]string)
myMap3 := map[string]string{"one":   "c++","two":   "java","three": "python",
}

使用方式:

//遍历
for key, value := range cityMap {fmt.Println(key)fmt.Println(value)
}
//删除
delete(cityMap, "Japan")
//修改
cityMap["USA"] = "Washington"

3.10 面向对象特征

  type声明一种新的数据类型,可以定义结构体,即把多种基本数据类型组合形成复杂的数据类型。%v可以格式化各种类型的输出。

type Book struct {title stringautu  string
}
var book1 Book
book1.title = "Golang"
book1.autu = "zhangsan"
fmt.Printf("%v\n", book1)

如果需要函数中改变值,需要传指针

changeBook(&book1)func changeBook(book *Book) {book.auth = "666"
}

  go语言中的类其实就是结构体绑定方法,

type Hero struct {Name stringAd   intLevel string
}
func (this Hero) GetName() {fmt.Println("Name = " + this.Name)
}
func (this Hero) SetName(newName string) {this.Name = newName
}
func (this Hero) Show() {fmt.Println("Name = ", this.Name)fmt.Println("Ad = ", this.Ad)fmt.Println("Level = ", this.Level)
}
//创建并初始化对象
hero := Hero{Name: "zhangsan", Ad: 100, Level: 1}

要注意,this Hero是调用这个方法的对象的拷贝,因此SetName不会修改原来的属性值,要实现修改还是需要使用传指针

func (this *Hero) Show() {fmt.Println("Name = ", this.Name)fmt.Println("Ad = ", this.Ad)fmt.Println("Level = ", this.Level)
}
func (this *Hero) GetName() string {return this.Name
}
func (this *Hero) SetName(newName string) {this.Name = newName
}

3.10.1 封装

  前面已经提到了,方法名首字母如果大写,可以被其他包访问。结构体名字,属性首字母如果大写可以被其他包访问,如果小写只有包内部可以访问。这就是go语言的封装。

3.10.2 继承

  在SuperMan中Human就表示继承了Human这个类。可以重写方法,添加新方法。

type Human struct {name stringsex  string
}
type SuperMan struct {Humanlevel int
}

创建对象:

human := Human{"zhangsan", "female"}
superMan := SuperMan{Human{"lisi", "female"}, 100}
//或者
var s SuperMan
s.name = "zhangsan"
s.sex = "male"
s.level = 3

3.10.3 多态

interface本质是一个指针,在实现类只要重写三个方法就实现了接口。如果重写不完全,接口就不能指向这个实现类。

type AnimalIF interface {Sleep()GetColor() stringGetType() string
}type Cat struct {color string
}
func (this *Cat) Sleep() {fmt.Println("Cat is sleeping")
}
func (this *Cat) Getolor() string {return this.color
}
func (this *Cat) GetType() string {return "Cat"
}

创建接口指向实现类,需要把对象的地址传过去。

var animal AnimalIF
animal = &Cat{"green"}
animal.Sleep()
animal = &Dog{"blue"}
animal.Sleep()

多态的方法

func showAnimal(animal AnimalIF) {animal.Sleep()fmt.Println("color = ", animal.GetColor())fmt.Println("type = ", animal.GetType())
}cat := Cat{"black"}
dog := Dog{"yellow"}
showAnimal(&cat)
showAnimal(&dog)

3.10.4 万能类型

interface{}interface{}
func myFunc(arg interface{}) {fmt.Println("myFunc is called...")fmt.Println(arg)
}book := Book{"golang", "zhangsan"}
myFunc(book)
myFunc(100)
myFunc("abc")
myFunc(3.14)
  • go语言提供了“类型断言”机制,判断是否是某种类型
func myFunc(arg interface{}) {value, ok := arg.(string)fmt.Println(value, ok)
}
  • 变量的内置pair
    一个变量包含类型type和值value,类型要么是静态类型,要么是具体类型。type和value组成pair

    示例,赋值的时候pair不会改变
var a string
a = "abc"
var allType interface{}
allType = a
str, ok := allType.(string)
fmt.Println(str, ok)

或者

type Reader interface {ReadBook()
}
type Writer interface {WriteBook()
}
type Book struct {
}
func (this Book) ReadBook() {fmt.Println("Read a Book")
}
func (this Book) WriteBook() {fmt.Println("Write a Book")
}func main() {b := Book{}var r Readerr = br.ReadBook()var w Writerw = r.(Writer)w.WriteBook()
}

3.11 反射

  在reflect包,两个重要API:TypeOf和ValueOf

func main() {var num float64 = 1.2345refelctNum(num)
}
func DoFileAndMethod(input interface{}) {//获取类型inputType := reflect.TypeOf(input)fmt.Println("input type is:", inputType.Name())//获取值inputValue := reflect.ValueOf(input)fmt.Println("input value is:", inputValue)//获取字段Fieldfor i := 0; i < inputType.NumField(); i++ {field := inputType.Field(i)value := inputValue.Field(i).Interface()fmt.Println(field.Name, field.Type, value)}//获取方法并调用for i := 0; i < inputType.NumMethod(); i++ {m := inputType.Method(i)fmt.Println(m.Name, m.Type)}
}

3.12 结构体标签Tag

注意要用反斜杠,里面是键值对,中间用空格隔开,主要的作用是根据这个标签,判断这个属性在不同包中怎么用。

type resume struct {Name string `info:"name" doc:"我的名字"`Sex  string `info:"sex"`
}
func findTag(str interface{}) {t := reflect.TypeOf(str).Elem()for i := 0; i < t.NumField(); i++ {tagString := t.Field(i).Tag.Get("info")fmt.Println("info:", tagString)}
}
func main() {var re resumefindTag(&re)
}

3.12.1 结构体标签的应用

  • 在json中的应用,编解码
    在转换为json时,会先检查是否在标签中有json键值对,有则将值取出来组成json字符串。
type Movie struct {Title  string   `json:"title"`Year   int      `json:"year"`Price  int      `json:"price"`Actors []string `json:"actors"`
}
func main() {movie := Movie{"喜剧之王", 2000, 10, []string{"周星驰", "张柏芝"}}//编码的过程 结构体-->jsonjsonStr, err := json.Marshal(movie)if err != nil {fmt.Println("json marshal error", err)return}fmt.Printf("jsonStr=%s\n", jsonStr)//解码的过程 json-->结构体myMovie := Movie{}err = json.Unmarshal(jsonStr, &myMovie)if err != nil {fmt.Println("json unmarshal error", err)return}fmt.Println(myMovie)
}
  • orm映射关系
4 Golang高阶

4.1 协程

4.1.1 co-routine

  进程或线程数量越多,切换成本越高,CPU资源越浪费,此外还有高内存占用的弊端。一个线程分为用户态和内核态,划分之后,内核线程称为线程thread,用户线程称为协程co-routine。通过一个内核线程和协程调度器,绑定多个协程。内核线程和协程之间的关系不适合一对多或者一对一,适合N对M。从图中可以看出主要需要优化的就是协程调度器。

4.1.2 Golang的协程——Goroutine

  Golang的协程内存小,几KB,灵活调度。G表示goroutine协程,P表示处理器,M表示内核线程。
调度器的设计策略:

  • 服用线程
  • 利用并行
  • 抢占
  • 全局G队列

go语言创建协程非常方便,使用go关键字加上函数即可。
第一种调用,go + 方法

//子Goroutine
func newTask() {i := 0for {i++fmt.Printf("new Goroutine : i = %d\n", i)time.Sleep(1 * time.Second)}
}//主Goroutine
func main() {go newTask()
}

第二种调用,匿名的go协程,匿名方法也可以有形参和返回值,但是返回值不能直接用参数去接,如果要接,其实就是要解决协程之间的通信,这里就要用到channel了

func main() {//用go创建承载一个形参为空,返回值为空的函数go func() {defer fmt.Println("A.defer")//匿名函数func() {defer fmt.Println("B.defer")runtime.Goexit()//退出当前协程,而不仅是匿名函数fmt.Println("B")}()fmt.Println("A")}()for {time.Sleep(1 * time.Second)}
}

4.2 协程的通信——channel

<-
func main() {//定义一个channelc := make(chan int)go func() {defer fmt.Println("goroutine结束")fmt.Println("goroutine正在运行...")c <- 666}()num := <-cfmt.Println("num = ", num)fmt.Println("main goroutine结束...")
}
num := <-c

4.2.1 无缓存的channel

  对于无缓存的channel,传递消息的一方如果提前到达了要传递channel的指令,但此时接收协程还没有执行到接收channel,那么发送方就需要一直等待,直到接收方来接收。

4.2.2 有缓存的channel

  对于有缓存的channel,发送方将数据发送到channel中便继续执行程序,如果管道中有数据,接收方直接取走数据;发送方发现channel中已经存满了数据时才会被阻塞,接收方直到channel中被取空了才会被阻塞。类似生产者消费者模式。

代码实例:

func main() {c := make(chan int, 3)fmt.Println(len(c), cap(c))go func() {defer fmt.Println("子goroutine结束...")for i := 0; i < 3; i++ {c <- ifmt.Println("子goroutine正在运行:len(c)=", len(c), "cap(c)=", cap(c))}}()for i := 0; i < 3; i++ {num := <-cfmt.Println("num=", num)}fmt.Println("main goroutine结束")
}

4.2.3 关闭channel

  • channel不像文件一样需要经常去关闭,除非确定不会再向channel中发送数据,或者想显式地结束range循环,才会去关闭channel。
  • 关闭channel后无法向它发送数据
  • 关闭channel后可以继续接收数据
  • 对于nil channel,无论收发都会被阻塞
func main() {c := make(chan int)go func() {for i := 0; i < 5; i++ {c <- i}close(c)}()for {if data, ok := <-c; ok {fmt.Println(data)} else {break}}fmt.Println("Main Finished..")
}
data, ok := <-cok

4.2.4 channel和range

类似的,可以用range来获取channel中的数据,把上面的例子修改,代码如下:

func main() {c := make(chan int)go func() {for i := 0; i < 5; i++ {c <- i}close(c)}()for data := range c {fmt.Println(data)}fmt.Println("Main Finished..")

4.2.5 channel和select

  单个goroutine下只能监控一个channel的状态,select能够实现监控多个channel的状态。

func fibonacii(c, quit chan int) {x, y := 1, 1for {select {case c <- x:x = yy = x + ycase <-quit:fmt.Println("quit")return}}
}
func main() {c := make(chan int)quit := make(chan int)go func() {for i := 0; i < 10; i++ {fmt.Println(<-c)}quit <- 0}()fibonacii(c, quit)
}

注意:方法传channel传的是指针。

5 Go modules 模块管理

  Go modules是Go语言的依赖解决方案,正式于go1.14推荐在生产上使用,目前只要安装了go就安装了go modules。

5.1 淘汰的Go path

go get -u ...

5.2 Go modules模式

5.2.1 go mod命令

命令功能
go mod init生成go mod文件
go mod download下载go mod文件中指明的所有依赖
go mod vendor导出项目所有依赖到vendor目录

5.2.2 go mod环境变量

go env
go env -w GO111MODULE=onhttps://proxy.golang.org,directhttps://mirrors.aliyun.com/goproxy/https://goproxy.cn,directgo env -w GOPROXY=https://goproxy.cn,directsum.golang.orggo env -w GOPRIVATE="..."

5.3 使用Go modules初始化项目

1、保证GO111MODULE是on,具体解释参考上面

go env -w GO111MODULE=on

2、初始化项目

  • 任意文件夹创建一个项目
 mkdir modules_testcd modules_test
  • 初始化go modules模块,创建go.mod文件,注意要跟上当前模块名称
go mod init github.com/kevin-zkp/modules_test
dir //windows下查看当前目录文件

打开如下:

  • 引用代码
import ("fmt""github.com/aceld/zinx/ziface""github.com/aceld/zinx/znet"
)//ping test 自定义路由
type PingRouter struct {znet.BaseRouter
}//Ping Handle
func (this *PingRouter) Handle(request ziface.IRequest) {//先读取客户端的数据fmt.Println("recv from client : msgId=", request.GetMsgID(), ", data=", string(request.GetData()))//再回写ping...ping...pingerr := request.GetConnection().SendBuffMsg(0, []byte("ping...ping...ping"))if err != nil {fmt.Println(err)}
}func main() {//1 创建一个server句柄s := znet.NewServer()//2 配置路由s.AddRouter(0, &PingRouter{})//3 开启服务s.Serve()
}
  • 下载包
//手动download
go get github.com/aceld/zinx/znet
go get github.com/aceld/zinx/ziface
//可以指定版本号,如下
go get github.com/aceld/zinx@v1.0.0
//自动download
执行go run会自动下载

go.mod文件会多一行如下,如果指定版本,在go.mod中指定

再打开go.sum文件

h1加哈希

5.4 修改项目模块的版本依赖关系

使用命令:

go mod edit -replace=版本号1=版本号2
//例如
go mod edit -replace=zinx@v1.0.1=zinx@v1.0.0
go: -replace=zinx@v1: need old[@v]=new[@w] (missing =)go mod edit -replace='zinx@v1.0.1'='zinx@v1.0.0'
6 Golang案例——即时通信系统

  项目源码:
  目的是覆盖大部分go语法特性,特别是网络,系统基础结构如下,使用了读写分离模型,使用九个版本迭代:

6.1 版本一:构建基础Server

nc 127.0.0.1 8888telnet 127.0.0.1 8888

6.2 用户上线功能

  OnlineMap记录上线的用户,使用channel进行广播。
如果使用windows自带的telnet会中文乱码,可以下载putty,选择Other,telnet,输入Ip地址和端口号,点击open,可以看到成功上线。

6.3 用户消息广播机制

"\r\n"

6.4 用户业务层封装

  把能够合并的代码封装成函数,sever.go中的用户上线,下线,广播的代码需要提取到user.go中。需要为User添加属性,表示属于哪个Server。

6.5 在线用户查询

who

6.6 修改用户名

rename|张三

6.7 超时强踢功能

  只要执行time.After就会重置,同时它其实是管道,只要监听管道中能否取到数据即可。把isLive写到上面,这样只要isLive触发,其他case会执行条件,但是不会执行括号内的代码,重置定时器。

6.8 私聊功能

to|张三|你好呀,我是...

6.9 客户端实现

.exe
go build -o server.exe server.go main.go user.go
go build -o client.exe client.go
.\client.exe -ip 127.0.0.1 -port 8888
func init() {flag.StringVar(&serverIp, "ip", "127.0.0.1", "设置服务器IP地址(默认是127.0.0.1)")flag.IntVar(&serverPort, "port", 8888, "设置服务器端口号(默认是8888)")
}

3、菜单显示

4、更新用户名


5、公聊模式

6、私聊模式
首先查询当前有哪些用户在线,提示选择一个用户

7 Golang生态拓展介绍

1、Web框架
beego:国内的框架,文档比较全
gin:国外的轻量级框架,性能较高,比较主流
echo:国外的,轻量级
Iris:国外的,更加重量级,性能较高
2、微服务框架
go kit:包含很多工具,比较灵活
Istio:包括熔断,安全审核等,适合繁琐的大型微服务
3、容器编排
Kubernetes:市场占有率高,谷歌出来的
Swarm:相对不那么高
4、服务发现
类似Java中的zookeeper
consul:服务发现,服务注册
5、存储引擎
k/v存储——etcd:类似Redis,且支持分布式,一致性比较好
分布式存储——tidb:类似MySQL
6、静态建站
hugo
7、中间件
消息队列——nsg
TCP长链接框架(轻量级服务器)——zinx
Leaf(游戏服务器)——Leaf
RPC框架——gRPC
redis集群——codis
8、爬虫框架
go query:效率比python高,但是目前爬虫生态还是python更好