Go语言上手-基础语言 | 青训营笔记

193 阅读25分钟

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

Go基础语法-Hello World

Go云平台开发环境:gitpod.io/#github.com…

安装完go后可以测试:

image.png package main 表示这个文件属于main包的一部分,main包是程序的入口包,也就是这个文件是程序的入口文件 import { "fmt" }输入输出格式化 image.png 第七行开始是main的函数,调用fmt.Println输出 如果直接运行输入go run 文件相对路径与文件名.go 如果编译成二进制用go build 文件相对路径与文件名.go 编译完成后用./ 文件名 运行

Go 变量

Go中字符串是内置类型,可以用+连接,可以用= 变量声明:var name = value 自动检测变量类型 如果创建特定类型: var b,c int = 1,2 另一种声明类型: f := float32(e) 常量把var改成const即可,golang的常量也没有特定类型

Go - if else 循环等

if后没有括号 if 7%2==0 { fmt.Println("7 is even") } for { }代表死循环 for j := 7; j<9; j++{ fmt.Println(j) } 可以用continue和break for i<=3 { fmt.Println(i) i = i+1 } switch不需要加break image.png switch可以不加变量,直接在case里判断条件

GO 数组

image.png 很少用数组,因为它的长度是固定的

GO 切片

切片不同于数组,它是可变长度的 image.png 可以任意时刻更改长度,也有更多功能。 make创建切片,可以指定长度; append追加切片元素,注意必须赋值给原数组,因为golang里slice的原理实际上是存储了长度+容量+一个指向数组的指针,如果容量不勾会发生扩容并返回一个新的slice,所以必须赋值回去。 可以用copy去拷贝数据到另一个切片。 slice切片也是左闭右开,注意不支持负数索引,需要用len(s)取出slice s的长度然后再做运算

GO map

在其他编程语言里可能叫做Hash或字典,map是实际使用过程中使用最频繁的数据结构,可以用make创建一个空map image.png 可以map名[key名] = value 写入 也可以通过方括号读取; 通过delete删除键值对 在从map里读取配位对时可以加一个,ok 用来获取这个map里到底有没有这个key存在。 golang里的map是完全无序的,遍历时不会按字母顺序也不会按插入顺序输出。

GO range

image.png 对于数组会返回两个值,索引-值。 遍历map,第一个值是key,第二个是value。

GO 函数

image.png 注意变量类型是后置的。业务逻辑里几乎所有函数都返回多个值,第一个值是真正的返回值,第二个值错误信息(13-16行),第二个值是表示是否存在。

GO 指针

image.png 指针的主要用途就是对常用的参数进行修改。参数要加*,注意调用时参数要加&。

GO 结构体

结构体是带类型的字段的集合

image.png 可以用结构体名称初始化一个结构体变量,如果写出了字段名也可以只指定一部分的值。如果没有初始化的字段会自动为空值。 用.读取或写入结构体的内容。 结构体也能作为函数的参数,同样有非指针和指针两种用法。指针用法可以实现修改和减少拷贝开销。

GO 结构体方法

image.png user挪到前面,函数从普通函数变成了类成员函数。带指针可以对结构体进行修改

GO 错误处理

image.png 不同于JAVA里的异常,GO里能很清晰地知道哪个函数返回了错误,在参数里加err error,返回nil, errors.New("错误内容"),如果没有错误就返回原本的返回值,nil 调用时注意也要写两个变量。调用完判断error是否存在,如果存在需要做一些处理(打印值退出或返回函数),只有没有error时会取真正的返回值

GO 字符串操作

image.png strings包里有很多字符串辅助函数 strings.Contains 判断字符串里是否包含另一个字符串 Count 字符串计数 Index 查找字符串的位置 Join连接多个字符串 Repeat重复多个字符串 len(s)获取字符串s的长度 注意一个中文对应多个字母

GO 字符串格式化

image.png format包里有非常多的字符串格式化方法 fmt.Println(a,b)打印多个变量并且换行 fmt.Printf() 类似C的printf,变量都用%v就可以,可以用%+v来得到详细的结构({字段名字:值}),%#v进一步详细得到其结构(构造的类型名称{字段:值})

