Golang基本语法-掘金课程笔记

简介: Golang基本语法-掘金课程笔记

数据类型

整型取值范围

01ebd755782e4c909dad0843d3544acf.jpeg

var n int8 n=100 fmt.Println(n) //100 没有问题 //如果赋值为200 则不行 因为int8取值范围最大是127


字符串


v1 := 'A' v2 := "A"


//单引号存储的是 ASCII编码 //A的ASCII值=65 //B的ASCII值B=66 //a的ASCII值a=97


fmt.Printf("v1的类型是%T,%d,值为%s\n", v1, v1) // int32 A 65

fmt.Printf("v2的类型是%T,%s\n", v2, v2) //string A


//字符串的长度 //中文占3个字节


fmt.Println(len(v2)) //1


//获取某个字节


s2 := `hello go`


fmt.Println(s2[0]) //104


占位符号

01ebd755782e4c909dad0843d3544acf.jpeg


字符转义


双引号单引号如果作为字符串输出,需要使用\"进行转义后才能使用 \n \r \t 为特殊作用字符。


//字符转义空格


fmt.Println("hello \t golang")//hello golang


//输出换行  \n


//回车符号 \r


//输出路径


fmt.Println("E:\\golang\\go.jpg")//E:\golang\go.jpg


//双引号


fmt.Println("hello \"golang\"") //hello "golang"


布尔


//布尔类型作为条件比较返回结果只有true和false两种


a:=10


fmt.Println(a==2) //false


fmt.Println(a!=2) //true


浮点型


Go语言有两种精度的浮点数 float32 和 float64。浮点类型的数据取值范围可以从很小或者很巨大.


1.79E-308 是 1.79 乘以 10的负308次方。 1.79E+308 是 1.79 乘以 10的308次方。

01ebd755782e4c909dad0843d3544acf.jpeg

单精度双精度两者区别

在内存中占有的字节数不同
  • 单精度浮点数在机内占4个字节。
  • 双精度浮点数在机内占8个字节。
有效数字位数不同
  • 单精度浮点数 有效数字7位。
  • 双精度浮点数 有效数字16位。
使用情况区别
  • 一般用来表示美元和分的时候用单精度类型。
  • 超出人类经验的数字函数,例如 sin() cos() tan() sqrt() 都使用双精度值。

数据类型的转换

var a int8 = 10 var b int16 //b=a  因为类型不同不能直接转换 
 b= int16(a) fmt.Println(a,b)//10,10f1:=3.12 var c int
c = int(f1) fmt.Println(f1,c)// 3.12 , 3  浮点类型转为整形的时候 只是取了整数部分
 //不是所有类型都能互相转换 //数值类型的可以转换   int 和bool 不能转换

程序控制结构

数组for 循环语句

arr := [5]int{1,2,3,4,5} 
//for循环 
for i:=0; i<len(arr);i++{ 
        fmt.Println(arr[i])
 }

for循环中的break 和continue

break 用来终止执行for循环语句,终止整个循环。continue 终止当前循环的迭代,重新进入下一条件,进入循环。+

goto语句

//语法

lable:func1

...

goto label

冒泡排序

package main
import "fmt"
func main() {
    values := []int{4, 3, 14, 85, 34, 27, 91, 95, 26,12,32}
    fmt.Println(values)
    BubblingASC(values)//正序冒泡
    BubblingDESC(values)//倒序冒泡
}
//冒泡排序 正序,大的靠后 小的靠前。
func BubblingASC(values []int) {
    for i := 0; i < len(values)-1; i++ {
        for j := i+1; j < len(values); j++ {
            if  values[i]>values[j]{ //左右两边数据对比 
                values[i],values[j] = values[j],values[i] //数据交换
            }
        }
    }
    fmt.Println(values)
}
//冒泡排序 倒序, 大的靠前 小的靠后。
func BubblingDESC(values []int) {
    for i := 0; i < len(values)-1; i++ {
        for j := i+1; j < len(values); j++ {
            if  values[i]<values[j]{ //左右两边数据对比 
                values[i],values[j] = values[j],values[i] //数据交换
            }
        }
    }
    fmt.Println(values)
}

return 返回

