生活札记
golang学习笔记 - 入门(一)
Go(又称 Golang)是 Google 的 Robert Griesemer,Rob Pike 及 Ken Thompson 开发的一种静态强类型、编译型语言。Go 语言语法与 C 相近,但功能上有:内存安全,GC(垃圾回收),结构形态及 CSP-style 并发计算。Go 是谷歌支持的开源编程语言,易于学习和入门,内置并发和强大的标准库,不断发展的合作伙伴、社区和工具生态系统。
go mod init 方式配置的vscode需要把项目放在根目录下,一个mod对应一个目录,不然vscode一直提示找不到包:
6小时入门go语言:https://www.bilibili.com/video/BV1vs4y1m7hX
一、Golang开发环境
Go官网下载地址:https://go.dev/dl/
根据自己系统,自行选择安装。如果是window系统,推荐下载可执行文件版,一路 Next,这里以linux为例。
下载Linux版本的go,并解压到:/usr/local
cd /usr/src
wget https://go.dev/dl/go1.18.2.linux-amd64.tar.gz //下载
tar -xvf go1.18.2.linux-amd64.tar.gz -C /usr/local/ //解压
配置环境变量GOROOT(安装目录)、GOPATH(项目目录)、GOPROXY(代理地址),我们在 Linux 系统下一般通过文件 $HOME/.bashrc 配置自定义环境变量,根据不同的发行版也可能是文件 $HOME/.profile,然后使用 gedit 或 vim 来编辑文件内容。
1)、编辑:
vim $HOME/.bashrc
2)、写入内容:
export GOROOT=/usr/local/go
export GOPATH=/copylian/www/go
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin //把/usr/local/go/bin目录、配置 GOROOT 到环境变量里
3)、重新加载文件
source $HOME/.bashrc
4)、查看Go版本以及环境变量,配置GOPROXY
go version //查看版本
go env //查看环境变量
go env -w GOPROXY="https://goproxy.cn,direct" //配置proxy代理
二、第一个程序,hello.go
//程序包名
package main
//导入函数
import (
"fmt"
"time"
)
//main函数主体
func main() {
fmt.Println("Hello World!!!")
time.Sleep(1 * time.Second)
}
//自动执行函数
func init() {
fmt.Println("main")
}
执行:go run hello.go
三、变量,type.go
//程序包名
package main
//导入函数
import (
"fmt"
"time"
)
//全局变量,gB := 300 方式无效
var gA = 250
var gC = "CopyLian"
//main函数主体
func main() {
//初始化值,类型
var a int
fmt.Println(a)
var b int = 100
fmt.Println(b)
var c = 150
fmt.Println(c)
fmt.Printf("Type is = %T\n", c)
//忽略var关键字
d := 200
fmt.Println(d)
//全局变量
fmt.Println(gA)
fmt.Printf("Type is = %T\n", gA)
fmt.Println(gC)
fmt.Printf("Type is = %T\n", gC)
//定义多个变量
var xx, yy int = 100, 200
var vv, zz = 150, "CopyLian"
fmt.Println(xx, yy)
fmt.Println(vv, zz)
var (
aa = 300
bb = "aaa"
)
fmt.Println("aa = ", aa, ", bb = ", bb)
}
四、常量,const.go
//程序包名
package main
//导入函数
import (
"fmt"
"time"
)
//全局常量
const (
cc = 100
dd = 200
)
//const关键字 iota,只能用在const常量里面,第一行的iota是0,每行加1,每行的公式继承上一行
const (
FUJIAN = iota
GUANGZHOU
SHENZHEN
)
const (
aaa, bbb = iota + 1, iota + 2
ccc, ddd
eee, fff = iota * 2, iota * 3
ggg = iota + 10
)
//main函数主体
func main() {
//定义常量
const aa int = 100
fmt.Println(aa)
const bb = "CopyLian"
fmt.Println("bb = ", bb)
fmt.Println("cc = ", cc)
fmt.Println("dd = ", dd)
fmt.Println("FUJIAN = ", FUJIAN)
fmt.Println("GUANGZHOU = ", GUANGZHOU)
fmt.Println("SHENZHEN = ", SHENZHEN)
fmt.Println("aaa = ", aaa)
fmt.Println("bbb = ", bbb)
fmt.Println("ccc = ", ccc)
fmt.Println("ddd = ", ddd)
fmt.Println("eee = ", eee)
fmt.Println("fff = ", fff)
fmt.Println("ggg = ", ggg)
//fmt.Println("hhh = ", hhh)
}
五、函数,function.go
init函数与import
init 函数可在package main中,可在其他package中,可在同一个package中出现多次。
main 函数只能在package main中。
执行顺序
golang里面有两个保留的函数:init函数(能够应用于所有的package)和main函数(只能应用于package main)。这两个函数在定义时不能有任何的参数和返回值。
虽然一个package里面可以写任意多个init函数,但这无论是对于可读性还是以后的可维护性来说,我们都强烈建议用户在一个package中每个文件只写一个init函数。
go程序会自动调用init()和main(),所以你不需要在任何地方调用这两个函数。每个package中的init函数都是可选的,但package main就必须包含一个main函数。
程序的初始化和执行都起始于main包。
如果main包还导入了其它的包,那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到fmt包,但它只会被导入一次,因为没有必要导入多次)。
当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包中的包级常量和变量进行初始化,接着执行init函数(如果有的话),依次类推。
等所有被导入的包都加载完毕了,就会开始对main包中的包级常量和变量进行初始化,然后执行main包中的init函数(如果存在的话),最后执行main函数。下图详细地解释了整个执行过程:
//程序包名
package main
//导入函数
import (
"fmt"
)
//自定义函数1
func foo1(a int, b string) string {
fmt.Println("---foo1---")
fmt.Println("a = ", a)
fmt.Println("b = ", b)
return b
}
//自定义函数2
func foo2(a int, b string) (c, d int) {
fmt.Println("---foo2---")
fmt.Println("a = ", a)
fmt.Println("b = ", b)
return c, d
}
//自定义函数3
func foo3(a int, b string) (c string, d int) {
fmt.Println("---foo3---")
fmt.Println("a = ", a)
fmt.Println("b = ", b)
return c, d
}
//自定义函数4
func foo4(a int, b string) (string, int) {
fmt.Println("---foo4---")
fmt.Println("a = ", a)
fmt.Println("b = ", b)
c := "CopyLian"
d := 200
return c, d
}
//main函数主体
func main() {
foo1 := foo1(100, "CopyLian")
fmt.Println("foo1 = ", foo1)
foo2, foo2_1 := foo2(200, "CopyLian")
fmt.Println("foo2 = ", foo2, ", foo2_1 = ", foo2_1)
foo3, foo3_1 := foo3(300, "CopyLian")
fmt.Println("foo3 = ", foo3, ", foo3_1 = ", foo3_1)
foo4, foo4_1 := foo4(400, "CopyLian")
fmt.Println("foo4 = ", foo4, ", foo4_1 = ", foo4_1)
}
六、import(导入函数)、init(自动执行函数),import_init.go
//程序包名
package main
//导入函数
import (
"fmt"
"golang/lib1"
. "golang/lib1" //导入自定义包,可直接调用函数,但其他包可能存在同样的函数名称
//_ "golang/lib1" //匿名包,存在不用,不会报错
b2 "golang/lib2" //自定义名称
)
//main函数主体
func main() {
lib1.Test()
Test()
Testa()
b2.Test()
}
//自动执行函数,可在同一个package中出现多次
func init() {
fmt.Println("main")
}
七、指针,pointer.go
//程序包名
package main
//导入函数
import (
"fmt"
)
//切换函数
func swap(a int, b int) {
var temp int
temp = a
a = b
b = temp
}
//切换函数
func swapPointer(a *int, b *int) {
var temp int
temp = *a
*a = *b
*b = temp
}
//main函数主体
func main() {
var a = 100
var b = 200
swap(a, b)
fmt.Println("a = ", a, "b = ", b)
//指针引用
swapPointer(&a, &b)
fmt.Println("a = ", a, "b = ", b)
//一级指针
var pa *int
pa = &a
fmt.Println("&a = ", &a)
fmt.Println("pa = ", pa)
//二级指针
var pb **int
pb = &pa
fmt.Println("&pb = ", &pa)
fmt.Println("pb = ", pb)
//三级指针
var pc ***int
pc = &pb
fmt.Println("&pc = ", &pb)
fmt.Println("pc = ", pc)
}
八、析构,后执行,defer.go
//程序包名
package main
//导入函数
import (
"fmt"
)
//defer函数
func deferA() {
fmt.Println("A")
}
func deferB() {
fmt.Println("B")
}
func deferC() {
fmt.Println("C")
}
func deferCall() {
defer deferA()
defer deferB()
defer deferC()
}
//return函数
func returnFun() int {
fmt.Println("return-0")
return 0
}
func deferFun() int {
fmt.Println("defer-1")
return 1
}
func returnDefer() int {
defer deferFun()
return returnFun()
}
//main函数主体
func main() {
//defer关键字定义:类似析构函数在函数执行完毕之后再执行,遵循先入后出的规则,如果同时存在defer、return则return先执行
defer fmt.Println("end")
defer fmt.Println("end2")
var a = 100
var b = 200
fmt.Println("a = ", a, "b = ", b)
//defer函数
defer deferCall()
//如果同时存在defer、return则return先执行
returnDefer()
}
九、数组、切片,array_slice.go
切⽚的⻓度和容量不同,⻓度表示左指针⾄右指针之间的距离,容量表示左指针⾄底层数组末尾的距离。切⽚的扩容机制,append的时候,如果⻓度增加后超过容量,则将容量增加2倍
//程序包名
package main
//导入函数
import (
"fmt"
)
//数组函数(固定数组:值拷贝)
func printArr(arr [5]int) {
for i := 0; i < len(arr); i++ {
fmt.Println(arr[i])
}
arr[0] = 1
}
//数组函数(动态数组:值引用)
func printArr1(arr []int) {
for index, value := range arr {
fmt.Println("键:", index, "值:", value)
}
fmt.Print("\n")
//动态数组值引用
arr[0] = 100
}
//main函数主体
func main() {
//数组
fmt.Printf("array:索引数组\n\n")
//数组
var arr1 [5]int
// for i := 0; i < len(arr1); i++ {
// fmt.Println(arr1[i])
// }
printArr(arr1)
fmt.Printf("Type is = %T\n\n", arr1)
//数组
arr2 := [5]int{1, 2, 3, 4}
for index, value := range arr2 {
fmt.Println("键:", index, "值:", value)
}
fmt.Printf("Type is = %T\n\n", arr2)
//数组1
fmt.Println("------a1------")
var a1 = []int{1, 2, 3}
printArr1(a1)
fmt.Println("------a1改变值------")
for index, value := range a1 {
fmt.Println("键:", index, "值:", value)
}
fmt.Printf("len = %d, cap = %d, value = %v\n\n", len(a1), cap(a1), a1)
//数组2
fmt.Println("------a2------")
a2 := []int{4, 5, 6}
fmt.Printf("len = %d, cap = %d, value = %v\n\n", len(a2), cap(a2), a2)
//数组3
fmt.Println("------a3------")
a3 := []int{}
fmt.Printf("len = %d, cap = %d, value = %v\n", len(a3), cap(a3), a3)
a3 = make([]int, 3)
a3[0] = 1
fmt.Printf("len = %d, cap = %d, value = %v\n\n", len(a3), cap(a3), a3)
//数组4
fmt.Println("------a4------")
a4 := make([]int, 3)
fmt.Printf("len = %d, cap = %d, value = %v\n", len(a4), cap(a3), a4)
a4[1] = 100
fmt.Printf("len = %d, cap = %d, value = %v\n\n", len(a4), cap(a4), a4)
//slice切片
fmt.Println("------slice------")
slice := []int{1, 2, 3, 4}
//var slice []int
fmt.Printf("len = %d, cap = %d, value = %v\n\n", len(slice), cap(slice), slice)
if slice == nil {
fmt.Println("slice是空切片!\n")
} else {
fmt.Println("slice不是空切片\n")
}
//append
fmt.Println("------append------")
append_arr := make([]int, 3, 5)
fmt.Printf("len = %d, cap = %d, value = %v\n\n", len(append_arr), cap(append_arr), append_arr)
append_arr = append(append_arr, 1)
fmt.Printf("len = %d, cap = %d, value = %v\n\n", len(append_arr), cap(append_arr), append_arr)
append_arr = append(append_arr, 2)
fmt.Printf("len = %d, cap = %d, value = %v\n\n", len(append_arr), cap(append_arr), append_arr)
append_arr = append(append_arr, 3, 4, 5)
fmt.Printf("len = %d, cap = %d, value = %v\n\n", len(append_arr), cap(append_arr), append_arr)
//slice切片截取(值引用),切片[low, high, max] 顾头不顾尾 [开始下标,结束下标,容量计算] (新容量 = max - low )
fmt.Println("------数组截取------")
s := []int{1, 2, 3, 4, 5, 6}
fmt.Printf("len = %d, cap = %d, value = %v\n\n", len(s), cap(s), s)
s1 := s[0:2]
fmt.Println(s1)
s1[0] = 100
s2 := s[0:1]
fmt.Println(s)
fmt.Println(s1)
fmt.Println(s2)
//copy
s3 := make([]int, len(s))
s1[0] = 666
copy(s3, s)
fmt.Println(s3)
s1[0] = 1
fmt.Println(s3)
}
十、map,map.go
//程序包名
package main
//导入函数
import (
"fmt"
)
//自定义函数
func printMap(mapData map[string]string) {
for index, value := range mapData {
fmt.Println("键:", index, "值:", value)
}
mapData["shoudu"] = "Beijing"
}
//main函数主体
func main() {
fmt.Println("---map:关联数组---\n")
//map类型1
var map1 map[string]string
fmt.Printf("Type is = %T\n\n", map1)
map1 = make(map[string]string)
map1["province"] = "FuJian"
map1["city"] = "XiaMen"
map1["area"] = "Huli"
fmt.Printf("len = %d, cap = %T, value = %v\n\n", len(map1), map1, map1)
fmt.Println(map1)
//map类型2
map2 := make(map[int]string)
map2[1] = "FuJian"
map2[2] = "XiaMen"
map2[3] = "Huli"
fmt.Println(map2)
//map类型3:最后一个值要带逗号
map3 := map[string]int{
"a": 1,
"b": 2,
}
fmt.Println(map3)
fmt.Println("\n")
//map类型4
map4 := map[string]string{
"province": "FuJian",
"city": "XiaMen",
"area": "Huli",
}
printMap(map4)
//删除
fmt.Println("\n------删除------")
delete(map4, "province")
printMap(map4)
//修改
fmt.Println("\n------修改------")
map4["city"] = "FuZhou"
printMap(map4)
fmt.Println("\n------原始------")
fmt.Println(map4)
}
十一、结构体,struct.go
//程序包名
package main
//导入函数
import (
"fmt"
)
//类型别名
type myint int
//定义结构体
type Book struct {
title string
number int
auth string
}
//自定义函数
func changeBook(book Book) {
book.title = "aaa"
}
func changeBook2(book *Book) {
book.title = "aaa"
}
//main函数主体
func main() {
fmt.Println("---struct---\n")
//自定义类型
var a myint = 10
fmt.Println("a = ", a)
fmt.Printf("Type is = %T\n", a)
//自定义结构体
//var book = Book{"标题", 100, "作者"}
book := Book{}
//var book Book
book.title = "标题"
book.number = 100
book.auth = "作者"
fmt.Println("book = ", book)
book.title = "1000"
fmt.Println("book = ", book)
changeBook(book)
fmt.Println("book = ", book)
changeBook2(&book)
fmt.Println("book = ", book)
}
十二、类,class.go
//程序包名
package main
//导入函数
import (
"fmt"
)
//定义结构体:大写代表public
type Hero struct {
name string //小写表示私有
Age int
Level string
}
//展示函数
func (this *Hero) Show() {
fmt.Println("Name = ", this.Name)
fmt.Println("Age = ", this.Age)
fmt.Println("Level = ", this.Level)
}
//设置函数
func (this *Hero) setName(name string) {
this.Name = name
}
//获取函数
func (this *Hero) getNname() string {
return this.Name
}
//main函数主体
func main() {
fmt.Println("---class---\n")
//实例化
hero := Hero{"飓风呀", 28, "100"}
//Show
hero.Show()
//setName
hero.setName("CopyLian")
hero.Show()
//getNname
heroName := hero.getNname()
fmt.Println("Name = ", heroName)
}
十三、继承,extend.go
//程序包名
package main
//导入函数
import (
"fmt"
)
//结构体1
type Human struct {
Name string
Age int
}
//结构体1方法
func (this *Human) Eat() {
fmt.Println("Human Eat...")
}
//结构体1方法
func (this *Human) Walk() {
fmt.Println("Human Walk...")
}
//结构体2
type SuperMan struct {
Human //继承Human类
Level string
}
//结构体3方法
func (this *SuperMan) Eat() {
fmt.Println("SuperMan Eat...")
}
//结构体3方法
func (this *SuperMan) Fly() {
fmt.Println("SuperMan Fly...")
}
//结构体3方法
func (this *SuperMan) Print() {
fmt.Println("Name = ", this.Name)
fmt.Println("Age = ", this.Age)
fmt.Println("Level = ", this.Level)
}
//main函数主体
func main() {
fmt.Println("---extend---\n")
//父类
fmt.Println("---父类---")
human := Human{"飓风呀", 100}
human.Eat()
human.Walk()
fmt.Println("\n")
//子类
fmt.Println("---子类---")
//superman := SuperMan{Human{"飓风呀", 100}, "100"}
//superman := SuperMan{}
var superman SuperMan
superman.Name = "飓风呀"
superman.Age = 100
superman.Level = "100"
superman.Eat()
superman.Walk()
superman.Fly()
superman.Print()
}
十四、接口,interface.go:接口本质上是一个指针
//程序包名
package main
//导入函数
import (
"fmt"
)
//interface结构体
type AnimalIf interface {
Sleep()
GetColor() string
GetType() string
}
//结构体1
type Dog struct {
color string
}
//结构体1重写方法
func (this *Dog) Sleep() {
fmt.Println("Dog is Sleep")
}
//结构体1重写方法
func (this *Dog) GetColor() string {
return this.color
}
//结构体1重写方法
func (this *Dog) GetType() string {
return "Dog"
}
//结构体2
type Cat struct {
color string
}
//结构体2重写方法
func (this *Cat) Sleep() {
fmt.Println("Cat is Sleep")
}
//结构体2重写方法
func (this *Cat) GetColor() string {
return this.color
}
//结构体2重写方法
func (this *Cat) GetType() string {
return "Cat"
}
//输出
func showAnimal(animal AnimalIf) {
animal.Sleep()
fmt.Println("color = ", animal.GetColor())
fmt.Println("type = ", animal.GetType())
}
//main函数主体
func main() {
fmt.Println("---interface---\n")
// //cat类
// cat := Cat{"black"}
// cat.Sleep()
// dcolor := cat.GetColor()
// ctype := cat.GetType()
// fmt.Println("color = ", dcolor)
// fmt.Println("type = ", ctype)
// //dog类
// dog := Dog{"yellow"}
// dog.Sleep()
// dogcolor := dog.GetColor()
// cattype := dog.GetType()
// fmt.Println("color = ", dogcolor)
// fmt.Println("type = ", cattype)
//interface类
var animal AnimalIf
//多态类
animal = &Cat{"white"}
showAnimal(animal)
//多态类
animal = &Dog{"yellow"}
showAnimal(animal)
}
十五、断言,interface{}.(类型)
//程序包名
package main
//导入函数
import (
"fmt"
)
//结构体
type Person struct {
Name string
}
//函数
func interfaceFun(arg interface{}) {
//fmt.Println(arg)
//给interface{} 提供类型断言机制
value, ok := arg.(string)
if ok {
fmt.Println(arg)
fmt.Println("是字符串")
fmt.Println(value)
} else {
fmt.Println(arg)
fmt.Println("不是字符串")
}
fmt.Println("\n")
}
//main函数主体
func main() {
fmt.Println("---interface{}---")
person := Person{"飓风呀"}
interfaceFun(person)
interfaceFun(200)
interfaceFun("100")
}
十六、反射,reflect.go
变量的结构:
//程序包名
package main
//导入函数
import (
"fmt"
"reflect"
)
//结构体
type Persons struct {
Name string
Age int
Level string
}
//结构体函数
func (this Persons) Call() {
fmt.Println("我是Call函数")
}
//结构体函数
func (this *Persons) Call2() {
fmt.Println("我是Call函数")
}
//函数
func myFun(arg interface{}) {
//类型、值
fmt.Println("我的类型是:", reflect.TypeOf(arg))
fmt.Println("我的值是:", reflect.ValueOf(arg))
//类型
mytype := reflect.TypeOf(arg)
myvalue := reflect.ValueOf(arg)
//fmt.Println(mytype.NumField())
//fmt.Println(myvalue.NumField())
//NumField 属性数量,通过Field获取字段,通过Field的方法Interface()获取值
for i := 0; i < mytype.NumField(); i++ {
field := mytype.Field(i)
value := myvalue.Field(i).Interface()
fmt.Printf("名称 = %s, 类型 = %s, 值 = %s\n", field.Name, field.Type, value)
}
//NumMethod 方法数量,通过Field获取字段,通过Field的方法Interface()获取值
fmt.Println(mytype.NumMethod())
for i := 0; i < mytype.NumMethod(); i++ {
method := mytype.Method(i)
fmt.Printf("名称 = %s, 类型 = %s\n", method.Name, method.Type)
}
}
//main函数主体
func main() {
fmt.Println("---reflect---")
//类型、值
// myFun(100)
// myFun("200")
persons := Persons{"飓风呀", 100, "100"}
myFun(persons)
persons.Call()
}
Golang reflect慢主要有两个原因
1、涉及到内存分配以及后续的GC;
2、reflect实现里面有大量的枚举,也就是for循环,比如类型之类的。
总结
1、反射可以大大提高程序的灵活性,使得interface{}有更大的发挥余地
反射必须结合interface才玩得转
变量的type要是concrete type的(也就是interface变量)才有反射一说
2、反射可以将“接口类型变量”转换为“反射类型对象”
反射使用 TypeOf 和 ValueOf 函数从接口中获取目标对象信息
3、反射可以将“反射类型对象”转换为“接口类型变量
reflect.value.Interface().(已知的类型)
遍历reflect.Type的Field获取其Field
4、反射可以修改反射类型对象,但是其值必须是“addressable”
想要利用反射修改对象状态,前提是 interface.data 是 settable,即 pointer-interface
通过反射可以“动态”调用方法,因为Golang本身不支持模板,因此在以往需要使用模板的场景下往往就需要使用反射(reflect)来实现
十七、结构体标签,tag.go
//程序包名
package main
//导入函数
import (
"fmt"
"reflect"
)
//结构体
type Persons2 struct {
Name string `info:"姓名" doc:"字符串"` //英文符号:``
Age int `info:"年龄"`
}
//结构体函数
func (this *Persons2) Call() {
fmt.Println("我是Call函数")
}
//函数
func myFun1(arg interface{}) {
//获取类型:标签
t := reflect.TypeOf(arg)
for i := 0; i < t.NumField(); i++ {
taginfo := t.Field(i).Tag.Get("info")
//fmt.Println(t.Field(i).Tag)
tagdoc := t.Field(i).Tag.Get("doc")
fmt.Println(taginfo, tagdoc)
}
}
//main函数主体
func main() {
fmt.Println("---tag---")
persons := Persons2{"飓风呀", 100}
myFun1(persons)
}
十八、JSON,json.go
//程序包名
package main
//导入函数
import (
"encoding/json"
"fmt"
)
//结构体
type Movie struct {
Title string `json:"title"` //英文符号:``
Year int `json:"sx"`
Prize int `json:"prz"`
Actors []string `json:"act"`
}
//main函数主体
func main() {
fmt.Println("---json---")
//json加密
movie := Movie{"飓风呀", 2022, 10000, []string{"飓风呀", "CopyLian"}}
jsonStr, err := json.Marshal(movie)
if err != nil {
fmt.Println("json err:", err)
return
}
fmt.Printf("%s", jsonStr)
//json解密
var myMovie Movie
err = json.Unmarshal(jsonStr, &myMovie)
if err != nil {
fmt.Println("json decode err:", err)
return
}
fmt.Printf("%v", myMovie)
}
十九、协程,goroutine.go
//程序包名
package main
//导入函数
import (
"fmt"
"time"
)
//任务函数
func goFun() {
for i := 0; i < 10000; i++ {
fmt.Println("goFun, i = ", i)
time.Sleep(1 * time.Second)
}
}
//main函数主体
func main() {
fmt.Println("---goroutine---")
//执行
// go goFun()
// //主函数
// for i := 0; i < 10000; i++ {
// fmt.Println("main, i = ", i)
// time.Sleep(1 * time.Second)
// }
//匿名函数:形参空、返回空
// go func() {
// defer fmt.Println("A.defer")
// func() {
// defer fmt.Println("B.defer")
// //退出当前的goroutine
// runtime.Goexit()
// fmt.Println("B")
// }()
// fmt.Println("A")
// }()
//匿名函数定义参数、返回值
go func(a int, b int) bool {
fmt.Println("a = ", a, "b = ", b)
return true
}(10, 20)
//死循环
for {
time.Sleep(1 * time.Second)
}
}
二十、管道,channel.go
channel是Go语言中的一个核心类型,可以把它看成管道。并发核心单元通过它就可以发送或者接收数据进行通讯,这在一定程度上又进一步降低了编程的难度。
channel是一个数据类型,主要用来解决go程的同步问题以及go程之间数据共享(数据传递)的问题。
goroutine运行在相同的地址空间,因此访问共享内存必须做好同步。goroutine 奉行通过通信来共享内存,而不是共享内存来通信。
引⽤类型 channel可用于多个 goroutine 通讯。其内部实现了同步,确保并发安全。
定义channel变量
和map类似,channel也一个对应make创建的底层数据结构的引用。
当我们复制一个channel或用于函数参数传递时,我们只是拷贝了一个channel引用,因此调用者和被调用者将引用同一个channel对象。和其它的引用类型一样,channel的零值也是nil。
定义一个channel时,也需要定义发送到channel的值的类型。channel可以使用内置的make()函数来创建:
chan是创建channel所需使用的关键字。Type 代表指定channel收发数据的类型。
make(chan Type) //等价于make(chan Type, 0)
make(chan Type, capacity)
当 参数capacity= 0 时,channel 是无缓冲阻塞读写的;当capacity > 0 时,channel 有缓冲、是非阻塞的,直到写满 capacity个元素才阻塞写入。
channel非常像生活中的管道,一边可以存放东西,另一边可以取出东西。channel通过操作符 <- 来接收和发送数据,发送和接收数据语法:
channel <- value //发送value到channel
<-channel //接收并将其丢弃
x := <-channel //从channel中接收数据,并赋值给x
x, ok := <-channel //功能同上,同时检查通道是否已关闭或者是否为空
无缓冲的channel
无缓冲的通道(unbuffered channel)是指在接收前没有能力保存任何数据值的通道。
有缓冲的channel
有缓冲的通道(buffered channel)是一种在被接收前能存储一个或者多个数据值的通道。
chan<- 表示数据进入管道,要把数据写进管道,对于调用者就是输出。
<-chan 表示数据从管道出来,对于调用者就是得到管道的数据,当然就是输入。
//程序包名
package main
//导入函数
import (
"fmt"
)
//main函数主体
func main() {
fmt.Println("---channel---")
//定义channel:无缓存
c := make(chan int)
//goroutine
go func() {
//结束
defer fmt.Println("goroutine结束")
fmt.Println("goroutine正在执行...")
//100发送到通道
c <- 100
}()
//读取通道值
num := <-c
//输出
fmt.Println("num = ", num)
}
//程序包名
package main
//导入函数
import (
"fmt"
"time"
)
//main函数主体
func main() {
fmt.Println("---channel_cache---")
//定义channel:有缓存
c := make(chan int, 3)
//输出
fmt.Printf("len = %d, cap = %d, value = %v\n\n", len(c), cap(c), c)
//goroutine
go func() {
//结束
defer fmt.Println("goroutine结束")
fmt.Println("goroutine正在执行...")
//循环
for i := 0; i < 5; i++ {
c <- i //数据写入通道
fmt.Printf("len = %d, cap = %d, value = %v\n\n", len(c), cap(c), c)
}
}()
time.Sleep(2 * time.Second)
//读取通道值
for i := 0; i < 5; i++ {
num := <-c //从通道读取数据
fmt.Println("num = ", num)
}
}
//程序包名
package main
//导入函数
import (
"fmt"
)
//main函数主体
func main() {
fmt.Println("---channel_close---")
//定义channel:有缓存
c := make(chan int, 3)
//输出
fmt.Printf("len = %d, cap = %d, value = %v\n\n", len(c), cap(c), c)
//goroutine
go func() {
defer fmt.Println("goroutine 结束")
//循环
for i := 0; i < 4; i++ {
c <- i //数据写入通道
//关闭通道
//close(c)
}
//关闭通道
close(c)
}()
//读取通道值
// for {
// //ok如果为true则channel未关闭,false则表示关闭
// if num, ok := <-c; ok { //从通道读取数据
// fmt.Println("num = ", num)
// } else {
// break
// }
// }
//可以采用range来迭代不断的操作channel
for num := range c {
fmt.Println("num = ", num)
}
fmt.Println("main 结束...")
}
单流程下⼀个go只能监控⼀个channel的状态,select可以完成 监控多个channel的状态,select具备多路channel的监控状态功能
//程序包名
package main
//导入函数
import (
"fmt"
)
//自定义函数
func goroutineFn(c, c2 chan int) {
x, y := 1, 1
for {
select {
case c <- x:
//如果c可写则case会进来
x = y
y = x + y
case <-c2:
fmt.Printf("c2结束\n")
return
}
}
}
//main函数主体
func main() {
fmt.Println("---channel_select---")
//定义channel
c := make(chan int)
c2 := make(chan int)
//goroutine
go func() {
defer fmt.Println("goroutine 结束")
//循环
for i := 0; i < 10; i++ {
fmt.Println("c = ", <-c)
}
//数据写入通道c2
c2 <- 0
}()
goroutineFn(c, c2)
fmt.Println("main 结束...")
}
二十一、Modules模式,Go Modules
go mod命令
命令 作用
go mod init 生成 go.mod 文件
go mod download 下载 go.mod 文件中指明的所有依赖
go mod tidy 整理现有的依赖
go mod graph 查看现有的依赖结构
go mod edit 编辑 go.mod 文件
go mod vendor 导出项目所有的依赖到vendor目录
go mod verify 校验一个模块是否被篡改过
go mod why 查看为什么需要依赖某模块
go mod 环境变量
可以通过 go env 命令来进行查看
$ go env
GO111MODULE="auto"
GOPROXY="https://proxy.golang.org,direct"
GONOPROXY=""
GOSUMDB="sum.golang.org"
GONOSUMDB=""
GOPRIVATE=""
GO111MODULE
可以通过来设置
$ go env -w GO111MODULE=on
GOPROXY
这个环境变量主要是用于设置 Go 模块代理(Go module proxy),其作用是用于使 Go 在后续拉取模块版本时直接通过镜像站点来快速拉取。GOPROXY 的默认值是:https://proxy.golang.org,direct
proxy.golang.org国内访问不了,需要设置国内的代理。GOPROXY 的值是一个以英文逗号 “,” 分割的 Go 模块代理列表,允许设置多个模块代理,假设你不想使用,也可以将其设置为 “off” ,这将会禁止 Go 在后续操作中使用任何 Go 模块代理。
阿里云:https://mirrors.aliyun.com/goproxy/
七牛云:https://goproxy.cn,direct
如:$ go env -w GOPROXY=https://goproxy.cn,https://mirrors.aliyun.com/goproxy/,direct
实际上 “direct” 是一个特殊指示符,用于指示 Go 回源到模块版本的源地址去抓取(比如 GitHub 等)
GOSUMDB
它的值是一个 Go checksum database,用于在拉取模块版本时(无论是从源站拉取还是通过 Go module proxy 拉取)保证拉取到的模块版本数据未经过篡改,若发现不一致,也就是可能存在篡改,将会立即中止。
GOSUMDB 的默认值为:sum.golang.org,在国内也是无法访问的,但是 GOSUMDB 可以被 Go 模块代理所代理(详见:Proxying a Checksum Database)。
因此我们可以通过设置 GOPROXY 来解决,而先前我们所设置的模块代理 goproxy.cn 就能支持代理 sum.golang.org,所以这一个问题在设置 GOPROXY 后,你可以不需要过度关心。
另外若对 GOSUMDB 的值有自定义需求,其支持如下格式:
格式 1:<SUMDB_NAME>+<PUBLIC_KEY>。格式 2:<SUMDB_NAME>+<PUBLIC_KEY> <SUMDB_URL>。
也可以将其设置为“off”,也就是禁止 Go 在后续操作中校验模块版本。
GONOPROXY/GONOSUMDB/GOPRIVATE
这三个环境变量都是用在当前项目依赖了私有模块,例如像是你公司的私有 git 仓库,又或是 github 中的私有库,都是属于私有模块,都是要进行设置的,否则会拉取失败。
更细致来讲,就是依赖了由 GOPROXY 指定的 Go 模块代理或由 GOSUMDB 指定 Go checksum database 都无法访问到的模块时的场景。
而一般建议直接设置 GOPRIVATE,它的值将作为 GONOPROXY 和 GONOSUMDB 的默认值,所以建议是直接使用 GOPRIVATE。
并且它们的值都是一个以英文逗号 “,” 分割的模块路径前缀,也就是可以设置多个,例如:
$ go env -w GOPRIVATE="git.example.com,github.com/eddycjy/mquote"
设置后,前缀为 git.xxx.com 和 github.com/eddycjy/mquote 的模块都会被认为是私有模块。
如果不想每次都重新设置,我们也可以利用通配符,例如:
$ go env -w GOPRIVATE="*.example.com"
这样子设置的话,所有模块路径为 example.com 的子域名(例如:git.example.com)都将不经过 Go module proxy 和 Go checksum database,需要注意的是不包括 example.com 本身。
//程序包名
package main
//导入函数
import (
"fmt"
)
//main函数主体
func main() {
fmt.Println("---gopath_gomod---")
//1、go env
//2、echo $GOPATH
//3、GOPATH:无版本控制概念、⽆法同步⼀致第三⽅版本号、⽆法指定当前项⽬引⽤的第三⽅版本号
//4、go env -w GO111MODULE=on
//5、GOPROXY:七牛云:https://goproxy.cn,direct,阿里云:https://mirrors.aliyun.com/goproxy/
//6、GOSUBDB、GONOSUMDB、GONOPROXY、GOPRIVATE = "*.examp.com"
//7、go mod edit -replace=github.com/aceld/zinx=github.com/cilium/ebpf@v0.7.0
//go mod help
//go env | grep GOPROXY
//go get xxx
//indirect:间接依赖
}
创建mod:
1、任意⽂件夹创建⼀个项⽬(不要求在$GOPATH/src) ,创建go.mod⽂件,同时起当前项⽬的模块名称
mkdir -p $HOME/copylian/modtest
go mod init copylian.com/modtest
就会⽣成⼀个go mod⽂件
module github.com/aceld/moudles_test
go 1.14
2、添加依赖库,如果源代码中依赖某个库(⽐如: github.com/aceld/zinx/znet)
⼿动down:go get github.com/aceld/zinx/znet
⾃动down
go mod ⽂件会添加⼀⾏新代码
module github.com/aceld/moudles_test
go 1.14
require github.com/aceld/zinx v0.0.0-20200315073925-f09df55dc746 // indirect
含义当前模块依赖github.com/aceld/zinx,依赖的版本是 v0.0.0-20200315073925-f09df55dc746 ,//indirect 表示间接依赖,因为项⽬直接依赖的是znet包,所以所间接依赖zinx包
3、会⽣成⼀个go.sum⽂件
github.com/aceld/zinx v0.0.0-20200315073925-f09df55dc746 h1:TturbcEfboY81jsKVSQtGkqk8FN8ag0TmKYzaFHflmQ=
github.com/aceld/zinx v0.0.0-20200315073925-f09df55dc746/go.mod h1:bMiERrPdR8FzpBOo86nhWWmeHJ1cCaqVvWKCGcDVJ5M=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
go.sum⽂件的作⽤:罗列当前项⽬直接或间接的依赖所有模块版本,保证今后项⽬依赖的版本不会被篡改,h1:hash:表示整体项⽬的zip⽂件打开之后的全部⽂件的校验和来⽣成的hash,如果不存在,可能表示依赖的库可能⽤不上,xxx/go.mod h1:hash go.mod⽂件做的hash
4、修改项⽬模块的版本依赖关系
go mod edit -replace=zinx@v0.0.0-20200306023939bc416543ae24=zinx@v0.0.0-20200221135252-8a8954e75100
go mod⽂件就会被修改
module github.com/aceld/modules_test
go 1.14
require github.com/aceld/zinx v0.0.0-20200306023939-bc416543ae24 // indirect
replace zinx v0.0.0-20200306023939-bc416543ae24 => zinx v0.0.0-20200221135252-8a8954e75100
二十一、Golang⽣态拓展
Web框架:
beego https://github.com/astaxie/beego
gin https://github.com/gin-gonic/gin
echo https://github.com/labstack/echo
Iris https://github.com/kataras/iris
微服务框架:
go kit http://gokit.io/
Istio https://istio.io/
容器编排:
Kubernetes https://github.com/kubernetes/kubernetes
swarm https://github.com/docker/classicswarm
服务发现
consul https://github.com/hashicorp/consul
存储引擎
k/v存储 etcd https://github.com/coreos/etcd
分布式存储 tidb https://github.com/pingcap/tidb
静态建站
hugo https://github.com/gohugoio/hugo
中间件
消息队列 nsq https://github.com/nsqio/nsq
Tcp⻓链接框架(轻量级服务器) zinx https://github.com/aceld/zinx
Leaf(游戏服务器) https://github.com/name5566/leaf
RPC框架 gPRC https://grpc.io/ https://github.com/grpc/grpc-go
redis集群 codis https://github.com/CodisLabs/codis
爬⾍框架
go query https://github.com/PuerkitoBio/goquery
二十二、简单即时通讯案例(IM)
生成server:go bulid -o server server.go user.go main.go
生成client:go bulid -o client client .go
开启服务:./server
客户端连接:nc 127.0.0.1 8080
1、入口main.php
//程序包名
package main
//main函数主体
func main() {
server := NewServer("127.0.0.1", 8080)
server.Start()
}
2、用户端:user.go
//程序包名
package main
import (
"net"
"strings"
)
//User类
type User struct {
Name string
Addr string
C chan string
conn net.Conn
server *Server //服务类
}
//创建一个用户的API
func NewUser(conn net.Conn, server *Server) *User {
userAddr := conn.RemoteAddr().String()
//实例化User
user := &User{
Name: userAddr,
Addr: userAddr,
C: make(chan string),
conn: conn,
server: server,
}
//启动监听User channel消息的goroutine
go user.ListenMessage()
return user
}
//用户上线
func (this *User) Online() {
//用户上线,将用混加入到onlineMap中
this.server.mapLock.Lock()
this.server.OnlineMap[this.Name] = this
this.server.mapLock.Unlock()
//广播当前用户上线消息
this.server.Broadcast(this, "已上线")
}
//用户下线
func (this *User) Offline() {
//用户下线
this.server.mapLock.Lock()
delete(this.server.OnlineMap, this.Name)
this.server.mapLock.Unlock()
this.server.Broadcast(this, "下线")
}
//给当前客户端发送消息
func (this *User) SendMsg(msg string) {
this.conn.Write([]byte(msg + "\r\n"))
}
//广播消息
func (this *User) DoMessage(msg string) {
//处理消息
if msg == "who" {
for _, user := range this.server.OnlineMap {
onlineMsg := "[" + user.Addr + "]" + user.Name + ":" + "在线..."
this.SendMsg(onlineMsg)
}
} else if len(msg) > 7 && msg[:7] == "rename|" {
//新名称
newName := strings.Split(msg, "|")[1]
//判断新名称是否存在
_, ok := this.server.OnlineMap[newName]
if ok {
this.SendMsg("当前用户名已被使用")
} else {
//删除旧数据,新加新数据
this.server.mapLock.Lock()
delete(this.server.OnlineMap, this.Name)
this.server.OnlineMap[newName] = this
this.server.mapLock.Unlock()
this.Name = newName
this.SendMsg("您已更新用户名:" + this.Name)
}
} else if len(msg) > 4 && msg[:3] == "to|" {
//消息格式:to|张三|内容
//1、获取对方用户名
remoteName := strings.Split(msg, "|")[1]
if remoteName == "" {
this.SendMsg("消息格式不正确,请使用\"to|张三|你好啊\"格式")
return
}
//2、根据用户名,得到对方的User对象
remoteUser, ok := this.server.OnlineMap[remoteName]
if !ok {
this.SendMsg("该用户名不存在")
return
}
//3、获取消息内容,通过对方的User对象将消息内容发出去
content := strings.Split(msg, "|")[2]
if content == "" {
this.SendMsg("无消息内容,请重发")
return
}
remoteUser.SendMsg(this.Name + "对您说:" + content)
} else {
this.server.Broadcast(this, msg)
}
}
//监听当前User channel的方法,一旦有消息,就直接发送给客户端
func (this *User) ListenMessage() {
for {
msg := <-this.C
this.conn.Write([]byte(msg + "\r\n"))
}
}
3、服务端:server.go
//程序包名
package main
//导入函数
import (
"fmt"
"io"
"net"
"strings"
"sync"
"time"
)
//服务类
type Server struct {
Ip string
Port int
//在线用户列表
OnlineMap map[string]*User
mapLock sync.RWMutex
//消息广播的channel
Message chan string
}
//创建Server接口
func NewServer(ip string, port int) *Server {
server := &Server{
Ip: ip,
Port: port,
OnlineMap: make(map[string]*User),
Message: make(chan string),
}
return server
}
//监听Message广播消息channel的goroutine,一旦有消息就发送给全部的在线User
func (this *Server) ListenMessager() {
for {
msg := <-this.Message
//将消息发送给全部的User
this.mapLock.Lock()
for _, cli := range this.OnlineMap {
cli.C <- msg
}
this.mapLock.Unlock()
}
}
//广播消息的方法
func (this *Server) Broadcast(user *User, msg string) {
sendMsg := "[" + user.Addr + "]" + user.Name + ":" + msg
this.Message <- sendMsg
}
//Handler 处理方法
func (this *Server) Handler(conn net.Conn) {
//当前链接业务
//fmt.Println("链接建立成功")
//实例化用户
user := NewUser(conn, this)
//用户上线,将用混加入到onlineMap中
user.Online()
//监听用户是否活跃channel
isLive := make(chan bool)
//接收客户端发送的消息
go func() {
buf := make([]byte, 4096)
for {
n, err := conn.Read(buf)
if n == 0 {
user.Offline()
return
}
if err != nil && err != io.EOF {
fmt.Println("Conn Read err: ", err)
return
}
//提取用户的信息(去除\n)
msg := string(buf[:n])
msg = strings.Replace(msg, "\n", "", 1)
msg = strings.Replace(msg, "\r", "", 1)
//将得到的消息进行广播
user.DoMessage(msg)
//用户的任意消息,代表用户是否活跃
isLive <- true
}
}()
//当前Handler阻塞
for {
select {
case <-isLive:
//当前用户是活跃的,应该重置定时器
//不做任何事情,为了激活select,更新定时器
case <-time.After(time.Second * 300):
//已超时
//将当前的user强制关闭
user.SendMsg("你被提了\r\n")
//销毁资源
close(user.C)
//关闭链接
conn.Close()
//退出当前的Handler,runtime.Goexit()
return
}
}
}
//启动函数
func (this *Server) Start() {
//socket 监听
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", this.Ip, this.Port))
if err != nil {
fmt.Println("net.Listen:", err)
return
}
//关闭 socket
defer listener.Close()
//启动监听Message的goroutine
go this.ListenMessager()
for {
//接收
conn, err := listener.Accept()
if err != nil {
fmt.Println("listener.Accept:", err)
continue
}
//处理 Handler
go this.Handler(conn)
}
}
4、客户端:client.go
package main
import (
"flag"
"fmt"
"io"
"net"
"os"
)
type Client struct {
ServerIp string
ServerPort int
Name string
conn net.Conn
flag int
}
func NewClient(serverIp string, serverPort int) *Client {
//创建客户端对象
client := &Client{
ServerIp: serverIp,
ServerPort: serverPort,
flag: 999,
}
//链接服务端
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", serverIp, serverPort))
if err != nil {
fmt.Println("net.Dial error:", err)
}
client.conn = conn
//返回对象
return client
}
//处理server的回应消息,直接显示到标准输出即可
func (client *Client) DealResponse() {
//一旦client.conn有数据,数据直接copy到stdout标准输出上,永久阻塞监听
io.Copy(os.Stdout, client.conn)
}
//菜单
func (this *Client) menu() bool {
var flag int
fmt.Println("1.公聊模式")
fmt.Println("2.私聊模式")
fmt.Println("3.更新用户名")
fmt.Println("0.退出")
fmt.Scanln(&flag)
if flag >= 0 && flag <= 3 {
this.flag = flag
return true
} else {
fmt.Println("请输入合法范围内的数字")
return false
}
}
//更新用户名
func (this *Client) UpdateName() bool {
fmt.Println(">>>请输入用户名:")
fmt.Scanln(&this.Name)
sendMsg := "rename|" + this.Name
_, err := this.conn.Write([]byte(sendMsg))
if err != nil {
fmt.Println("conn.Write error:", err)
return false
}
return true
}
//公聊模式
func (this *Client) PublicChat() {
//用户输入的消息
var chatMsg string
fmt.Println("<<<<请输入聊天内容, exit退出.")
fmt.Scanln(&chatMsg)
for chatMsg != "exit" {
//消息发送给服务器,不为空则发送
if len(chatMsg) != 0 {
sendMsg := chatMsg
_, err := this.conn.Write([]byte(sendMsg))
if err != nil {
fmt.Println("conn.Write err", err)
break
}
}
chatMsg = ""
fmt.Println("<<<<请输入聊天内容, exit退出.")
fmt.Scanln(&chatMsg)
}
}
//查询用户
func (this *Client) SelectUsers() {
sendMsg := "who"
_, err := this.conn.Write([]byte(sendMsg))
if err != nil {
fmt.Println("conn.Write err", err)
return
}
}
//私聊模式
func (this *Client) PrivateChat() {
var RemoteName string
var chatMsg string
this.SelectUsers()
fmt.Println("请输入聊天对象[用户名], exit退出.")
fmt.Scanln(&RemoteName)
for RemoteName != "exit" {
fmt.Println("<<<请输入消息内容, exit退出.")
fmt.Scanln(&chatMsg)
for chatMsg != "exit" {
//消息发送给服务器,不为空则发送
if len(chatMsg) != 0 {
sendMsg := "to|" + RemoteName + "|" + chatMsg
_, err := this.conn.Write([]byte(sendMsg))
if err != nil {
fmt.Println("conn.Write err", err)
break
}
}
chatMsg = ""
fmt.Println("<<<<请输入消息内容, exit退出.")
fmt.Scanln(&chatMsg)
}
this.SelectUsers()
fmt.Println("请输入聊天对象[用户名], exit退出.")
fmt.Scanln(&RemoteName)
}
}
//菜单
func (this *Client) Run() {
for this.flag != 0 {
for this.menu() != true {
}
//根据不同模式进入不同业务
switch this.flag {
case 1:
this.PublicChat()
break
case 2:
this.PrivateChat()
break
case 3:
this.UpdateName()
break
}
}
}
var ServerIp string
var ServerPort int
// ./client -ip 127.0.0.1 -port 8080
func init() {
//设置命令行
flag.StringVar(&ServerIp, "ip", "127.0.0.1", "设置服务器IP地址(默认:127.0.0.1)")
flag.IntVar(&ServerPort, "port", 8080, "设置服务端口(默认:8080)")
}
func main() {
//解析命令行
flag.Parse()
client := NewClient(ServerIp, ServerPort)
if client == nil {
fmt.Println("链接服务器失败")
return
}
fmt.Println("链接服务器成功...")
//开启一个goroutine去处理server的回执消息
go client.DealResponse()
//菜单
client.Run()
}
参考资料&教程
1、go get 快速导入GitHub中的包:https://www.jianshu.com/p/16aceb6369b6
2、goland编写go语言导入自定义包出现: package xxx is not in GOROOT (/xxx/xxx) 的解决方案:https://blog.csdn.net/qq_27184497/article/details/122160400
3、Vscode中Golang引入自定义包报错 package xxx is not in GOROOT:https://blog.csdn.net/qq_40209780/article/details/123133467
4、GO111MODULE的设置(及GOPROXY):https://www.cnblogs.com/pu369/p/12068645.html
5、关于go反射中的NumMethod方法的一点发现:https://www.bilibili.com/read/cv8985976/
6、VS Code 安装go插件失败分析及解决方案:https://blog.csdn.net/qq_36564503/article/details/124509832
7、VSCode: Could not import Golang package:https://stackoverflow.com/questions/58518588/vscode-could-not-import-golang-package
8、vscode 远程开发 go 提示 You are outside of a module and outside of $GOPATH/src:https://www.jianshu.com/p/089efae0bdd3
基础教程:
视频1:https://www.bilibili.com/video/BV1gf4y1r79E
笔记1:https://www.yuque.com/aceld/mo95lb/zwukev
视频2:https://www.bilibili.com/video/BV1s341147US
笔记2:https://www.bilibili.com/read/readlist/rl496566
进阶教程:
文明上网理性发言!
已完结!