GO JSON处理

image.png 结构体只要保证每个字段的第一个字母是大写,那么这个结构体a就能用json.Marshal(a)去序列化,序列化之后会变成一个byte数组 可以简单理解为字符串,打印时要用string进行类型转换才能打印出字符串,否则会打印出16进制的编码。 序列化后的字符串也可以简单地用Unmarshal()反序列化到一个空结构体变量里。 正常情况下序列化的字符串是大写字母开头,如果需要小写,在字段后面加json的tag(比如第10行)

GO 事件处理

image.png 最常用的是time.Now()可以快速获取当前时间,也可以用time.Date()构造一个带时区的时间(10-11行)。 构造完的时间可以用.Year()等获取时间信息 可以用.Sub将两个时间相减,可以看期间有多少分多少秒。 可以用.Format格式化一个时间到时间字符串,注意字符串是用一个特定的时间而不是其他语言的YYYY-mm-dd 可以用.Parse将字符串解析成时间 可以用time.Unix()获取一个时间戳。

GO 数字解析

字符串和数字之间的转换,都在strconv包下 string convert

image.png 可以用ParseFloat和ParseInt解析字符串;parseint字符串三个参数,第二个代表进制 如果传0表示自动去推测,第三个参数64代表64位精度的整数。 也可以用Atoi快速把字符串转十进制数字,如果输入不合法会返回错误

GO 获取进程信息

image.png 用os.Args获取进程在执行时的一些命令行参数。比如上面用go run example/20-env/main.go a b c d 直接执行一个go的源文件 用os.Getenv()获取环境变量,os.Setenv写入环境变量 用exec.Command快速启动子进程并获取一些输入输出

GO实战 猜谜游戏

猜数-生成随机数

第一步要生成一个0-100的随机数

image.png 为了生成随机数需要math/rand包

image.png 一直打印同一个数字

image.png 使用之前需要设置随机数种子,否则每次都会生成相同的数字。一般的惯例用法是在程序启动时用启动的时间戳来初始化随机数种子 rand.Seed(time.Now().UnixNano()) 再来运行一次,每次都会输出不一样的结果 image.png

猜数-读取用户输入

每一个程序执行的时候都会打开几个文件,包括stdin,stdout,stderror这种。 这里用的stdin文件用os.Stdin去得到,直接操作这个文件非常不方便,我们用bufio.NewReader来把它转成一个只读的流,然后可以对这个流进行操作 .readString('\n')从这个流里读取一行,读取一行后会多一个换行符,需要用strings.TrimSuffix(input,"\n")去掉换行符,然后用strconv.Atoi把它转成数字。 中间如果转化失败则打印错误并直接退出进程 image.png

输出结果效果: image.png bufio在后面还会用到,所以这里用了较复杂的输入方式

猜数-实现判断逻辑

image.png 此时程序可以正常工作但是玩家只能输入一次猜测。无论猜测是否正确,程序都会退出。 需要加一个循环,加在读取处。 并且把出错退出的return改成continue,只有在胜利时才退出游戏。并且注意把第一行输出的随机数结果注释掉 image.png

GO实战-命令行版本词典

调用时输出一个单词,然后输出它的音标和注释(调用第三方API查询单词的翻译并打印出来)。在这个例子里我们将会学习如何用go语言发送http请求、解析json、还会学习如何用代码生成提高开发效率 image.png 要用到的API: image.png 打开页面后右键-检查,然后输入一个单词点一下翻译:

image.png 然后点击上边标签栏的Network,然后在Name中从下往上找找到dict的请求,注意要找General的Request method是POST的。我的界面是这样:

image.png Payload请求是一个post请求,这个参数是个json,json里包括两个字段,source是要翻译的单词,trans_type的en2zh表示英文到中文: image.png

preview中explanations是解释,prons是音标 请求还有一系列的header(响应头),可以看到有十来个。

右键这个dict请求-copy-Copy as cURL 注意用edge浏览器要赋值cURL(bash)

image.png

在线词典-代码生成

