Go 是一个开源的编程语言,它能让构造简单、可靠且高效的软件变得容易。
Go是从2007年末由Robert Griesemer, Rob Pike, Ken Thompson主持开发,后来还加入了Ian Lance Taylor, Russ Cox等人,并最终于2009年11月开源,在2012年早些时候发布了Go 1稳定版本。现在Go的开发已经是完全开放的,并且拥有一个活跃的社区。
Go 语言特色
- 简洁、快速、安全
- 并行、有趣、开源
- 内存管理、数组安全、编译迅速
Go 语言用途
Go 语言被设计成一门应用于搭载 Web 服务器,存储集群或类似用途的巨型中央服务器的系统编程语言。
对于高性能分布式系统领域而言,Go 语言无疑比大多数其它语言有着更高的开发效率。它提供了海量并行的支持,这对于游戏服务端的开发而言是再好不过了。
其他语言也可以有高并发,但是go语言的高并发是它天生的
学习前提
- 具有一种后端语言开发经验(c/c++/python/java/php等)
- 具备基本的网络编程能力和并发思想
- 熟悉Linux基本命令
- 了解计算机基本结构
引用说明
文章内容是我看b站视频https://www.bilibili.com/video/BV1gf4y1r79E?spm_id_from=333.1007.top_right_bar_window_default_collection.content.click作者刘丹冰Aceld,时做的笔记。
新的语言
如果你已经掌握了一门语言,那么一定体会到入门一门新的计算机语言无非就时学习它的条件,分支,循环,异常,还有就是这门语言它特有的写法。以下这个go语言的新奇语法

看到结构体,指针,main函数这些是不是感觉和c语言有些许相似,所以可以猜到它是一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。编译型就是在运行前先用编译器编译下,才能执行,像如python这种解释型语言可以直接运行,但是代价就是运行效率较低。
下载与安装
这种东西就不再赘述,自行百度,IDE的话可以选择vscode(免费),JetBrains的Goland(理论上收费)但是网上一大堆激活码,或者是学生的自己申请一个学校的学生邮箱激活就行,Linux的话就vim喽。
与其他语言对比

GO语言优劣
优势

大名鼎鼎的docker就是用go开发的。
劣势