package main
import "fmt"
func main() {
  a, b, c := myfunc()
  fmt.Printf("a = %d, b = %d, c = %d\n", a, b, c)
}
func myfunc() (a, b, c int) {
  a, b, c = 111, 222, 333
  return
}

切片Slice

切片也是一种存储相同类型的数据结构,但是不同于数组的是它的大小可以改变,如果长度不够可以自动扩充。

image.png

//声明一个切片slice
var slice []int
//使用make函数创建切片
s1:=make([]int,0,5)
fmt.Println(s1)// [] 打印空的切片
s1=append(s1,1,2)
fmt.Println(s1)// [1,2]
//因为切片可以扩容  所以定义容量为5 但是可以加无数个数值
s1=append(s1,3,4,5,6,7)
fmt.Println(s1)// [1,2,3,4,5,6,7] 
//添加一组切片到另一切片中
s2:=make([]int,0,3)
s2=append(s2,s1...) //...表示将另一个切片数组完整加入到当前切片中

make()与new() 的区别

make()是Go语言中的内置函数,主要用于创建并初始化slice切片类型,或者map字典类型,或者channel通道类型数据。他与new方法的区别是。new用于各种数据类型的内存分配,在Go语言中认为他返回的是一个指针。指向的是一个某种类型的零值。make 返回的是一个有着初始值的非零值。

//测试使用new方法新建切片
slice1 := new([]int)
fmt.Println(slice1) //输出的是一个地址  &[]
//使用make创建切片
slice2 := make([]int, 5)
fmt.Println(slice2)//输出初始值都为0的数组, [0 0 0 0 0]
fmt.Println(slice1[0])//结果出错 slice1是一个空指针 invalid operation: slice1[0] (type *[]int does not support indexing)
fmt.Println(slice2[0])//结果为 0 因为已经初始化了

切片是如何扩容的

package main
import (
    "fmt"
)
func main() {
    s1 := make([]int, 0, 3)
    fmt.Printf("地址%p,长度%d,容量%d\n", s1, len(s1), cap(s1))
    s1 = append(s1, 1, 2)
    fmt.Printf("地址%p,长度%d,容量%d\n", s1, len(s1), cap(s1))
    s1 = append(s1, 3, 4, 5)
    fmt.Printf("地址%p,长度%d,容量%d\n", s1, len(s1), cap(s1))
    //地址0xc000010540,长度0,容量3
    //地址0xc000010540,长度2,容量3
    //地址0xc00000e4b0,长度5,容量6
    //如果添加的数据容量够用, 地址则不变。
    //如果实现了扩容, 地址就会发生改变成新的地址,旧的则自动销毁。
}
  • 每一个切片都引用了一个底层数组。
  • 切片本身不能存储任何数据,都是这底层数组存储数据,所以修改切片的时候修改的是底层数组中的数据。
  • 当切片添加数据时候,如果没有超过容量,直接进行添加,如果超出容量自动扩容成倍增长。
  • 切片一旦扩容,指向一个新的底层数组内存地址也就随之改变。

值传递与引用传递

  • 基本类型:int、float、string、bool
  • 复合类型:array、slice、map、struct、pointer、function、chan
  • 值类型:int、float、string、bool、array、struct
  • 引用类型: slice、pointer、map、chan 等都是引用类型。

引用传递因为存储的是内存地址,所以传递的时候则传递是内存地址,所以会出现多个变量引用同一个内存。

//数组为值传递类型
//定义一个数组 arr1
arr1 := [4]int{1, 2, 3, 4}
arr2 := arr1            //将arr1的值赋给arr2
fmt.Println(arr1, arr2) //[1 2 3 4] [1 2 3 4]  输出结果 arr1与arr2相同,
arr1[2] = 200           //修改arr1中下标为2的值
fmt.Println(arr1, arr2) //[1 2 200 4] [1 2 3 4] 结果arr1中结果改变,arr2中不影响
//说明只是将arr1中的值给了arr2 修改arr1中的值后并不影响arr2的值
//切片是引用类型
//定义一个切片 slice1
slice1 := []int{1, 2, 3, 4}
slice2 := slice1            //将slice1的地址引用到slice2
fmt.Println(slice2, slice2) //[1 2 3 4] [1 2 3 4]   slice1输出结果 slice2输出指向slice1的结果,
slice1[2] = 200             //修改slice1中下标为2的值
fmt.Println(slice1, slice2) //[1 2 200 4] [1 2 200 4] 结果slice1中结果改变,因为修改的是同一份数据
//说明只是将slice1中的值给了slice2 修改slice1中的值后引用地址用的是同一份 slice1 和slice2 同时修改
fmt.Printf("%p,%p\n", slice1, slice2)//0xc000012520,0xc000012520
//切片引用的底层数组是同一个 所以值为一个地址 是引用的底层数组的地址
fmt.Printf("%p,%p\n", &slice1, &slice2)//0xc0000044a0,0xc0000044c0
//切片本身的地址