Convert curl commands to code (curlconverter.com) image.png 然后粘贴刚刚复制的cURL请求,在下面编程语言里选择GO

在线词典-生成代码解读

image.png 12行创建了一个http client,client可以指定很多参数比如timeout,代表client发起请求的最大超时,这里没有指定就是不限制时间。 14行创建了一个http的post请求,返回的是一个req的变量。创建请求的参数第一个是method,第二个参数是URL,第三个参数是data,不是字符串而是流,我们正常输入的会是一个字符串,所以我们会用strings.NewReader来把它转成一个流。波底可能是很大很大的一个文件比如从本地读取的一个很大的文件,如果全部放在内存里面可能会导致发出这个请求需要非常大的内存,所以这里参数会是一个流,只需要占用很小的内存。 下面是一大堆请求头,不怎么影响结构。 client.Do(req)真正发起请求,如果由于DNS解析失败或者断网导致连不上服务器的话,err会!=nil,这里会直接退出进程。 否则会成功拿到响应resp,按照golang的习惯会直接defer resp.Body.Close()这是因为返回的response的body同样是一个流,在golang里为了避免资源泄漏,需要加一个defer去手动关闭这个流。defer会在函数结束之后从下往上触发。 41行把这个流整个读到内存里变成一个byte数组,然后用%s打印出来,其实就是json字符串 运行这一段代码,就能成功输出这一大串的json: image.png

我的运行结果: image.png

生成request body

现在有个问题是输入是固定的,我们最终肯定要从一个变量输入而不是一个json的字符串输入,我们要用到json序列化。 上一小节学到的知识,我们正常要序列化一个json只需要构造一个结构体,结构体的字段名字和json一一对应,然后直接调用“json.Marshal(结构体)”即可 image.png 同理这里的结构体构造: image.png 红色表示先前使用good为例的固定翻译 20行:先new一个结构体变量,初始化里面的字段名字 接下来用json.Marshal去序列化这个request变成一个byte数组(不是真正的字符串),然后把它转换成data。

解析response body

要把response解析出来: image.png 要获取其中的几个字段比如explanations解释、音标等然后把它们输出到屏幕,在js或python这些脚本语言中这些body返回的会是一个字典或者一个叫map的结构,可以直接用[]或者.去取值,但是golang这种方式不是最佳实现,更常用的方式是和request处理一样,我们需要写一个结构体,然后这个结构体的字段和返回的response是一一对应的,然后再把返回的json反序列化到结构体里面。 但是有一点,我们看到浏览器里这个api返回的结构非常非常复杂,如果要一一对应非常繁琐且容易出错,所以我们还是要用这节课学过的知识——代码生成:打开这个网址JSON转Golang Struct - 在线工具 - OKTools

image.png 然后把浏览器里的preview粘贴过去,然后点击转换-嵌套,我们就能生成对应的结构体(如果点击转换-展开就会生成独立的多个结构体),这里我们不需要对返回结果做过多的操作,我们可以选择转换-嵌套这样生成的结果会紧凑一点

image.png 定义结构体变量dictResponse然后用json.Unmarshal去反序列化到变量里,注意要打&符号才能写入结构体,%#v会用最详细的方式来打印结构体,包括结构体名、字段名,打印出的效果: image.png image.png

词典-打印结果

我们刚刚用%#v打印出了整个结构体,但是里面大部分字段我们是不需要的,我们需要的是音标、解释 image.png 可以在结构体里看出音标是.Dictionary.Prons.En和EnUs,解释是一个数组,用range去循环打印 91-93加了三行代码,这个属于是防卫式编程,因为87行返回的response不一定是正确的response,也有可能是参数错了什么的返回一个403或者网页错了返回404,如果状态码不是200要把状态码和返回的报文打印出来方便我们诊断问题。 如果不诊断直接往下跑的话,如果出错反序列化得到的会是空结果。

词典-完善代码

完成字段打印之后最后再来改一下函数的主体 把传入单词改为变量 image.png 判断os.Args 如果不是两个(后面没有接一个单词),就打印错误然后退出,如果是两个就取到第一个参数调用query即可。 可以换不同单词查看输出:

image.png

image.png 我们这就完成了一个简单的命令行词典。

SOCKS5 代理介绍

接下来会做一个明显更复杂的项目,代码也比之前长。我们会来写一个SOCKS5代理服务器。对大家来说提到代理服务器第一个想到的可能是翻墙,但是很遗憾的是,SOCKS5虽然是代理协议但是它不能直接用来翻墙,它的协议都是明文传输。 这个协议历史非常久远,它诞生于互联网早起,它的用途是:如果某些公司的内网为了确保安全性,它可能配置很严格的防火墙策略,但它的副作用是哪怕是管理员访问一些资源也会很麻烦,SOCKS5相当于在防火墙内部开一个口,让授权的用户可以通过单个端口去访问内部的所有资源。 实际上很多翻墙软件最后暴露的也会是一个SOCKS5协议的端口给一些浏览器之类的使用。如果有些同学开发过爬虫的话就会知道,在爬虫的过程中很容易遇到比如IP访问频率超过限制,然后就会报错,这时很多人会上网找一些代理IP池(免费的或者收费的),代理IP池里很多的代理协议也就是SOCKS5协议。 最终写完代理服务器的效果: image.png 如果有同学会配置的话可以在浏览器里去配置使用这个代理,然后打开网页,服务器这边就会输出你所访问的域名+端口。 我们也可以在curl里面去测试 curl --sock5 代理服务器的IP:端口 -v(打印请求发送过程的所有细节) URL, 如果请求正常返回就说明代理是正常工作的。

SOCKS5代理-原理

image.png SOCKS5协议的工作原理:正常一个浏览器访问一个网站,如果不经过代理服务器的话,要先和对话的网站建立TCP连接(三次握手),在握手完成之后正常发起HTTP请求,然后服务器返回HTTP响应。这个过程比较简单,如果设置代理服务器的话,流程就会变得稍微复杂: 首先是浏览器要和SOCKS5代理服务器建立TCP连接,代理服务器再和真正的服务器建立TCP连接,这里总共分为四个阶段: 第一个是协商(握手)阶段,此时用户的浏览器会向SOCKS5服务器去发送请求,发送一个报文,这个报文里包括一个协议版本号(一般就是V5),还有支持的认证的种类(比如用密码或者不需要认证) 第二个阶段是认证阶段,代理服务器会从里面选一个它自己支持的认证方式返回给浏览器,告诉浏览器建议用什么方式,如果返回00就代表不需要认证,返回其他认证的话下一步会走认证流程。这里我们会跳过对认证流程的描述,因为我们要实现的是一个不加密的代理。 第三个请求阶段,认证工作之后浏览器会向SOCKS5代理服务器发送下一个报文,包括协议的版本号、请求的类型,一般是Collection请求,就代表我命令代理服务器你要和某个域名、某个IP、某个端口建立TCP连接。代理服务器收到响应之后就会真正的和后端服务器去建立TCP连接,然后返回一个报文告诉浏览器成功建立连接了。 第四个relay阶段,此时浏览器正常发送请求,代理服务器收到请求后会把其转发到真正的服务器上,如果真正的服务器有响应的话也会把响应返回浏览器。实际上代理服务器并不关心流量的细节,这里可以是HTTP流量也可以是TCP流量,这就是SOCKS5协议的工作原理。 接下来我们简单地去实现它。

SOCKS5代理 - TCP echo server