Hello,GO!
hello.go
package main
//程序的包名和程序叫啥名字(hello.go)没关系。
import "fmt" //引入打印函数所在的包
/*多行注释是这样的,同时引用多个包这么写,末尾无逗号
import (
"fmt"
"time"
)
*/
func main() { //注意!这个大括号和函数名一定是同一行的,语法层面的规定否则报错
fmt.Println("Hello, GO!") //末尾可以加;但是最好别加
}
程序写完后,编译执行的命令:
- go run hello.go (编译+运行)
- go build hello.go (生成一个可执行文件)
变量声明
注意!go定义变量的''和""是不一样的定义变量只能用双引号,和python不同。
局部变量声明
- 方法一:默认值为0 var a int
- 方法二:自定义初始值 var b int =100
- 方法三:省略数据类型,自动匹配 var c =100
- 方法四:省略var关键字,用语法糖: (常用方法) d:=100
全局变量
方法四不支持
多变量
单行写法
var xx,xy=100,'haha'
多行写法
var(
w int =100
j bool =false
)
常量与枚举
go通过关键字const定义常量。常量顾名思义只读,不可修改。
const a int= 10
const (
a=10
b=20
)
在定义常量的时候有时候会有这种需求比如:希望a=10 b=20 c=30这种,每次都自己手写略显不优雅,可以通过关键字iota来帮助const定义枚举类型
第一行的iota的默认值为0,没增加一行就会累加一
const (
a= iota //iota =0
b //iota=1
c //iota=2
)
//修改表达式可以通过修改首行的表达式来实现各种枚举类型
const(
a= iota *10 //iota=0 0
b //iota=1 10
c //iota=2 c 20
)
const(
a,b=iota+1,iota +2 //iota=0 1,2
c,d //iota=1 2,3
)
函数
返回单个值
package main
import "fmt"
func f1(a string, b int) int {
fmt.Println(a)
fmt.Println(b)
c := 100
return c
}
func main() {
c := f1("aaa", 50)
fmt.Println(c)
}
返回多值匿名
func f2(a string, b int) (int, int) {
fmt.Println(a)
fmt.Println(b)
return 1, 2
}
返回多值带形参
func f3(a string, b int) (r1 int, r2 int) {
fmt.Println(a)
fmt.Println(b)
//给带有名称的返回值赋值,不赋值的话默认都为0,r1,r2属于函数的形参为局部变量
r1 = 100
r2 = 200
return
}
//或者
func f3(a string, b int) (r1 int, r2 int) {
fmt.Println(a)
fmt.Println(b)
//给带有名称的返回值赋值,不赋值的话默认都为0
return 100,200
}
//如果r1 r2类型一样可以这么写
func f3(a string, b int) (r1 , r2 int) {
'''
指针
假设我们想写个函数(changeval)来改变变量a的值
package main
import "fmt"
func changeval(p int) { //这里初始化形参p的时候会开辟一个新的空间,默认值为0,传递参数只是把a的值复制给了p
p = 10
}
func main() {
var a int = 1
changeval(a) //把a当作参数传入
fmt.Println(a) //最后p变成了10但是a依旧是那个少年
}

这时候就要想办法把真正的a传入了,想要传入真正的a那么底层就是传入a的地址就行,只需要设置一个索引让p指向a的地址。
package main
import "fmt"
func changeval(p *int) { //此处定义的p是一个指针类型,指向整型的指针类型
*p = 10 //*P表示把这个指针指向的地址赋值为10
}
func main() {
var a int = 1
changeval(&a) //&a表示把a的真正地址传入
fmt.Println(a)
}

练习-两个变量值互换
思路:实现两个变量a,b值互换只需要把b的地址赋值给a,a的地址赋值给b,当然在操作的时候当执行完第一步的时候,两个变量指向的地址就一样了。所以需要定义一个中间变量用来存放第一个变量的值
package main
import "fmt"
func swap(a *int, b *int) {
var middle int
middle = *a
*a = *b
*b = middle
}
func main() {
var a int = 10
var b int = 20
swap(&a, &b)
fmt.Println(a, b)
}
当然在python中只需
一行即可,但是实现的 原理还是一样的
defer语句
一个函数在执行最后,结束之前会执行的东西,就是在这个函数生命周期最后执行的东西,类似于django的信号,c++的signal函数,java的finally,通俗一点就是函数在自己快没的时候可以通过defer大叫一声告诉其他人。
从介绍来看,defer在return后执行,因为是生命周期的最后嘛,defer是以压栈的方式执行的,所以多个defer一起的 时候是先进后出的。

package main
import "fmt"
func rt() int {
fmt.Println("return")
return 0
}
func df() int {
defer fmt.Println("1")
defer fmt.Println("2")
return rt()
}
func main() {
df()
}
执行结果长这样

数组
固定长度数组
定义方式
- var myarry1 [10] int
- myarry2:=[10] int {1,2,3,4} //给了十个空间但只定义了前四个,后面的为默认值0
- myarry2:=[4] int {1,2,3,4}
package main
func printarry(arry [4]int) { //这里形参只能为[4]因为定义数组的时候长度固定为4了
//值拷贝
for index, value := range arry { \\可以通过range把这个数组的下标和值打印出来
print("index=", index, "value=", value, "\n")
}
}
//如果不用index的话这么写: for _, value := range arry {
//_表示一个匿名参数,意思是你可以不用但必须要有,因为给定的range返回值有两个
func main() {
myarry1 := [4]int{1, 2, 3, 4}
printarry(myarry1)
}
如果是定义的固定数组,实在这个printarry函数里修改不了值的,因为它是只拷贝,只是贴了个标签,没有把真正的地址指向数组。这时候就需要动态数组(slice)了,动态数组是引用传递。go中一般默认都是拷贝传递,只有slice.map,channel这些是天生的指针。
动态数组(slice)
定义
所谓动态那么就可以想到它的大小不是固定的了,那么顺理成章。
1.myarry :=[] int {1,2,3,4} //定义的时候[]里啥都不写不就好,传入了四个值所以分配了四个空间
2.var myarry1 [] int //声明是一个切片但是没有分配空间
myarry1= make([]int,3) //给myarry1分配了三个空间,初始值为0
3.var myarry2 [] int =make([]int,3) //声明一个切片同时分配空间
4.myarry3:=make([]int,3) //声明一个切片同时分配空间,通过:=推到出myarry3是切片
使用
func printarry(arry int) {
//引用传递
}
判断silce是否空切片
if myarry1==nil{ //这个nil约等于其他语言的null但是又有很多不同之处
…
}
else{
‘’'
}
silce追加与截取
追加
之前通过make([]int ,3)这这种方式定义的是指切片的长度,make还可以传一个参数指的是切片的容量。make([]int,4,5)这种意思就是它开辟了5个空间,但是里面只有4个元素

package main
import "fmt"
func main() {
var numbers = make([]int, 3, 5)
fmt.Println(len(numbers), cap(numbers)) //长度为3,容量为5
//追加元素
numbers = append(numbers, 2) //第一个参数是要追加的数组,第二个是值
fmt.Println(len(numbers), cap(numbers)) //长度为4,容量为5
numbers = append(numbers, 3, 4) //这里直接追加了两个元素长度为6,numbers的容量不够了,自动开辟原理的二倍的容量,也就是新容量变成10了
fmt.Println(len(numbers), cap(numbers), numbers) //长度为6,容量为10
}
让我们看一下结果

如果make的时候不传入容量的参数,那么默认和长度一样。
截取
截取很简单和其他语言一样,第一个元素索引为0.
package main
import "fmt"
func main() {
numbers := []int{1, 2, 3, 4}
fmt.Println(numbers, numbers[0:2]) //[1,2]
}
map
map用法基本上和silce一样,只不过以键值对存放。
定义
package main
import "fmt"
func main() {
mymap1 := make(map[int]string)
mymap1[1] = "go"
mymap1[2] = "java"
mymap2 := map[int]string{ //当然也可以在定义的时候直接写入一些键值对
1: "java", //末尾记得有逗号
}
fmt.Println(mymap1)
fmt.Println(mymap2)
}
使用
package main
import "fmt"
func main() {
mymap1 := make(map[int]string)
//添加
mymap1[1] = "go"
mymap1[2] = "java"
//修改
mymap1[2] = "python"
//删除
delete(mymap1, 2)
//遍历
for k, v := range mymap1 {
fmt.Println(k, v)
}
fmt.Println(mymap1)
}
!注意map是引用传递,不是拷贝传递,可以直接把map传给一个函数在里面修改即可
包管理
和其他语言的包管理一样,Go modules,如果不用就只能在go语言的环境变量设置的路径下开始写起包的位置,程序会找不到,很长很臭。而且没有版本控制的概念,每次包的版本时都要手撸一遍,很麻烦。这时候包管理工具就该体现作用了,首先要设置各种配置开启go modules。
输入go env查看当前配置,至于每行配置时什么意思看set后面的英文也可以了解绝大多数了,比如第一行,set GO111MODULE一看就是开启包管理的配置, 怎么进行一些其他个性化设置自行百度

换源等等几乎每个语言都有这种工具,很简单至于怎么开启包管理,这个是它的换源方法,用的七牛云的镜像
go env -w GOPROXY=https://goproxy.cn
在当前工程环境下使用 go mod init xxx(导入时起的名字)可以初始化一个配置文件:go.mod,命令成功后内部应该出现
module xxx //这个就是你起的名字
go 1.18 //这是go的版本号
导入第三方包
就是从网上下载的包,比如:"github.com/aceld/zinx/znet"
import (
"fmt"
"github.com/aceld/zinx/znet"
)
这时候执行go get github.com/aceld/zinx/znet
就可以从网上下载下来,再看go.mod你会发现多了一行新的东西
require github.com/aceld/zinx v1.0.1 // indirect
indirect表示间接依赖的意思
与此同时,还多了一个叫go.sum的文件内部长这样
github.com/aceld/zinx v1.0.1 h1:WnahGyE7tDJvkJRVK2VI/m57aHEeUjr12EAYpOYW3ng=
github.com/aceld/zinx v1.0.1/go.mod h1:Tth0Fmjjpel0G8YjCz0jHdL0sXlc4p3Km/l/srvqqKo=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
就是对远程的包和go.mod本身进行md5校验看看有没有被中间人篡改过保证安全性和完整性。
原理就是:可以自己保存一个文本文件,然后用程序算出它的md5值,然后再改变文本文件的内容,再计算一遍md5值,这时候会发现前后md5值不同了,那么就说明文件被修改了
修改依赖版本
如果已经第三方库的作者把版本更新了,直接go mod get的就是最新版本,go.mod里:require github.com/aceld/zinx v1.0.1 // indirect
可以通过手动把句话的v1.0.1修改成想要的版本,但是不够优雅,因为我们是发生了替换的,这样直接修改日后不太好明白从哪个版本替换成哪个版本的。
要想优雅的实现修改依赖版本,只需要输入
go mod edit -replace v1.0.1=v1.0.2 //(旧版本=新版本)
执行后就会在go.mod文件夹里多处一行结果,最终的go.mod长这样
module tcp_server
go 1.18
require github.com/aceld/zinx v1.0.1 // indirect
replace zinx v1.0.1=>v1.0.2
导入本地包
如果不适用包管理的话,本地路径导包要写绝对路径,或者是go的环境变量那里写开始,或者是放在go语言源码那里,比如导入fmt就i直接写就行,很麻烦。
比如当前的项目目录是这样的
│ go.mod
│ go.sum
│ lib2file.go
│ main.go
│
└─importlib1
lib1file.go
lib1_2.go
要想导入importlib1,使用go mod 只需要在main里面输入
import (
"fmt"
"tcp_server/importlib1" //tcp_server 这个是在只需go mod init xxx 的这个xxx,也可以打开go.mod文件手动修改第一行:module tcp_server
//importlib1这个是你向导入包的所在文件夹名字,
)
这里导入的是importlib1这个文件夹,里面的两个文件的第一行必须一样均为package xxx

main文件里面这么调用
lib1.Libtest()
lib1.Lib1_2test()
执行结果:

说明了首先是两个int说明了,go在导包的时候初始化包是深度优先的,先把所有要导的包都初始化了再执行其他代码。
相互导入
就是再相同路径下互相导入,比如在main.go中导入lib2file.go。
只需要lib2file.go遵守上面说的规则在同一个文件夹里package xxx必须相同,所以lib2file.go的代码可以写成这样
package main
import "fmt"
func init() {
fmt.Println("lib2 int")
}
func Lib2test() {
fmt.Println("lib2 test")
}
想在main.go中使用Libe2test只需要直接执行,无需import 导入
func main() {
lib1.Libtest()
lib1.Lib1_2test()
Lib2test() //同一路径下的包直接执行,无需导入
}
在编译运行的时候,把两个包同时编译才行,否则只编译一个包那么自然就找不到另一个了
命令行输入go run main.go libe2file.go
成功后就会执行
go的基础语法就介绍到这里了。