深拷贝和浅拷贝

深拷贝是指将值类型的数据进行拷贝的时候,拷贝的是数值本身,所以值类型的数据默认都是深拷贝。浅拷贝指的是拷贝的引用地址,修改拷贝过后的数据,原有的数据也被修改。 那么如何做到引用类型的深拷贝?也就是需要将引用类型的值进行拷贝。修改拷贝的值不会对原有的值造成影响。

1,使用range循环获取元素中的值 进行拷贝

//使用range循环将切片slice中的元素一个一个拷贝到切片s2中
slice := []int{1, 2, 3, 4}
s2 := make([]int, 0)
for _, v := range slice {
    s2 = append(s2, v)
}
fmt.Println(slice)  //结果 [1 2 3 4]
fmt.Println(s2)     //结果 [1 2 3 4]

2,使用深拷贝数据函数: copy(目标切片,数据源)

//copy(目标切片,数据源)  深拷贝数据函数
s2 := []int{1, 2, 3, 4}
s3 := []int{7, 8, 9}
copy(s2, s3)        //将s3拷贝到s2中    
fmt.Println(s2)     //结果 [7 8 9 4]
fmt.Println(s3)     //结果 [7 8 9]
copy(s3, s2[2:])    //将s2中下标为2的位置 到结束的值 拷贝到s3中 
fmt.Println(s2)     //结果 [1 2 3 4]
fmt.Println(s3)     //结果 [3 4 9]
copy(s3, s2)        //将s2拷贝到s3中
fmt.Println(s2)     //结果 [1 2 3 4]
fmt.Println(s3)     //结果 [1 2 3]

切片的删除

删除切片中元素的方法

//方法一 获取切片指定位置的值 重新赋值给当前切片
slice:=[]int{1,2,3,4}
slice=slice[1:]//删除切片中开头1个元素  结果 [2,3,4]
//方法二 使用append不会改变当前切片的内存地址
slice = append(slice[:0], slice[1:]...) // 删除开头1个元素
fmt.Println(slice)

删除指定的下标元素

slice:=[]int{1,2,3,4}   
i := 2      // 要删除的下标为2
slice = append(slice[:i], slice[i+1:]...) // 删除中间1个元素
fmt.Println(slice)  //结果[1 2 4]

删除切片结尾的方法

slice := []int{1, 2, 3, 4}
slice = slice[:len(slice)-2] // 删除最后2个元素
fmt.Println(slice)           //结果 [1,2]

复合数据 map 键值对 key:value 类型的数据

image.png

var m1 map[int]string         //只是声明 nil
var m2 = make(map[int]string) //创建
m3 := map[string]int{"语文": 89, "数学": 23, "英语": 90}
fmt.Println(m1 == nil) //true
fmt.Println(m2 == nil) //false
fmt.Println(m3 == nil) //false
//map 为nil的时候不能使用 所以使用之前先判断是否为nil
if m1 == nil {
    m1 = make(map[int]string)
}
//1存储键值对到map中  语法:map[key]=value
m1[1]="小猪"
m1[2]="小猫"
//2获取map中的键值对  语法:map[key]
val := m1[2]
fmt.Println(val)
//3判断key是否存在   语法:value,ok:=map[key]
val, ok := m1[1]
fmt.Println(val, ok) //结果返回两个值,一个是当前获取的key对应的val值。二是当前值否存在,会返回一个true或false。
//4修改map  如果不存在则添加, 如果存在直接修改原有数据。
m1[1] = "小狗"
//5删除map中key对应的键值对数据 语法: delete(map, key)
delete(m1, 1)
//6 获取map中的总长度 len(map)
fmt.Println(len(m1))
//map的遍历
因为map是无序的 如果需要获取map中所有的键值对
可以使用 for range
map1 := make(map[int]string)
map1[1] = "张无忌"
map1[2] = "张三丰"
map1[3] = "常遇春"
map1[4] = "胡青牛"
//遍历map
for key, val := range map1 {
    fmt.Println(key, val)
}