由于整个协议是较复杂的,我们先不会尝试直接去实现它,先实现一个tcp echo server,逻辑很简单就是你给它发送什么它就给你回复什么,这样我们可以测试server写得对不对。 image.png net.Listen侦听端口,侦听完返回到server,然后用server.Accept去接受请求,如果成功就会返回一个连接,然后在process里去处理这个连接,注意前面有一个go关键字,这里代表会启动一个go routine,可以暂时类比为其他语言里的启动一个子线程去处理这个连接,只不过在golang里go routine的开销会比子线程、子进程要小很多,可以轻松处理上万的并发。 重点是process函数的实现,代表函数配置的时候一定要把这个连接关掉,因为这个连接的生命周期就是函数的生命周期。 接下来用bufio.NewReader去基于这个连接创建一个只读的带缓冲的流(在第一个例子里有用到过) for循环的死循环里用reader.ReadByte()每次读一个字节,然后用conn.Write([]byte{b})把这个字节写入 []byte包装作类型转换,出错就直接break关闭连接。 bufio实际上是一个带缓冲的流,那就意味着28行看起来是一个字节一个字节的读,这其实是看起来非常低效的,因为正常的服务端可能都是几百个字节\KB\M发送的,会有很多次系统调用,但实际上底层实现会把它做一个合并,就是读第一个字节的时候可能提前把下一个KB都读完,这样再读剩下的数据时都会瞬间返回。 写完server后简单测试一下,这里测试就不能用curl命令了,用nc命令,这个命令可以直接和某个IP+端口去建立一个TCP连接: image.png 然后输入任意字符串如hello,服务期就会返回hello。那么这一步已经完成了一个能返回你输入信息的TCP server

SOCKS5代理-auth

接下来去试图实现协议的第一部,认证阶段。

image.png 先实现一个空的auth函数,参数是一个只读流,后面加一个原始的TCP连接,函数体待会再看。 再来改一下process函数,把刚刚的死循环删掉,改为调用这个auth函数。 认证阶段的逻辑:第一步浏览器会给代理服务器发送一个报文,这个报文有三个字段,第一个字段是version协议版本号,第二个字段建成方式的树木,最后一个字段是对每个建成方式的编码。 一些常用的建成方式:0代表不需要建成,2代表用户名-密码的建成。 先要把报文给完整读出来,前两个报文都是单字节的,可以用reader.ReadByte()去读一个字节,55行读入版本号,如果出错返回出错信息并关闭连接。 然后读methodSize,再用methodSize去创建一个缓冲区,然后用一个io.ReadFull去把它填充满。 此时我们就成功读到了所有的三个字段,加日志把它打印出来。 按照协议我们需要返回一个包告诉浏览器我们选择了哪种建成方式,77行把包构造出来,第一个字节是协议版本号(5),后面是选择的建成方式。 写完这个代码之后我们简单滴用curl命令测一下。

image.png 注意如果端口占用需要用cmd的netstat -ano | findstr xxx,找到占用端口的进程的pid后再在cmd用taskkill /pid xxx -f中止相应进程即可 此时可以预期curl命令一定是不成功的,因为我们的协议还只实现了第一步,但是看日志应该是能成功打印出来version和method两个字段的,说明我们当前的实现是正确的。

SOCKS5代理- 请求阶段

image.png 这一段代码会试图阻止浏览器发送一个报文,这里面携带了用户需要访问的url或者IP+端口,然后先把它打印出来。 用一个和auth函数类似的connection函数,签名是一致的。然后同样在process函数里面去调用。(上面是auth下面是connect) 回忆一下请求阶段的逻辑:浏览器会发送一个报文,报文里包含6个字段,第二个字段CMD只支持connetion请求(就是让代理服务器和下游服务器创建连接), 第四个字段ATYP是重点关注的——目标地址类型,它可能是多种类型比如IPv4,IPv6或者是一个域名(1代表ipv4,3代表域名)如果是ipv4的话后面的地址就是固定长度4个字节,如果是域名的话下面是变长的一个字符串。第一个字节是长度,后面的n个字节就是真正的域名。最后是端口号2个字节。 前四个字节可以用ReadByte读取,这里用另一种方式,创建一个长度为4的缓冲区,然后用io.ReadFull把它直接填充满,这样就可以一次性读取到前四个字段。因为他们都是定长的。 接下来111-116对每个字段验证它们的合法性,接下来对于atyp有不同的类型,如果是IPV4同样需要去读4个字节,上面的缓冲区恰好是四个字节,直接继续用即可,然后把它打印成一个ip地址(124行)。 如果atyp是host类型,照例先读一个字节(host的长度)然后再make一个对应长度的字符串用ReadFull把它填充满,填充满之后把它转化成一个字符串即可(135行); 如果是ipv6也是读一个固定长度的,这里就暂时不实现了因为用得比较少,其他方式也不予支持。 前五个字段已读完,最后剩端口号两个字节,我们可以用一个新的两个字节的缓冲区io.ReadFull,这里我们用另一种方式实现,我们复用之前的长度为4的缓冲区,用切片语法把它裁剪成两个字节的缓冲区,把它填充满。然后新的切片和原始切片是复用底层数据的,所以145行的缓冲区是能直接读到端口号数据的,解析出来整形数组。 147打印,表示将与这个地址这个端口号建立连接。 按照协议,我们接收到浏览器这个请求之后我们还要给与一个回报,回报报文字段还是挺多,但是很多都用不上,比如BND.ADDR, BND.PORT,都不是我们所支持的这种connection请求所必须的,我们都会直接把它填成0值。 所以按照协议我们第一个字段填成5,后面REP填成0代表成功,atyp为1代表ipv4,后面BND.ADDR不用 4个字节4个0, port两个字节两个0。那么我们得connection阶段就算完成。我们可以简单测试下。

