初识go语言| 青训营笔记

135 阅读14分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第二篇笔记

前言:

Go(又称Golang)是Google开发的一种静态、强类型、编译型、并发型,并具有垃圾回收功能的编程语言。

近期通过自己的学习,对go语言有了一定的认识,以下是我通过查阅资料,在学习过程中整理的一些笔记,在此与大家分享,希望对大家有所帮助。笔记内容偏向于基础语法类,文章结尾有惊喜。

一、基本语法:

1、变量:

变量声明:var 变量名 类型(如:var a,b float64)

变量初始化:a= 赋值1,b= 赋值2

自动推导类型:a:= 赋值

a,b=12,45(等价于:a=12,b=45)---声明变量的同时并赋值

a,b=b,a //直接交换a,b的值,即运行后:a=45,b=12

匿名变量:下划线"-",不想要的参数位置用"-"代替即可,用来丢弃某个值

常量(不允许修改其值):constant 变量名 类型 = 赋值

自动推导:constant c=12------直接定义常量c为int型,并赋值为12

多个常量或变量定义:

var(//同时声明变量,a,b,c,其类型由每行变量的类型决定
    a int
    b float64
    c string 
    d:=34//可以自动推导,只能用":="
    )
const (//同样可以一次声明多个常量,且类型标识可写可不写
        j int =10//有类型标识
    k float =3.14
    m =34//无类型标识,只能用"="不能用":=""
    n 23.45
        )
2、iota 枚举:

1,iota常量自动生成器,每个一行,自动累加一,同行相等

2,iota给常量赋值使用

3,iota遇到const重置为零,iota是第几行变量其值就是:行数-1

const(
    a1=iota//a1=0
    a2=iota//a2=1
    a3,a4,a5=iota,iota,iota//a3,a4,a5都为2
    a6=iota//a6=3
        )
3、变量输入与输出:
//输入:
var a,b int 
var c string
fmt.Scanf("%d%d%c",&a,&b,&c)//格式化输入
fmt.Scan(&a,&b,&c)//自动检测,简便输入
//输出
fmt.Printf("%d,%d,%c",a,b,c)//格式化输出
fmt.Println(a,b,c)//直接输出a,b,c对应的值,并自动换行
​

格式化字符含义:

image.png

4、defer的使用:
package main
​
import "fmt"func main(){
    defer fmt.Println("aaaaa")//defer语句在程序结束前瞬间执行
    defer fmt.Println("bbbbb")
    defer fmt.Println("ccccc")//多个defer语句存在时,由下至上依次执行
​
}
//运行结果如下图

image.png

5、类型别名:
type bigint int64 //把int64类型改为bigint 
var a bigint //声明bigint型变量a,也可以看作声明int64类型变量a
type(//一次别名多个变量
    myint int//int改名为myint
    mystr string//string改名为mystr
    )
​
6、条件语句:
//if 类型 
if bool1{ 
    //待执行命令 
}else if bool2{ 
    //待执行命令
}else { 
    //待执行命令 
}
//switch类型 
//switch用于在不同条件下执行不同语句,每一个case都是唯一分支
//测试顺序由上至下,直至匹配为止
//case后面不用跟break 
//switch分别有带变量和不带变量两种类型 
//带变量
switch var1 { 
    case 0: 
    //待执行命令 
    case 1: 
    //待执行命令
    default:
    //待执行命令 
}
//不带变量
switch { 
    case var1 > 2: 
    //待执行命令 
    case var1 > 0: 
    //待执行命令 
    default:
    //待执行命令
}
//用switch判断变量类型
//可以用来检测v的类型(必须创建空接口,因为空接口可以接收任何类型的变量)
func Receiver(v interface{}) {
    switch v.(type){
    case string: fmt.Println("这个是string")
    case int :fmt.Println("这个是int")
    case bool :fmt.Println("这个是bool")
    }
​
}
7、循环语句:
//golang中只有for循环,没有while循环
//和C的for一样
for init; condition; post { 
    //待执行命令 
}
//和C的while一样
for condition{
    //待执行命令 
}
//例 for x:=0;x<5;x++{
fmt.Println(x)
}
for true{ 
    fmt.Println("无限循环") 
}
//循环中可使用break跳出循环 
//循环中可使用continue跳过当前循环,进行下一次循环
//可使用goto实现条件转移,跳出循环等(强烈不建议使用)//特殊语句:switch v.(type){
                case 情况1:语句
                case 情况2:语句
                ......
                }