map结合Slice

//创建一个map存储第一个人的信息
    map1 := make(map[string]string)
    map1["name"] = "张无忌"
    map1["sex"] = "男"
    map1["age"] = "21"
    map1["address"] = "明教"
    //如果需要存储第二个人的信息则需要重新创建map
    map2 := make(map[string]string)
    map2["name"] = "周芷若"
    map2["sex"] = "女"
    map2["age"] = "22"
    map2["address"] = "峨眉山"
    //将map存入切片 slice中
    s1 := make([]map[string]string, 0, 2)
    s1 = append(s1, map1)
    s1 = append(s1, map2)
    //遍历map
    for key, val := range s1 {
        fmt.Println(key, val)
    }

sync.map的使用

package main
import (
    "fmt"
    "sync"
)
//声明sync.Map
var syncmap sync.Map
func main() {
    //Store方法将键值对保存到sync.Map
    syncmap.Store("zhangsan", 97)
    syncmap.Store("lisi", 100)
    syncmap.Store("wangmazi", 200)
    // Load方法获取sync.Map 键所对应的值
    fmt.Println(syncmap.Load("lisi"))
    // Delete方法键删除对应的键值对
    syncmap.Delete("lisi")
    // Range遍历所有sync.Map中的键值对
    syncmap.Range(func(k, v interface{}) bool {
        fmt.Println(k, v)
        return true
    })
}

包管理和常用包介绍

包的概念就是我们程序中的目录,我们所写的所有代码都放在包中在定义的时候用package定义包, 然后使用 import 引入包。Go语言提供了很多内置包,例如:fmt、strings、strconv、os、io 等等。

strings包

//是否包含指定的字符串中任意一个字符 有一个出现过 就返回true
fmt.Println(strings.ContainsAny(s1,"glass"))
//返回指定字符出现的次数 
fmt.Println(strings.Count(s1,"g"))
//文本的开头
fmt.Println(strings.HasPrefix(s1,"ok"))
//文本的结尾
fmt.Println(strings.HasSuffix(s1,".txt"))
//查找指定字符在字符串中存在的位置 如果不存在返回-1
fmt.Println(strings.Index(s1,"g"))
//查找字符中任意一个字符出现在字符串中的位置
fmt.Println(strings.IndexAny(s1,"s"))
//查找指定字符出现在字符串中最后一个的位置
fmt.Println(strings.LastIndex(s1,"s"))
//字符串的拼接
s2:=[]string{"123n","abc","ss"}
s3:=strings.Join(s2,"_")
fmt.Println(s3)// 123n_abc_ss
//字符串的切割
s4:=strings.Split(s3,"_")
fmt.Println(s4)// 返回切片[]string{"123n","abc","ss"}
//字符串的替换
s5 := "okoletsgo"
s6 := strings.Replace(s5, "o", "*", 1)
fmt.Println(s6)//*koletsgo
//TODO 1 只替换1次,  -1 全部替换
//字符串的截取
//str[start:end]包含start 不包含end

strconv包

主要用于字符串和基本类型的数据类型的转换

/str:="aa"+100
//字符串和整形数据不能放在一起  所以需要将100 整形转为字符串类型 
//+号在字符串中表示字符串的连接 在整形中表示数据的计算
//string 转 bool类型
s1 := "true" //字符串
b, err := strconv.ParseBool(s1)
if err != nil {
    fmt.Println(err) //打印错误信息
}
fmt.Printf("%T,%t", b, b) //bool,true
//string 转int
s1 := "100" //字符串
b, err := strconv.ParseInt(s1, 10, 64)
//10 表示s1要转的数据是10进制 64位
if err != nil {
    fmt.Println(err) //打印错误信息
}
fmt.Printf("%T,%d", b, b) //int64,100
//整形转为字符串
s := strconv.Itoa(23)
//字符串转为整形
i, e := strconv.Atoi(s)

time包