image.png 还是会失败,但是我们应该能看到我们能正常打印出我们需要访问的ip和端口,说明当前我们的实现都是正确的。这样子我们接下来就可以做最后一步,我们需要真正和这个ip端口去建立连接双向转发数据,我们代理就算完成了。

SOCKS5代理 - relay阶段

最后一步,和真正的服务器建立tcp连接。我们会用到net包的Dial函数,这个就是简单的去用tcp协议往ip或者域名+端口去建立tcp连接。 建立连接后如果没有出错我们第一时间加一个defer dest.Close()来在函数结束的时候去关闭连接。 接下来我们需要建立浏览器和下游服务器的双向数据转发,找一下标准库,在io包里有一个Copy函数,它可以实现一个单向数据转发,可以把src这样一个只读流里面的数据用一个死循环去逐步拷贝到dst这个可写流(和第一版显示输入的数据的代码是类似的)。但现在要实现双向数据转发用一个io.Copy是不够的,我们要启动两个go routine

image.png 启动两个go routine在里面分别调用io.Copy,注意两个拷贝的方向是不一样的,一个是从用户的浏览器拷贝数据到底层的服务器,另一个是从底层的服务器拷贝数据到用户的浏览器,但是现在的版本实现有一个问题,其中go routine是几乎不耗时间的,正常情况下会直接跑到第183行,正割函数就直接返回了,连接也就被关闭了。我们需要等待任何一个方向的Copy失败,就代表可能某一方关闭连接了,我们此时才中止整个连接。 我们会用到标准库里的一个context机制,它是golang标准库里一个很重要的内容,我们会用.WithCancel来创建一个context值,然后在182行我们会等待context.Done,即等待context执行完成,这个时机也是cancel函数被调用的时机,任何一个go routine出错的时候就调用一下cancel函数,第171行也是防御式编程,也会调用一次cancel虽然没有什么意义。这样就实现了任何一个方向的copy失败我们就返回这个函数并把双方的连接都关闭掉、清理数据。这一部代理服务器就完工了。我们可以最后来测试下。

image.png 我的运行: image.png curl命令正常是会成功的,然后在curl命令的日志里面它和socks5代理服务器去协商然后去发起请求,然后再去发起HTTP请求、接收收到响应,然后左边窗口的代理服务器也会打印出已和某个IP、某个端口建立连接了。 image.png 我们也能在浏览器里去测试这个代理,在chrome浏览器里我们需要安装一个chrome插件SwitchyOmega,然后在这个页面点击新建情景模式,代理服务器选socks5,代理服务器端口同。设置完点应用选项、应用更改,然后再点右上角小圈圈切换成刚刚配置的代理。此时再打开新的网页的话,新的网页的所有流量都会通过代理服务器,代理服务器这边则会输出你所访问的域名+端口: image.png image.png

Go语言学习线路图: image.png

Q&A: 注意用Scanf和用户输入字符串不一样的话可能会重复Scanf多次才能消化掉输入,一般建议按行读取然后解析。 windows下按住ctrl点击函数会跳到源码。