这是我参与「第五届青训营 」伴学笔记创作活动的第 4 天
继续学习了 Go 语言基本语法后,通过实战的方式,可以帮助我们更好更正确的在短时间内掌握这门新学习的 Go 语言。接下来通过一个个例子为之后大项目开发打好基础。
今天分享三个案例:
- 猜谜游戏
- 在线词典 - SOCKS5 代理
猜谜游戏
题目描述
程序首先生成一个0-100 之间的随机整数,让玩家去猜,玩家每次输入一个数字之后,程序会告知玩家是猜大了还是猜小了还是猜对了,即,猜测的值是高于那个秘密数值还是低于秘密的随机数,没猜对的话,提示玩家继续猜测。如果猜对了,就告诉玩家胜利,并退出程序。
代码分析
通过题目描述我们对题目进行分析,得到以下要点:
(1)生成一个随机数
rand
包实现了伪随机数生成器。
具体用法可以查看官方文档:math_rand go官方标准文档
import
导入随机数的包
import (
"math/rand"
)
生成一个随机数
//调用 rand 生成伪随机int
fmt.Println(rand.Int())
fmt.Println(rand.Int31())
生成特定范围内的随机整数
maxNum := 100
fmt.Println(rand.Intn(100))
secretNumber := rand.Intn(100)
secretNumber := rand.Intn(maxNum)//maxNum是一个变量名
每次生成的随机数都不一样
如果不设置随机数种子的话,每一次都会生成相同的随机序列。即 rand
包会在程序每次运行时都产生确定的序列。(因为调用的是默认的公共资源)。
如果需要每次运行产生不同的序列,应使用Seed
函数进行初始化。即:rand.Seed
设置随机数种子
如何让每次生成的随机数都不一样,那么就让每次随机数种子不一样,而每分每秒的时间再更改,且不会重复,因此我们引入 时间
time
包,用时间戳作为随机数种子。
rand.Seed(time.Now().UnixNano())// 取纳秒时间戳,可以保证每次的随机数种子都不同
secretNumber := rand.Intn(maxNum)
fmt.Println("The secret number is ", secretNumber)
(2)获取正确输入的值
首先这个需要与玩家进行互动,但是玩家可能不能正确输入猜测是数字,比如玩家在屏幕上输入一个字符串、大于100的数、负数、非整数……情况。
那如何实现用户输入输出,并解析成数字呢?Go 语言中是这样的:
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')
感觉这一部分用 Go 语言实现起来,不是很好理解。。。
首先:每个程序执行的时候都会打开几个文件,比如:
sdin stdout stder
等
(如果有和我一样不懂这些文件具体含义的话,或者百度,或者继续看)
stdin
文件可以用os Stdin
来得到,但是直接操作这文件很不方便,我们会用buio.Newheader
把一文件转换一个reader
变量,reader
变量上会有很多用来操作一个流的操作,可以用ReadString
方法来读取一行。
这样做的目的是,如果读取失败了的话,还可以打印错误并能退出。
if err != nil {
fmt.Println("An error occured while reading input. Please try again", err)
return
}
然后 ReadString
返回的结果结尾的换行符,要把它去掉,再转换成数字。
input = strings.Trim(input, "\r\n")
如果转换失败,我们同样打印错误,退出。
input = strings.Trim(input, "\r\n")
guess, err := strconv.Atoi(input)
if err != nil {
fmt.Println("Invalid input. Please enter an integer value")
return
}
这里有几个知识点,基础小白我来总结一下,方便理解。
(1)导入必要的包
import (
"bufio"
"os"
"strconv"
"strings"
)
(2)这里引入了 bufio
包
bufio
包实现了缓存IO
。它包装了io.Reader
和io.Writer
对象,创建了另外的Reader
和Writer
对象,它们也实现了io.Reader
和io.Writer
接口,不过它们是有缓存的。该包同时为文本I/O
提供了一些便利操作。
更多Go 输入输出的用法 参考:Go 语言中文网-输入输出
(3)bufio.NewReader(os.Stdin)
bufio.NewReader()
bufio.Reader 结构包装了一个 io.Reader 对象,提供缓存功能,同时实现了 io.Reader 接口。
bufio 包提供了两个实例化 bufio.Reader 对象的函数:NewReader 和 NewReaderSize。其中,NewReader 函数是调用 NewReaderSize 函数实现的。
bufio.NewReader()
“包装一个 io.Reader 或 io.Writer 对象,创建另一个对象(Reader 或 Writer),该对象也实现接口(interface)但提供缓冲和一些文本。
(4)os.Stdin
指向标准输入文件
/dev/stdin
,即os.Stdin
是标准输入文件/dev/stdin的指针
。 os.Stdin是os包File结构体的指针类型。 os.Stdin是os包的一个变量。它是os包的NewFile函数的返回值。NewFile函数返回指向标准输入文件/dev/stdin的指针。
(5)strings.Trim
func Trim(s string, cutset string) string
返回将 s 前后端所有 cutset 包含的 utf-8 码值都去掉的字符串。
strings.Trim("!!! Achtung! Achtung! !!!", "!")
//Achtung! Achtung
(6)reader.ReadString
reader.ReadString
调用了 ReadBytes
方法,并将结果的 []byte
转为 string
类型。
ReadBytes 从输入中读取直到遇到界定符(delim)为止,返回的 slice 包含了从当前到界定符的内容 (包括界定符) 。如果 ReadBytes 在遇到界定符之前就捕获到一个错误,它会返回遇到错误之前已经读取的数据,和这个捕获到的错误(经常是 io.EOF)。
同样的还有这些方法: ReadSlice、ReadBytes和 ReadLine 方法
(7)strconv.Atoi(input)
将字符串类型转换为int
类型。
(8)if err != nil
if err != nil{
//do something
}
当出现error
不等于 nil
的时候,说明出现某些错误了,需要我们对这个错误进行一些处理,而如果等于 nil
说明运行正常没有错误。那什么是 nil
呢?nil
的意思是无,或者是零值。在 Go
语言中,如果你声明了一个变量但是没有对它进行赋值操作,那么这个变量就会有一个类型的默认零值。每种类型对应的零值为:
- bool -> false
- numbers -> 0
- string -> ""
- pointers -> nil
- slices -> nil
- maps -> nil
- channels -> nil functions -> nil interfaces -> nil
Go的文档中说到,nil是预定义的标识符,代表指针、通道、函数、接口、映射或切片的零值,也就是预定义好的一个变量。
(3)比较数值大小
比较玩家输入的值和生成的秘密数值,因为这两个变量的类型都是数值,因此可以直接进行比较。
这里只需要使用 if else
语句,根据比较的结果,用 fmt.Println("")
对玩家输出不同的提示语。
if guess > secretNumber {
fmt.Println("Your guess is bigger than the secret number. Please try again")
} else if guess < secretNumber {
fmt.Println("Your guess is smaller than the secret number. Please try again")
} else {
fmt.Println("Correct, you Legend!")
}
(4)让程序循环
贴一下完整代码:
package main
import (
"bufio"
"fmt"
"math/rand"
"os"
"strconv"
"strings"
"time"
)
func main() {
maxNum := 100
rand.Seed(time.Now().UnixNano())
secretNumber := rand.Intn(maxNum)
// fmt.Println("The secret number is ", secretNumber)
fmt.Println("Please input your guess")
reader := bufio.NewReader(os.Stdin)
for {
input, err := reader.ReadString('\n')
if err != nil {
fmt.Println("An error occured while reading input. Please try again", err)
continue
}
input = strings.Trim(input, "\r\n")
guess, err := strconv.Atoi(input)
if err != nil {
fmt.Println("Invalid input. Please enter an integer value")
continue
}
fmt.Println("You guess is", guess)
if guess > secretNumber {
fmt.Println("Your guess is bigger than the secret number. Please try again")
} else if guess < secretNumber {
fmt.Println("Your guess is smaller than the secret number. Please try again")
} else {
fmt.Println("Correct, you Legend!")
break
}
}
}
SOCKS5 代理
什么是 SOCKS5
SOCKS5
代理服务器所用的协议是代理协议,但是它的协议都是明文传输。
其历史比较久远,诞生于互联网早期。它的用途是,比如某些企业的内网为了确保安全性,有很严格的防火墙策略,但是带来的副作用就是访问某些资源会很麻烦。
SOCKS5
相当于在防火墙开了个口子,让授权的用户可以通过单个端口去访问内部的所有资源,实际上很多翻墙软性,最终暴露的也是一个 SOCKS5
协议的端口。
其实断章取义,感觉“代理”两个字就有种第三方的感觉。
SOCKS5 协议的工作原理
首先从正常浏览器访问一个网页开始,如果不经过代理服务器的话,就是我们平时了解的流程:
先和对方的网站建立 TCP
连接,经过三次握手,握手完之后发起 HTTP
请求,然后返回 HTTP
响应。想了解的同学可以参考这篇文章,写的非常具体:juejin.cn/post/715325…
而如果设置代理服务器之后,流程会变成这样:
请求请求请求
首先,浏览器和 SOCKS5
代理建立 TCP
连接,代理再和真正的服务器建立 TCP 连接。这里可以分成四个阶段:握手阶段、认证阶段、请求阶段、 relay 阶段。
- 1. 握手阶段
浏览器会向SOCKS5
代理发送请求,包的内容包括一个协议的版本号,还有支持的认证的种类,SOCKS5
服务器会选中一个认证方式,返回给浏览器,如果返回的是 00 的话就代表不需要认证,返回其他类型的话会开始认证流程。
-
2. 认证阶段 参考:juejin.cn/post/715325…
-
3. 请求阶段
认证通过之后浏览器会向SOCKS5
服务器发起清或。主要信息包括:版本号,请求的类理,一般主要是 connection
请求,就代表代理服务器要和某个域名或者某个IP
地址某个端口建立 TCP
连接。代理服务器收到响应之后,会真正和后端服务器建立连接,然后返回一个响应。
- 4. relay阶段
此时浏览器会正常发送请求,然后代理服务器接收到请求之后,会直接把请求转换到真正的服务器上,如果真正的服务器以后返回响应的话,那么也会把请求转发到浏览器。
实际上 代理服务器并不关心流量的细节,可以是 HTTP
流量,也可以是其它 TCP
流量。
新学习一门计算机语言要坚持上机实战,从学会动手解决问题中进步。