time包操作的都是时间,时间的单位都包括年,月,日,时,分,秒,毫秒,微妙,纳秒,皮秒。

package main
import (
    "fmt"
    "time"
)
func main() {
    //获取当前时间
    t := time.Now()
    fmt.Println(t) //2020-03-31 21:26:01.7307507 +0800 CST m=+0.001999001
    //获取的时间后面的信息是时区
    //上面的时间看起来不是很方便 于是需要格式化时间
    s := t.Format("2006年1月2日 15:04:05")
    fmt.Println(s)
}

需要注意的是Go语言中时间的格式化,需要指定格式化时间的模板, 不管年月日的类型格式怎么写,但是具体的数值必须写成2006-01-02 15:04:05, 如果不是这个日期就不能够格式化,这个时间也是为了纪念Go语言诞生的时间。

s := t.Format("2006-1-2 15:04:05")
fmt.Println(s) //打印出的格式就是当前的时间 2020-3-31 23:08:35
s := t.Format("2006/1/2")
fmt.Println(s) //打印出的格式就是当前的年月日 2020/3/31
//字符串类型的时间
str := "2020年3月31日"
//第一个参数是模板,第二个是要转换的时间字符串
s, _ := time.Parse("2006年1月2日", str)
fmt.Println(s) //打印出的格式就是2020-03-31 00:00:00 +0000 UTC
//获取年月日信息
year, month, day := time.Now().Date()
fmt.Println(year, month, day) //2020 March 31
//获取时分秒信息
hour, minute, second := time.Now().Clock()
fmt.Println(hour, minute, second) //23 23 54
//获取今年过了多少天了
tday := time.Now().YearDay()
fmt.Println(tday) //91  (今年已经过了91天了)
//获取今天是星期几
weekday := time.Now().Weekday()
fmt.Println(weekday) //Tuesday

时间戳

时间戳是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总毫秒数。它主要是为用户提供一份电子证据.

package main
import (
    "fmt"
    "time"
)
func main() {
    //获取指定日期的时间戳
    t := time.Date(2020, 3, 31, 23, 30, 0, 0, time.UTC)
    timestamp := t.Unix()
    fmt.Println(timestamp) //1585697400
    //获取当前时间的时间戳
    timestamp2 := time.Now().Unix()
    fmt.Println(timestamp2) //1585669151
    //当前时间的以纳秒为单位的时间戳
    timestamp3 := time.Now().UnixNano()
    fmt.Println(timestamp3) //1585669151296330900
}
//时间间隔 相加
now := time.Now()
//当前时间加上一分钟
t := now.Add(time.Minute)
fmt.Println(now) //2020-03-31 23:43:35.0004791 +0800 CST m=+0.002999201
fmt.Println(t)   //2020-03-31 23:44:35.0004791 +0800 CST m=+60.002999201
//计算两个时间的间隔
d := t.Sub(now)
fmt.Println(d) //1m0s  相差一分钟

时间戳与时间格式互转

//将指定时间转为时间戳格式
beforetime := "2020-04-08 00:00:00"                             //待转化为时间戳的字符串
timeLayout := "2006-01-02 15:04:05"                             //转化所需模板
loc := time.Now().Location()                                    //获取时区
theTime, _ := time.ParseInLocation(timeLayout, beforetime, loc) //使用模板在对应时区转化为time.time类型
aftertime := theTime.Unix()                                     //转化为时间戳 类型是int64
fmt.Println(theTime)                                            //打印输出theTime 2020-04-08 00:00:00 +0800 CST
fmt.Println(aftertime)                                          //打印输出时间戳 1586275200
//再将时间戳转换为日期
dataTimeStr := time.Unix(aftertime, 0).Format(timeLayout) //设置时间戳 使用模板格式化为日期字符串
fmt.Println(dataTimeStr)

dep管理方案

但是早期的Go语言被很多开发者所诟病的一个问题是依赖包的管理,在Golang1.5版本之前需要设置GOPATH来解决所有包依赖的问题,但是这样会有很多问题,如果我们两个项目引用的包版本不一致,而GOPATH中只有一个版本,就需要使用多个GOPATH来解决这样的问题,这样来回切换GOPATH是很不方便的。于是Go在1.9之后加入包管理方案解除了GOPATH的依赖。于是出现了depglide在项目中加入了vender目录来存储所有项目中需要引入的包。