//可以用来检测v的类型(必须创建空接口,因为空接口可以接收任何类型的变量)
func Receiver(v interface{}) {
    switch v.(type){
    case string: fmt.Println("这个是string")
    case int :fmt.Println("这个是int")
    case bool :fmt.Println("这个是bool")
    }
​
}
8、函数:
//格式 
func 函数名(输入的变量及类型,可多个多种)(返回参数及类型,可多个多种){ 
    //函数体 
    return a,b//需要返回函数值就有return,否则就不用return
}
//例 
func test(B bool , I int , S string)(bool,int){ 
    //当B为true输出i,反之输出s 
    //当B为true返回false和从0到I的多个正整数
    //当B为false返回true和0 
    a:=1 
    b:=0 
    if B{
        for x := 0; x < I; x++ {
        fmt.Println(x) 
        }
        return false,a 
    }else{
        fmt.Println(S) 
        return true,b 
    }
}
//range 
//用于迭代数组、切片、通道、集合 
for num:=range nums{
    //待执行命令
}
//例 
nums := []int{2, 3, 4, 5, 6}
for num:=range nums{ 
    fmt.Println(nums[num]) 
}
//注意,num是从0开始的非负整数//不定参数函数(格式:func 函数名(变量名...数据类型)返回值{执行代码} )
func add(nums...int)int{//输入的任意个数的参数以数组的形式存入数组nums中,nums可以任意命名,后面紧跟"...类型"
    var sum int
    for _,date:=range nums{
        sum=sum + date
    }
    return sum
}
​
func main(){
    sum:=add(1,234)//实现了输入数据的相加求和,输入数据的个数任意
    fmt.Println(sum)
}
9、指针:
//声明格式:var 变量名 *类型
//例:var p1 *int    //声明了一个int型指针变量p1
    var p2 *string  //声明了一个string型指针变量p2
给指针型变量赋值时,被赋值变量要加取值符“&”
例:b:=10,a:=12
    p1=&b//把变量b的地址赋值给了p1,同时p1地址对应的变量值等于b,为10,p1与b对应的地址相同
    *p1=b//把变量b的值复制并储存到了地址p1,此时*p1=12,但p1!=&b
//实例
package main
​
import "fmt"
​
func main(){
    var p *string
    ph:="这是指针"
    py:="这也是指针"
    p=&ph ------>操作1
    fmt.Printf("*p的值是:%v\n",*p)
    fmt.Printf("p的地址是:%v\n",p)
    fmt.Printf("p的地址是:%v\n",&ph)
    *p=py  ------>操作2
    fmt.Printf("*p的值是:%v\n",*p)
    fmt.Printf("p的地址是:%v\n",p)
    fmt.Printf("p的地址是:%v\n",&ph)
}
//运行结果:
*p的值是:这是指针
p的地址是:0xc000088220
p的地址是:0xc000088220
*p的值是:这也是指针
p的地址是:0xc000088220  ——————可见进行操作2时并未改变p的地址
p的地址是:0xc000088220
​
进程 已完成,退出代码为 0
10、数组:

数组长度,容量固定

//一维数组:
var a[] int//int为数组类型,也可以定义为string型
var a[5]int = [5]int{ 1,2,3,4,5}//定义数组并赋值
b:=[5]int{ 1,2,3,4,5}//短声明并赋值
c:=[5]int{2:10,4:20}//给指定元素赋值,其结果为:a[2]=10,a[4]=20,其余元素均为0
//二维数组:
var a[3][4]int //定义数组:第一个表示行数,第二个表示列数,a[][]后边紧跟数组类型
b:=[3][4]int{
    {1,2,3,4},//
    {2,3,4,1},//这三行可以不换行,但","必须要有
    {3,4,1,2},//
}              //短定义并初始化二维数组b
11、切片:

长度,容量可变

s:= []int{}//切片[]里面为空或"...",切片的长度和容量可以不固定
s:=make([]int,n,m)//创建了一个s切片,n长度,m为容量
s=append(s,7)//给切片末尾追加一个成员
12、字典map:
//格式:map[KeyType]valueType   //key---->valuey 一一对应
var m1 map=[int]string{     //也可以用短声明:m1:=map[int]string{110: "mike",111: "yoyo",112: "lily"}
    110:"mike",
    111:"yoyo",
    112:"lily",    //每行结束必须要有","
}
//也可以用make来创建
m2:=make(map[int]string)
//可以指定容量
m2:=make(map[int]string10)//这个10是指容量,不是长度;但并不固定,可以后续追加,跟append有相似之处
m2[i]="xxxx"//向m2中添加元素
//遍历
for key,value:=range m1{
    fmt.Printf("key=%v value=%v",key,value)
}
//map删除: delete(map名,key)
delete(m1,110)//把map:m1中110key对应的元素删除了

