「GO」三个实战案例 | 青训营笔记

31 阅读8分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 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.Readerio.Writer 对象,创建了另外的 Reader Writer 对象,它们也实现了 io.Readerio.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 协议的端口。

image.png 其实断章取义,感觉“代理”两个字就有种第三方的感觉。

SOCKS5 协议的工作原理

首先从正常浏览器访问一个网页开始,如果不经过代理服务器的话,就是我们平时了解的流程:

先和对方的网站建立 TCP 连接,经过三次握手,握手完之后发起 HTTP 请求,然后返回 HTTP 响应。想了解的同学可以参考这篇文章,写的非常具体:juejin.cn/post/715325…

而如果设置代理服务器之后,流程会变成这样: 请求请求请求 首先,浏览器和 SOCKS5 代理建立 TCP 连接,代理再和真正的服务器建立 TCP 连接。这里可以分成四个阶段:握手阶段、认证阶段、请求阶段、 relay 阶段。

add7b252364e6b22eec0fdcc1c45fb0.png

  • 1. 握手阶段

浏览器会向SOCKS5代理发送请求,包的内容包括一个协议的版本号,还有支持的认证的种类,SOCKS5 服务器会选中一个认证方式,返回给浏览器,如果返回的是 00 的话就代表不需要认证,返回其他类型的话会开始认证流程。

认证通过之后浏览器会向SOCKS5服务器发起清或。主要信息包括:版本号,请求的类理,一般主要是 connection 请求,就代表代理服务器要和某个域名或者某个IP地址某个端口建立 TCP 连接。代理服务器收到响应之后,会真正和后端服务器建立连接,然后返回一个响应。

  • 4. relay阶段

此时浏览器会正常发送请求,然后代理服务器接收到请求之后,会直接把请求转换到真正的服务器上,如果真正的服务器以后返回响应的话,那么也会把请求转发到浏览器。

实际上 代理服务器并不关心流量的细节,可以是 HTTP流量,也可以是其它 TCP 流量。


新学习一门计算机语言要坚持上机实战,从学会动手解决问题中进步。