安装dep

MacOS brew install dep

Linux curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh

Windows go get -u github.com/golang/dep/cmd/dep

![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/64adf95bb54a40d89466c37751be51a2~tplv-k3u1fbpfcp-zoom-1.image)  

使用 dep status 查看项目依赖的详细信息和状态。

dep ensure 查看所有的依赖库都已经安装,如果没有就去下载。 dep ensure add github.com/go-sql-driver/mysql 下载添加新的依赖库,并增量更新清单文件和校验描述文件。 dep不是每次都去下载,他会先在本地环境中找如果没有找到则会到网上下载并添加到本地仓库。

mod初始化

使用mod需要注意的是:

  • 如果Go的版本太低不能使用,建议将Go的版本升级到最新。
  • 环境变量中可以增加GOPROXY=https://goproxy.io 这样没有梯子的情况下可以正确的加载相应的包文件。
  • 环境变量GO111MODULE不要设置,如果已经增加了这个变量请务必设置为GO111MODULE=auto
  • 在项目的根目录下使用命令go mod init projectName

执行上面的命令之后,就已经可以开发编译运行此项目了。他已经自动引用了项目中所有的包文件。

函数

image.png

1,大写首字母函数共用,小写私用

匿名函数

定义一个匿名函数直接加上()就相当于直接调用了,通常只能调用一次,可以将匿名函数赋值给一个变量,这个变量就代表了这个函数。则可以调用多次。

  • 匿名函数可以作为另一个函数的参数
  • 匿名函数可以作为另一个函数的返回值
    package main import ( "fmt" ) func main() { res2 := oper(20, 12, add) fmt.Println(res2)
//匿名函数作为回调函数直接写入参数中
res3 := oper(2, 4, func(a, b int) int {
    return a + b
})
fmt.Println(res3) 
  • } func add(a, b int) int { return a + b } func reduce(a, b int) int { return a - b } //oper就叫做高阶函数 //fun 函数作为参数传递则fun在这里叫做回调函数 func oper(a, b int, fun func(int, int) int) int { fmt.Println(a, b, fun) //20 12 0x49a810A   第三个打印的是传入的函数体内存地址 res := fun(a, b)//fun 在这里作为回调函数 程序执行到此之后才完成调用 return res }


目录
相关文章
|
2天前
|
Go
4.1 golang基础语法
4.1 golang基础语法
30 0
|
2天前
|
SQL 前端开发 Go
编程笔记 GOLANG基础 001 为什么要学习Go语言
编程笔记 GOLANG基础 001 为什么要学习Go语言
|
2天前
|
Go
Golang深入浅出之-Go语言基础语法:变量声明与赋值
【4月更文挑战第20天】本文介绍了Go语言中变量声明与赋值的基础知识,包括使用`var`关键字和简短声明`:=`的方式,以及多变量声明与赋值。强调了变量作用域、遮蔽、初始化与零值的重要性,并提醒读者注意类型推断时的一致性。了解这些概念有助于避免常见错误,提高编程技能和面试表现。
27 0
|
2天前
|
存储 IDE 编译器
编程笔记 GOLANG基础 005 第一个程序:hello world 使用vscode
编程笔记 GOLANG基础 005 第一个程序:hello world 使用vscode
|
2天前
|
Go 开发工具
编程笔记 GOLANG基础 004 GOLANG常用命令及VSCODE快捷键
编程笔记 GOLANG基础 004 GOLANG常用命令及VSCODE快捷键
|
2天前
|
Go 开发工具 git
编程笔记 GOLANG基础 003 Go语言开发环境搭建
编程笔记 GOLANG基础 003 Go语言开发环境搭建
|
2天前
|
存储 Java Go
编程笔记 GOLANG基础 002 Go语言简介
编程笔记 GOLANG基础 002 Go语言简介
|
2天前
|
JSON 安全 Java
golang面试:golang基础语法(一)
golang面试:golang基础语法(一)
43 0
|
6月前
|
存储 人工智能 Java
深入了解Golang:基本语法与核心特性解析
深入了解Golang:基本语法与核心特性解析
|
7月前
|
安全 IDE Linux
【Golang】一文学完 Golang 基本语法
【Golang】一文学完 Golang 基本语法
31 0