image.png

13、结构体:

image.png

package main
​
import "fmt"
//定义一个结构体类型
type student struct{
    //结构体包含的成员及成员类型
    id int
    name string
    sex string
    age int
    address string
}
func main() {
    zhang:=student{1,"张","男",18,"重庆"}//定义并赋值结构体
    fmt.Println(zhang)
    fmt.Println("性别:",zhang.sex)
//结构体全部赋值
    var s student//声明结构体s
    s=student{2,"耿","男",18,"重庆"}//全部赋值
    fmt.Println(s)
//结构体部分赋值
    var q student//声明结构体q
    q.address ="上海"//部分赋值,给结构体q中的address赋值
    q.name="小宏"//部分赋值,给结构体q中的name赋值,未赋值部分默认为0
    fmt.Println(q)
//结构体成员使用:指针变量
    var p1 *student//定义了一个指针变量p1
    p1=&zhang//指针变量用来保存了zhnag的地址
//通过指针操作成员时  p1.id与(*p1).id完全等价,并且只能用"."运算符
    p1.id=5//改变原成员的值
    (*p1).name="mike"
    fmt.Println(*p1)
 //用new()函数申请一个新的结构体
    p2:=new(student)
    p2.name="傻逼"
    fmt.Println(p2.name)
}

二、进阶语法

1、语言可见性规则:

1.如果想使用别的包的函数、结构体类型、结构体成员,函数名、类型名、结构体变量名,首字母必须大写。如果首字母是小写,只能在同一个包里使用

2、驼峰命名法:

小驼峰命名法:第一个单词以小写字母开始,其他单词的首字母则需要大写

var sumNum int
var firstName string
var isValid bool
var printValue func()

大驼峰命名法:每个单词的首字母都采用大写

var SumNum int
var FirstName string
var IsValid bool
var PrintValue func()
3、面向对象:

image.png

4、方法:

image.png

type (接收者 类型)方法名(输入参数)返回参数{}
//类型1
package main
​
import "fmt"type long int//给类型int起一个别名,后面若要使用这个方法,类型必须与其相同,为long
func (a long)Add(b long)long{
​
    return a+b
}
​
func main(){
    var m long=10
    y:=m.Add(19)
    fmt.Println(y)
}
//类型2
package main
​
import "fmt"type person struct{
    name string
    sex string
    age int
}
func (p *person)change(name string,sex string,age int){
    p.name=name
    p.sex=sex
    p.age=age
}
func main(){
    p1:=person{"mike","男",18}
    fmt.Println(p1)
    var p2 person
    (&p2).change("yoyo","女",20)
    fmt.Println("p1=",p1)
    fmt.Println("p2=",p2)
}
5、方法值:
package main
​
import (
    "fmt"
)
​
type person struct{
    name string
    sex string
    age int
}
func (p *person)change(name string,sex string,age int){
    p.name=name
    p.sex=sex
    p.age=age
    fmt.Println(*p)
}
func (p person)value(){
    p.name="小宏"
    p.sex="男"
    p.age=18
    fmt.Println(p)
}
func main(){
    p1:=person{"mike","男",18}
    fmt.Println(p1)
    var p2 person
    //方法值
    pFunc1:=p1.change
    pFunc1("yyo","nv",17)
    pFunc2:=p2.value//这个就是方法值,调用函数时,无需再传递接收着,隐藏了接收者
    pFunc2()//等价于pp2.value()
    //方法表达式
    f1:=(*person).change
    f1(&p1,"kaka","男",17)//需要输入形参类型
    f2:=(person).value//不需要输入形参类型
    f2(p2)//显式把接受者传过去,等价于(*p2).value()/
}
6、接口:

接口定义:

type Humaner interface{
    SayHi()
}
//接口命令习惯以er结尾
//接口只有方法声明,没有实现,没有数据字段
//接口可以匿名嵌入其他接口,或嵌入到结构中

接口实现:

package main
​
import "fmt"//定义接口类型
type Humaner interface {
    //方法,只有函数声明,没有实现,由别的类型(自定义类型)实现
    sayhi()
}
type Student struct{
    name string
    id int
}
type  Teacher struct{
    name string
    age int
}
//student实现了此方法
func (temp *Student) sayhi(){
    fmt.Println(temp)
}
//teacher实现了此方法
func (temp Teacher) sayhi(){
    fmt.Println(temp)
}
func main(){
    //定义接口类型变量
    var i Humaner
    //只要实现了此接口方法的类型,那么这个类型的变量就可以给i赋值
    s:=&Student{"mike",666}
    i= s
    i.sayhi()//调用了Studebt类型
    t:=&Teacher{"jeke",45}
    i=t
    i.sayhi()//调用了Teacher类型
}
7、接口继承(匿名字段):
package main
​
import "fmt"type Student struct{
    Name string
    Age int
    Id int
}
type Human interface {//子集
    sayhi()
}
func (p Student)sayhi(){
    fmt.Println(p)
}
type Human2 interface {//超集 
    Human//匿名字段,继承了sayhi()
    Twice()
}
func (p Student)Twice(){
    fmt.Println(p,p)
}
func main(){
    var p Student
    p=Student{
        Name: "mike",
        Age:  19,
        Id:   666,
    }
    p.sayhi()//继承过来的方法
    p.Twice()
}
//超集可以转化为子集,反过来不可以
var i Human2//超集
var j Human//子集
j=i//可以实现
j=i//错误,不能实现
8、空接口:

image.png

tips:可以把 interface{} 作为一种类型声明变量,其声明的变量可以接收任意类型

9、类型断言:
package main
​
import "fmt"type Student struct{
    Name string
    Age int
}
​
func main(){
//if断言:
    i:=make([]interface{},3)
    i[0]=1                               //int
    i[1]="hello go"                      //string
    i[2]=Student{"小红",18}   //Student
    //第一个返回下标,第二个返回下标对应的值,date分别是i[0],i[1],i[2]
    for index,date:=range i {
        //第一个返回的是值,第二个返回判断结果的真假
        if value,ok:=date.(int);ok==true{        //date.(类型A)判断变量date是否为类型A,是:返回true;否:返回false
            fmt.Printf("i[%d]类型为int,内容为%d\n",index,value)
        }else if value,ok:=date.(string);ok==true{
            fmt.Printf("i[%d]类型为int,内容为%s\n",index,value)
        }else if value,ok:=date.(Student);ok==true{
            fmt.Printf("i[%d]类型为Student,内容为%v\n",index,value)
        }
    }
//switch断言:
    for index,date:=range i {
        switch value:=date.(type){
        case int :fmt.Printf("i[%d]类型为int,内容为%d\n",index,value)
        case string:fmt.Printf("i[%d]类型为int,内容为%s\n",index,value)
        case Student:fmt.Printf("i[%d]类型为Student,内容为%v\n",index,value)
        }
    }
/*运行结果:((两次)
i[0]类型为int,内容为1
i[1]类型为int,内容为hello go
i[2]类型为Student,内容为{小红 18}
​
进程 已完成,退出代码为 0*/
    
}
​
​

三、错误处理

1、error接口的使用:

1,fmt包使用:fmt.Errorf( )

格式化创建错误提示

package main
​
import "fmt"func main(){
    err1:=fmt.Errorf("%s","This is a normol error.")
    fmt.Println("err1=",err1)
}

2,errors包使用:errors.New( )

package main
​
import (
    "errors"
    "fmt"
)
​
func main(){
    err2:=errors.New("This is a normal eroor2.")
    fmt.Println("err2=",err2)
}

error( )源码:

image.png

image.png

系统的error包: image.png

error接口的应用:

package main
​
import (
    "errors"
    "fmt"
)
​
func divade(a,b int)(result int,err error){
    err=nil
    if b==0{
        err=errors.New("分母不能为零!")
    }else{
        result=a/b
    }
    return
}
func main(){
    result,err:=divade(10,0)
    if err!=nil{
        fmt.Println(err)
    }else{
        fmt.Println(result)
    }
}
2、panic函数:

//系统崩溃会自主调用该函数,也可以自己显示调用该函数

package main
​
import "fmt"func main(){
    fmt.Println("aaaaa")
    fmt.Println("bbbbb")
    panic("程序崩溃")//显示调用panic函数,导致程序崩溃
    fmt.Println("不会执行此语句")
}
3、recover函数:

(必须搭配defer来使用,可以跳过该错误继续执行)

package main
​
import "fmt"
​
func testa(){
    fmt.Println("aaaaaa")
}
func testb(x int){
    //设置recover
    defer func(){
        if err:=recover();err!=nil{
            fmt.Println(err)
        }
    }()//匿名函数并调用
    var a [10]int
    a[x]=11//当x>9时,数组越界,产生一个panic,程序崩溃
}
func testc(){
    fmt.Println("ccccc")
}
func main(){
    testa()
    testb(20)
    recover()
    testc()
}

四、字符串操作:

1、string包
package main
​
import (
    "fmt"
    "strings"
)
​
func main() {
    //Contains:检查字符串s中是否包含substr,返回bool值,包含,返回true;不包含返回false
    fmt.Println(strings.Contains("seafood", "foo")) //返回true
    fmt.Println(strings.Contains("seafood", "bar")) //返回false
    //Join:字符串链接,把slice a通过sep链接起来
    s := []string{"one", "two", "three"}
    fmt.Println(strings.Join(s, "。")) //输出:one。two。three
    //Index:在字符串s中查找substr所在的位置,返回位置值,找不到返回 -1
    fmt.Println(strings.Index("chicken", "ken")) //返回4:c是0,h是1,i是2,c是3,ken是4,所以返回4
    fmt.Println(strings.Index("chicken", "dmr")) //chicken中没有dmr所以返回-1
    //Repeat:重复s字符串count次,最后返回重复的字符串
    fmt.Println(strings.Repeat("ba", 3)) //重复了”ba“三次,并返回了重复后的结果:bababa
    //Replace:在s字符串中,把old字符串替换为new字符串,n表示替换的次数,小于0表示全部替换
    fmt.Println(strings.Replace("ba ba ba ba ba ba", "ba", "ma", 2))  //只替换了前面两个"ba"
    fmt.Println(strings.Replace("ba ba ba ba ba ba", "ba", "ma", -1)) //”ba“被全部替换了
    //Split:把s字符串按照sep分割,返回slice
    fmt.Printf("%q\n", strings.Split("a,b,cd", ",")) //以”,“为分割符分割为:["a" "b" "cd"]  储存到一个新数组中
    fmt.Printf("%q\n", strings.Split("a,b,cd", ""))//若sep为空,则以把每个单体分别分割开来:["a" "," "b" "," "c"                                                                                                         "d"]
    //Trim:在s字符串的头部和尾部去除cutset指定的字符串
    fmt.Printf("%q\n", strings.Trim("!!!mike!!", "!")) //不管!有多少个,直到前后删完为止。最后返回:"mike"
    //Fields:去除s字符串的空格符,并且按照空格分割返回slice
    fmt.Printf("%q\n", strings.Fields(" one two three ")) //返回:["one" "two" "three"]
}
2、strconv包(强转)

image.png

image.png

3、正则表达式:

不得而已,一般不用

//例:
package main
​
import (
    "fmt"
    "regexp"
)
​
func main() {
    buf:="abc agc a7c akc a8c a9c akc"
    //解释规则,他会解析正则表达式,如果成功返回解释器,如果解释失败返回nil
    reg1:=regexp.MustCompile(`a.c`)
    //根据规则提取关键信息
    result:=reg1.FindAllStringSubmatch(buf,-1)
    fmt.Println(result)
}

五、json操作

1、通过结构体生成json:

package main
​
import (
    "encoding/json"
    "fmt"
)
​
type IT struct {   //`json:"成员"`进行二次编码
    Company string `json:"-"`//忽略此成员,不输出company
    Subjects []string `json:"subjects"` //把首字母改为小写输出
    Isok bool
    Price float32 `json:",string"` //先转化为字符串再输出
}
func main() {
    s:=IT{
        Company:  "tset",
        Subjects: []string{"go语言","python","c","c++"},
        Isok:     true,
        Price:    66.66,
    }
    result,err:=json.Marshal(s)//编码,根据类容生成json文本
    result2,err2:=json.MarshalIndent(s,""," ")//格式化编码
    fmt.Println(string(result),err)
    fmt.Println(string(result2),err2)
}
​

2、json解码到结构体:

package main
​
import (
    "encoding/json"
    "fmt"
)
​
type IT struct {   //`json:"成员"`进行二次编码
    Company string `json:"company"`//忽略此成员,不输出company
    Subjects []string `json:"subjects"` //把首字母改为小写输出
    Isok bool  `json:"isok"`
    Price float32 `json:"price"` //先转化为字符串再输出
}
func main() {
    jsonbuf:=`{
        "company": "tset",
        "subjects": [
    "go语言",
    "python",
    "c",
    "c++"
    ],
    "isok": true,
    "price": 66.66
    }`
    var temp IT//定义一个结构体变量
    err:=json.Unmarshal([]byte(jsonbuf),&temp)//解码json内容,第二个参数要地址传递
    if err!=nil{
        fmt.Println("error")
        return
    }
    fmt.Println(temp)
}

3、通过map生成json:

package main
​
import (
    "encoding/json"
    "fmt"
)
​
func main() {
    m:=make(map[string]interface{},4)
    m["company"]="test"
    m["subjects"]=[]string{"go语言","python","c","c++"}
    m["isok"]=true
    m["price"]=66.66
    //编码成json
    result,err:=json.MarshalIndent(m,"","   ")
    if err!=nil{
        fmt.Println("error")
        return
    }
    fmt.Println(string(result))
}

4、json解析到map:

package main
​
import (
    "encoding/json"
    "fmt"
)
​
func main() {
    jsonbuf := `{
        "company": "tset",
        "subjects": [
    "go语言",
    "python",
    "c",
    "c++"
    ],
    "isok": true,
    "price": 66.66
    }`
    M := make(map[string]interface{}, 4)
    err := json.Unmarshal([]byte(jsonbuf), &M)
    if err != nil {
        fmt.Println("error")
        return
    }
    fmt.Printf("M = %+v\n", M) //详细打印
    //类型断言,先断言之后才能同类型赋值
    for key, value := range M {
        fmt.Printf("%v——————》%v\n", key, value)
        switch date := value.(type) {
        case string:
            fmt.Printf("map[%s]的值类型为string,type=%v\n", key, date)
        case bool:
            fmt.Printf("map[%s]的值类型为bool,type=%v\n", key, date)
        case float32:
            fmt.Printf("map[%s]的值类型为float32,type=%v\n", key, date)
        case []interface{}:
            fmt.Printf("map[%s]的值类型为[]interface{},type=%v\n", key, date)
        }
    }
}

六、文件操作:

1、建立与打开文件

image.png

2、写文件

image.png

package main
​
import (
    "fmt"
    "os"
)
​
func WriteFile(path string)  {
    f,err:=os.Create(path)//创建文件
    if err!=nil{
        fmt.Println("err=",err)
        return
    }
    defer f.Close()//函数结束前关闭文件
    var buf1,buf2 string
    for i:=0;i<10;i++{
        buf1=fmt.Sprintf("i=****%d\n",i)
        fmt.Println("buf=",buf1)
        f.WriteString(buf1)
​
    }
    totallevle := 30
    for i := 1; i <= totallevle; i++ {
        for k := 1; k <= totallevle-i; k++ {
            buf1=fmt.Sprintf(" ")//给buf1赋值
            f.WriteString(buf1)//在文件中写入括号中的内容
        }
        for j := 1; j <= 2*i-1; j++ {
            fmt.Print("*")
            buf2=fmt.Sprintf("*")
            f.WriteString(buf2)
        }
        f.WriteString("\n")
    }
}
func main() {
    //配置路径.当前路径下的test.txt文件
    path:="test.txt"
    WriteFile(path)
}
3、读文件

image.png

4、删除文件

image.png

总结:

学习不是一腔热血,而是长久坚持,善于利用工具,提高学习效率。

推荐学习方式:视频+文档+手撸代码

宝藏推荐:

www.bilibili.com/ 俗称哔哩哔哩大学,视频学习好帮手

www.runoob.com/ 菜鸟教程,新手的最爱,基础语法很nice

www.liwenzhou.com/ 李文周的博客,go语言学习的个人博客,个人比较喜欢

hao.studygolang.com/ go语言中文网站,有很多关于go语言的资源

studygolang.com/pkgdoc go语言包官方文档,中文的,很友好

pkg.go.dev/ go语言包官方文档,英文的,最原始的,更准确

#以上内容来自:推荐网站+手撸代码+个人理解

#如有不足,欢迎指正

#如有侵权,联系删除