后端 | 青训营笔记

103 阅读13分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 1 天

一、Go语言上手-基础语法

1、简介

1.1 什么是Go语言?

Go语言是Google出品的一门通用性计算机语言

1.高性能、高并发

有和C++、Java媲美的性能,还对高并发的支持,不像有的语言以库的方式支持;在Go Land中不要寻找经过高度性能优化的第三方库来,只需要使用标准库或者是基于标准库的第三方库即可开发高并发应用程序。

2.语法简单、学习曲线平缓

Go的语法简单易懂,Go语言体语法,文格类似于C语言,并且在C语言的基础上进行了大幅度的简化,比如去掉了不需要的表达式,括号,循环也只有for循环一种方法,就可以同时实现数值、键值等等各种便利。因此Go语言上手非常容易学习出现,不需要像C或者C++这些语言,需要两到三年的学习期,一个是Go开发者,只需要短短一周时间就可以从学习阶段转到真正的开发阶段,并完成一个高并发的应用程序的开发。

3.丰富的标准库

4.完善的工具链

内置完整的单元测试框架,能够支持单元测试、性能测试、代码覆盖率、数值间断检测、性能优化,以保证代码正常运行的必备条件

5.静态链接

6.快速编译

7.跨平台

8.垃圾回收

和Java类似,无需考虑内存的分配释放

1.2 哪些公司在使用Go语言,主要用于哪些场景?

字节跳动(编写微服务,开源IPC框架,Go)、腾讯、滴滴、百度、Google、face book、七牛云、bilibili、Ping CAP;

场景:

云计算、微服务;大数据、区块链、物联网;

1.3 字节跳动为什么全面拥抱Go语言?

1.最初使用的Python,由于性能问题换成了Go.

2.C++不太适合在线web业务

3.早期团队非Java背景

4.性能比较好

5.部署简单、学习成本低

6.内部RPC和HTTP框架的推广

2、入门

2.1 开发环境

(1)安装Go Lang

go.dev/ studygolang.com/dl goproxy.cn/

(2)配置集成开发环境

image-20230115113612771

(3)基于云的开发环境

gitpod.io/#github.com…

短链接: hi-hi.cn/gitpod

image-20230115113738213

2.2 基础语法

(1)Hello World

import("fmt")导入标准库中的fmt包,主要用于往屏幕输入输出字符串,格式化字符串

func main()main函数

package main  //代表文件属于main包的一部分,是程序的入口文件import ("fmt") //导入标准库中的fmt包,func main() {
   fmt.Println("hello world")  //调用了fmt中的Println打印方法
}

go run example/01-hello/main.go

go run直接运行

go build example/01-hello/main.go /.main

先使用go build,再调用/.main执行

(2) 变量

简介:关于Go的变量类型,Go是一种强类型语言,每一个变量都有它自己的变量类型,变量类型包括字符串、整型、浮点型、布尔型;Go语言中,变量是内置类型,可以直接通过"+"进行拼接,例如:var a = "initial" g := a + "foo",也可以使用"="比较两个字符串;Go语言中的运算符执行顺序与C++类似。

1 变量的声明

第一种方式:var 变量名 = 值; 例如:var a = "initial"

一般会自动推导变量的类型,也可以自定义

var 变量名 数据类型 = 值,例如:var b, c int = 1, 2

第二种方式: 变量名:= 值,例如:f := float32(e)

var a = "initial"var b, c int = 1, 2var d = truevar e float64
​
f := float32(e)
​
g := a + "foo"
fmt.Println(a, b, c, d, e, f) // initial 1 2 true 0 0
fmt.Println(g)                // initialapple
2 常量

const 变量名 数据类型 = 值,例如:const s string = "constant"

常量没有使用的类型,会根据上下文使用来自动确定类型。

const s string = "constant"
const h = 500000000
const i = 3e20 / h
fmt.Println(s, h, i, math.Sin(h), math.Sin(i))

2.3 基础语法-if else

Go语言中的 if 相较于 java,if 后面的表达式没有小括号;如果在 if 后面加上括号,在编译保存时,会自动去掉括号;if 和 else后面必须跟 {}

if 7%2 == 0 {
   fmt.Println("7 is even")
}else {
   fmt.Println("7 is odd")
}
​
if num := 9; num < 0 {
        fmt.Println(num, "is negative")
    } else if num < 10 {
        fmt.Println(num, "has 1 digit")
    } else {
        fmt.Println(num, "has multiple digits")
    }

2.4 基础语法-循环for

Go中只有 for 一种循环,for 后面的表达式不需要括号

(1) 死循环

for后面什么都不写

for {
   fmt.Println("loop")
   break
}
(2) 经典遍历循环

在循环中,可以通过 continue、break 控制循环

j := 7; j < 9; j++ 三个表达式哪个都可以省略

for j := 7; j < 9; j++ {
   fmt.Println(j)
}
​
for n := 0; n < 5; n++ {
    if n%2 == 0 {
        continue
    }
    fmt.Println(n)
}
​
for i <= 3 {  
    fmt.Println(i)
    i = i + 1
}

2.5 基础语法-switch

switch 后面的表达式不需要括号;Golang中switch-case分支下不需要加break,Go在执行完当前分支后,会直接跳出当前的switch。

a := 2
switch a {
case 1:
   fmt.Println("one")
case 2:
   fmt.Println("two")  //执行完当前语句后,直接执行t := time.Now()
case 3:
   fmt.Println("three")
case 4, 5:
   fmt.Println("four or five")
default:
   fmt.Println("other")
}  
t := time.Now()

Golang中的switch可以支持任意的变量类型,例字符串,结构体;甚至可以执行if-else语句,switch 后面可以不加变量,直接在case语句进行判断,更方便易懂。

switch {
case t.Hour() < 12:
   fmt.Println("It's before noon")
default:
   fmt.Println("It's after noon")
}

2.6 基础语法-数组

数组一种具有编号,且长度固定的元素序列

定义数组:

一维数组 var 数组名 [数组长度]数组类型var a [5]int

二维数组 var 数组名 [行] [列]数组类型,var twoD [2][3]int

len() 数组长度的方法

给数组元素赋值:数组名[数组的索引] = 值a[4] = 100

var a [5]int   //定义以为数组
a[4] = 100     //给数组赋值
fmt.Println("get:", a[2])
fmt.Println("len:", len(a))

b := [5]int{1, 2, 3, 4, 5}   //定义数组且赋值
fmt.Println(b)

var twoD [2][3]int   //定义二维数组
for i := 0; i < 2; i++ {
   for j := 0; j < 3; j++ {
      twoD[i][j] = i + j
   }
}
fmt.Println("2d: ", twoD)

2.7 基础语法-切片 slice

不同于数组,是可变长度的数组,可以在任意时刻去更改长度

使用 make() 创建切片,s := make([]string//数据类型, 3//长度)

使用len()计算长度

使用append()在数组后面追加元素,必须赋值回原数组s = append(s, "e", "f")

在Golang中,slice的原理是存储了长度、容量、指向数组的指针;在使用append()方法时,如果容量不够,会发生扩容,并返回一个新的slice,所以需要赋值回去

使用copy()来拷贝数值,c := make([]string, len(s)) copy(c, s)

拥有像python一样的切片操作,s[2:5]截取数组中第二个位置到第五个位置(不包括第五个元素)的元素;不同于python,Golang不能使用复数索引,需用len()去除长度,再做一定的运算

s := make([]string, 3)  //使用make()创建切片
s[0] = "a"
s[1] = "b"
s[2] = "c"
fmt.Println("get:", s[2])   // c
fmt.Println("len:", len(s)) // 3   //使用len()计算长度

s = append(s, "d")           //使用append()在数组后面追加元素,必须赋值回原数组
s = append(s, "e", "f")
fmt.Println(s) // [a b c d e f]

c := make([]string, len(s))
copy(c, s)        				//可以使用copy()来拷贝数值
fmt.Println(c) // [a b c d e f]

fmt.Println(s[2:5]) // [c d e]  //使用切片操作,截取数组中某一段元素
fmt.Println(s[:5])  // [a b c d e]
fmt.Println(s[2:])  // [c d e f]

good := []string{"g", "o", "o", "d"}
fmt.Println(good) // [g o o d]

2.8 基础语法-map

在其他变成语言中,被称为哈希或者字典;map是实际使用过程中最频繁的数据结构。

使用make(map[key的类型]value的类型)创建空map,map[key的类型]value的类型

使用m[key]=value,写入(key,value)键值对,m["one"] = 1

使用len()计算长度

使用delete(m, "one")删除m中key为"one"的(k,v)键值对

在读取时,可以在变量名后加上ok,判断map中是否有对应的键值对r, ok := m["unknow"]

//map是随机的顺序

m := make(map[string]int)   //创建空map,map[key的类型]value的类型
m["one"] = 1				//写入(key,value)键值对,m[key]=value
m["two"] = 2
fmt.Println(m)           // map[one:1 two:2]
fmt.Println(len(m))      // 2
fmt.Println(m["one"])    // 1
fmt.Println(m["unknow"]) // 0

r, ok := m["unknow"]    	//在读取时,可以在变量名后加上ok,判断map中是否有对应的键值对存在
fmt.Println(r, ok) // 0 false
//map是随机的顺序
delete(m, "one") 		//删除k,v键值对

m2 := map[string]int{"one": 1, "two": 2}
var m3 = map[string]int{"one": 1, "two": 2}
fmt.Println(m2, m3)

2.9 基础语法-range

对于一个slice或者map,可以使用range进行快速的遍历,可以有效减少代码。

对于数组会返回两个值,一个是数组的索引,一个是对应位置的值;如果不需要索引可以使用"_"忽略

nums := []int{2, 3, 4}
sum := 0
for i, num := range nums {  //对于数组会返回两个值,i是数组的索引,num是对应位置的值
   sum += num
   if num == 2 {
      fmt.Println("index:", i, "num:", num) // index: 0 num: 2
   }
}
fmt.Println(sum) // 9

对于map,返回key,value,也可以单独返回key

m := map[string]string{"a": "A", "b": "B"}
for k, v := range m {
   fmt.Println(k, v) // b 8; a A
}
for k := range m {
   fmt.Println("key", k) // key a; key b
}

2.10 基础语法-函数

Golang语言和其他语言函数不同的地方在于函数,变量类型是后置的func add(a int, b int) int{}

func add(a int, b int) int {
   return a + b
}
func add2(a, b int) int {
	return a + b
}

Golang中原生支持返回多个值,但是在实际的业务场景中,几乎都返回多个值;第一个是真正的返回结果,第二个是错误信息

func exists(m map[string]string, k string) (v string, ok bool) {
    //第一个值是value ,第二个值是判断是否存在
   v, ok = m[k]
   return v, ok
}
func main() {
	res := add(1, 2)
	fmt.Println(res) // 3

	v, ok := exists(map[string]string{"a": "A"}, "a")
	fmt.Println(v, ok) // A True
}

2.11 基础语法-指针

相比于C++中的指针,用途非常有限;主要用途是对常用的参数进行修改。

func add2(n int) { 
   n += 2   		//该写法是不起作用
}

func add2ptr(n *int) {   //在定义时加上*
   *n += 2			//运算时加上*,实现一个变量自增2的函数
}

func main() {
   n := 5
   add2(n)
   fmt.Println(n) // 5
   add2ptr(&n)			//为了遇上面的*对应,调用时参数前需要加&
   fmt.Println(n) // 7
}

2.12 基础语法 - 结构体

带类型的字段集合

使用type定义结构体

type user struct {   
   name     string
   password string
}
package main

import "fmt"

type user struct {   //使用type 定义结构体,有name,password
   name     string
   password string
}

func main() {
   a := user{name: "wang", password: "1024"}  //使用结构体名称初始化结构体变量,在初始化时需要传入每个字段的初始值
   b := user{"wang", "1024"}
   c := user{name: "wang"}  //也可以只指定一部分的值,若没有值,换为空值
   c.password = "1024"  //对于字段可以用.读取字段和写入字段里面的内容
   var d user  //创建一个空对象
   d.name = "wang"//设值
   d.password = "1024"  //设值

   fmt.Println(a, b, c, d)                 // {wang 1024} {wang 1024} {wang 1024} {wang 1024}
   fmt.Println(checkPassword(a, "haha"))   // false
   fmt.Println(checkPassword2(&a, "haha")) // false
}
//结构体能作为函数的参数,作为参数也会有指针和非指针两种用法
func checkPassword(u user, password string) bool {  //非指针
   return u.password == password
}

func checkPassword2(u *user, password string) bool {  //指针
   return u.password == password  //使用指针可以对结构体进行修改,也可以避免大结构体的某些开销
}

2.13 基础语法-结构体方法

Golang中可以为结构体定义一些方法,类似于其它语言中的类成员函数

type user struct {
   name     string
   password string
}
//类成员函数
func (u user) checkPassword(password string) bool {  //不带指针
   return u.password == password
}

func (u *user) resetPassword(password string) {//带指针,可对结构体进行修改
   u.password = password
}

func main() {
   a := user{name: "wang", password: "1024"}
   a.resetPassword("2048")
   fmt.Println(a.checkPassword("2048")) // true
}

image-20230115162434400

image-20230115162456262

2.14 基础语法-错误处理(error)

Golang中错误处理的习惯是使用一个参数的返回值来传递信息;不同于java中的异常,Go语言可以很清晰的知道哪个函数返回了错误,并且能够用简单if-else去处理错误;

package main

import (
   "errors"
   "fmt"
)

type user struct {
   name     string
   password string
}
//在函数中,可以在返回值加一个error,这就代表着这个函数可能返回错误,return也需要return两个值
func findUser(users []user, name string) (v *user, err error) {
   for _, u := range users {
      if u.name == name {
         return &u, nil  //如果没有错误就返回一个nil值,nil可以代表很多类型的零值,可以直接使用nil
      }
   }
   return nil, errors.New("not found")  //如果错误则返回错误信息
}

func main() {
   u, err := findUser([]user{{"wang", "1024"}}, "wang")  //接收需要两个变量 u,err
   if err != nil {
      fmt.Println(err)
      return
   }
   fmt.Println(u.name) // wang

   if u, err := findUser([]user{{"wang", "1024"}}, "li"); err != nil {
      fmt.Println(err) // not found
      return
   } else {
      fmt.Println(u.name)
   }
}

2.15 基础语法-字符串操作(string)

strings包中包含许多字符串工具

strings.Contains(a, "ll") 判断字符传中是否包含另一个字符串

strings.Count(a, "l") 字符串计数,字符串中有几个该字符

strings.HasPrefix(a, "he")

strings.HasSuffix(a, "llo")

strings.Index(a, "ll") 查找某个字符串的位置

strings.Join([]string{"he", "llo"}//需要连接的字符串, "-"//使用什么符号连接) 连接多个字符串

strings.Repeat(a, 2) 重复字符串

len(a)获取字符串的长度,注意:一个中文可能对应多个字符

strings.Replace(a, "e", "E", -1)

strings.Split("a-b-c", "-")

strings.ToLower(a)

strings.ToUpper(a)

package main

import (
   "fmt"
   "strings"
)

func main() {
   a := "hello"
   fmt.Println(strings.Contains(a, "ll"))                // true   //判断字符传中是否包含另一个字符串
   fmt.Println(strings.Count(a, "l"))                    // 2		//字符串计数,字符串中有几个该字符
   fmt.Println(strings.HasPrefix(a, "he"))               // true	
   fmt.Println(strings.HasSuffix(a, "llo"))              // true
   fmt.Println(strings.Index(a, "ll"))                   // 2		//查找某个字符串的位置
   fmt.Println(strings.Join([]string{"he", "llo"}, "-")) // he-llo	//连接多个字符串,
   fmt.Println(strings.Repeat(a, 2))                     // hellohello  //重复字符串
   fmt.Println(strings.Replace(a, "e", "E", -1))         // hEllo
   fmt.Println(strings.Split("a-b-c", "-"))              // [a b c]
   fmt.Println(strings.ToLower(a))                       // hello
   fmt.Println(strings.ToUpper(a))                       // HELLO
   fmt.Println(len(a))                                   // 5
   b := "你好"
   fmt.Println(len(b)) // 6
}

2.16 基础语法-字符串格式化(fmt)

在fmt包中有许多字符串格式化相关的方法

fmt.Println() 输出换行

fmt.Printf("s=%v\n", s)与C++中的printf极为相似,不同的是Golang是使用 %v就可以输出任意类型的变量,不需要进行区分;使用 %+v得到更为详细的结构;使用 %#v会进一步详细;

point{1, 2}同一数据,%v:p={1 2};%+v:p={x:1 y:2};%#v:p=main.point{x:1, y:2}

使用 %.nf,打印出保留n为小数的浮点数

package main

import "fmt"

type point struct {
   x, y int
}

func main() {
   s := "hello"
   n := 123
   p := point{1, 2}
   fmt.Println(s, n) // hello 123
   fmt.Println(p)    // {1 2}

   fmt.Printf("s=%v\n", s)  // s=hello
   fmt.Printf("n=%v\n", n)  // n=123
   fmt.Printf("p=%v\n", p)  // p={1 2}				//可以输出任意类型的变量,不需要进行区分
   fmt.Printf("p=%+v\n", p) // p={x:1 y:2}			//得到更为详细的结构
   fmt.Printf("p=%#v\n", p) // p=main.point{x:1, y:2}	//会进一步详细

   f := 3.141592653
   fmt.Println(f)          // 3.141592653
   fmt.Printf("%.2f\n", f) // 3.14			//打印出保留n为小数的浮点数
}

2.17 基础语法-JSON处理

对于已有的结构体,只要确保每个Key都是大写,保证在Golang中是公开字段就可以了

第一步,buf, err := json.Marshal(a) 使用json.Marshal()方法序列化,将结构体转换为byte数组

第二步,在打印时,需要将byte数组转化为字符串类型,fmt.Println(string(buf))

json.Unmarshal(buf, &b) //反序列化,将json格式转换为结构体,并传入空的结构体对象中

json.MarshalIndent(a, "", "\t") 转成可读性更好的json字符串

image-20230115175450204

package main

import (
	"encoding/json"
	"fmt"
)

type userInfo struct {
	Name  string
	Age   int `json:"age"` //`json:"age"`将Age字段改为小写
	Hobby []string
}

func main() {
	a := userInfo{Name: "wang", Age: 18, Hobby: []string{"Golang", "TypeScript"}}
	buf, err := json.Marshal(a)		//序列化
	if err != nil {
		panic(err)
	}
	fmt.Println(buf)         // [123 34 78 97...]
	fmt.Println(string(buf)) // {"Name":"wang","age":18,"Hobby":["Golang","TypeScript"]}

	buf, err = json.MarshalIndent(a, "", "\t")   //转成可读性更好的json字符串
	if err != nil {
		panic(err)
	}
	fmt.Println(string(buf))

	var b userInfo
	err = json.Unmarshal(buf, &b)  //反序列化,将json格式转换为结构体,并传入空的结构体对象中
	if err != nil {
		panic(err)
	}
	fmt.Printf("%#v\n", b) // main.userInfo{Name:"wang", Age:18, Hobby:[]string{"Golang", "TypeScript"}}
}

2.18 基础语法-时间处理

time.Now() 获取当前时间

time.Date(2022, 3, 27, 1, 25, 36, 0, time.UTC)构造一个带时区的时间

t2.Sub(t)利用.Sub()对两个时间点做减法,得到一个时间段

t.Format("2006-01-02 15:04:05")格式化时间,得到对应格式的时间字符串;2006-01-02 15:04:05该时间是写在包中的

time.Parse("2006-01-02 15:04:05", "2022-03-27 01:25:36")将一个时间字符串转换成时间,第一个值必须是"2006-01-02 15:04:05",第二个值是要转换的string

now.Unix() //获取当前时间戳

package main

import (
   "fmt"
   "time"
)

func main() {
   now := time.Now()   //最常用的时间处理,获取当前时间
   fmt.Println(now) // 2022-03-27 18:04:59.433297 +0800 CST m=+0.000087933
   t := time.Date(2022, 3, 27, 1, 25, 36, 0, time.UTC)   //构造一个带时区的时间
   t2 := time.Date(2022, 3, 27, 2, 30, 36, 0, time.UTC)
   fmt.Println(t)                                                  // 2022-03-27 01:25:36 +0000 UTC
   fmt.Println(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute()) // 2022 March 27 1 25		//通过构造好时间的对象中.Year(),.Month(),.Day(),.Minute()来获取对应的信息
   fmt.Println(t.Format("2006-01-02 15:04:05"))                    // 2022-03-27 01:25:36		//格式化时间,得到对应格式的时间字符串
   diff := t2.Sub(t)   //利用.Sub("")对两个时间点做减法,得到一个时间段
   fmt.Println(diff)                           // 1h5m0s  
    fmt.Println(diff.Minutes(), diff.Seconds()) // 65 3900   //再调用对象的.Minute(),或者.Seconds()得到多少分钟,多少秒
   t3, err := time.Parse("2006-01-02 15:04:05", "2022-03-27 01:25:36")
    //将一个时间字符串转换成时间,第一个值必须是"2006-01-02 15:04:05",第二个值是要转换的string;
   if err != nil {
      panic(err)
   }
   fmt.Println(t3 == t)    // true
   fmt.Println(now.Unix()) // 1648738080  //now.Unix()获取时间戳
}

2.19 基础语法-数字解析

Golang中关于数字之间的转换都在"strconv"包中

strconv.ParseFloat(字符串,精度)解析字符串,并将字符串转换为浮点型

strconv.ParseInt(字符串,进制,精度)解析字符串,并将字符串转换为整型

strconv.Atoi("123")将字符串转换成数字

package main

import (
   "fmt"
   "strconv"
)

func main() {
   f, _ := strconv.ParseFloat("1.234", 64) //解析字符串,并将字符串转换为浮点型
   fmt.Println(f) // 1.234

   n, _ := strconv.ParseInt("111", 10, 64)  //解析字符串,并将字符串转换为整型
   fmt.Println(n) // 111

   n, _ = strconv.ParseInt("0x1000", 0, 64)
   fmt.Println(n) // 4096

   n2, _ := strconv.Atoi("123")  //将字符串转换成数字
   fmt.Println(n2) // 123

   n2, err := strconv.Atoi("AAA")  //如果字符串不合法,则会返回错误信息
   fmt.Println(n2, err) // 0 strconv.Atoi: parsing "AAA": invalid syntax
}

2.20 基础语法-进程信息

os.Args获取命令行在执行时候的参数

package main

import (
   "fmt"
   "os"
   "os/exec"
)

func main() {
   // go run example/20-env/main.go a b c d
   fmt.Println(os.Args)  	//获取命令行在执行时候的参数         	
    						// [/var/folders/8p/n34xxfnx38dg8bv_x8l62t_m0000gn/T/go-build3406981276/b001/exe/main a b c d]
   fmt.Println(os.Getenv("PATH")) // /usr/local/go/bin...  //获取环境变量
   fmt.Println(os.Setenv("AA", "BB"))					//写入环境变量

   buf, err := exec.Command("grep", "127.0.0.1", "/etc/hosts").CombinedOutput()	//快速启动子进程,并且获取其输入输出
   if err != nil {
      panic(err)
   }
   fmt.Println(string(buf)) // 127.0.0.1       localhost
}

3、实战

3.1 猜谜游戏介绍

image-20230115221243318

随机生成1-100的随机整数,玩家需要输入一个数字,程序会告诉玩家该数字是高于还是低于随机数字,并且让玩家再次猜测,如果猜对就结束程序

3.1.1 猜谜游戏-生成随机数

rand.Intn(maxNum)生成随机数,最大为maxNum

package main

import (
   "fmt"
   "math/rand" //随机数包
)go

func main() {
   maxNum := 100 //定义一个变量
   secretNumber := rand.Intn(maxNum)  //使用rand.Intn(maxNum)生成随机数
   fmt.Println("The secretNumber is", secretNumber)
}

为什么上述代码每次执行都是一样的数字?

需要生成随机数种子rand.Seed(time.Now().UnixNano()) //生成随机数种子,后面加时间戳

3.1.3 猜谜游戏-读取用户输入
fmt.Println("Please input your guess")
reader := bufio.NewReader(os.Stdin)		//1、os.Stdin打开Stdin文件,并使用bufio.NewReader将文件转换为只读的流,就可以操作流
input, err := reader.ReadString('\n')	//2、reader.ReadString('\n')读取这个流的一行
if err != nil {
   fmt.Println("An error occured while reading input. Please try again", err)
   return
}
input = strings.Trim(input, "\r\n")  //3、用strings的Trim()方法将换行符去掉

guess, err := strconv.Atoi(input)	//4、并用strconv.Atoi()将其流转化为数字
if err != nil {
   fmt.Println("Invalid input. Please enter an integer value")
   return
}
fmt.Println("You guess is", guess)	//5、得到用户输入的数字
3.1.4 猜谜游戏-实现判断逻辑
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!")
}
3.1.5猜谜游戏-实现游戏循环

使用for的死循环实现

package main

import (
   "bufio"
   "fmt"
   "math/rand" //随机数包
   "os"
   "strconv"
   "strings"
   "time"
)

func main() {
   //1、生成随机数
   maxNum := 100                     //定义一个变量
   rand.Seed(time.Now().UnixNano())  //生成随机数种子,后面加时间戳
   secretNumber := rand.Intn(maxNum) //使用rand.Intn(maxNum)生成随机数
   //fmt.Println("The secretNumber is", secretNumber)
   
   fmt.Println("Please input your guess")
   reader := bufio.NewReader(os.Stdin)   //1、使用os打开Stdin文件,并使用bufio.NewReader()将Stdin文件转换为流
    //使用for死循环,实现无限制输入
   for {
      input, err := reader.ReadString('\n') //2、reader.ReadString('\n')读取这个流输入的一行
      if err != nil {
         fmt.Println("An error occured while reading input. Please try again", err)
         return
         continue //出错了则执行下一次输入
      }

      input = strings.Trim(input, "\r\n") //3、将读取的流去掉换行符
      guess, err := strconv.Atoi(input)   //4、将流转换为数字,得到用户输入的数字
      if err != nil {
         fmt.Println("Invalid input. Please enter an integer value")
         return
      }
      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 //胜利则跳出循环
      }
   }

}

3.2在线词典介绍

命令行词典,可以在命令行执行的时候,查询一个单词,会返回单词的音标和注释;

原理:调用第三方api查询单词的翻译,并且打印出来;

如何用Go语言发送Http请求?解析JSON?如何使用代码生成来提高开发效率?

image-20230115225330521

3.2.1 抓包

获取对应api信息 fanyi.caiyunapp.com/#/

image-20230115230507638

3.2.2 代码生成

cUrl 代码生成网址 curlconverter.com/#go

image-20230115230735286

package main

import (
   "fmt"
   "io/ioutil"
   "log"
   "net/http"
   "strings"
)

func main() {
    //设置timeout,请求的最大超时时间
   client := &http.Client{}
   var data = strings.NewReader(`msg1=wow&msg2=such&msg3=data`)
    req, err := http.NewRequest("POST", "http://fiddle.jshell.net/echo/html/", data)  //1、创建POST请求,data是一个流
   if err != nil {
      log.Fatal(err)
   }
    
    //2、设置请求头
    
   req.Header.Set("Origin", "http://fiddle.jshell.net")
   // req.Header.Set("Accept-Encoding", "gzip, deflate")
   req.Header.Set("Accept-Language", "en-US,en;q=0.8")
   req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36")
   req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
   req.Header.Set("Accept", "*/*")
   req.Header.Set("Referer", "http://fiddle.jshell.net/_display/")
   req.Header.Set("X-Requested-With", "XMLHttpRequest")
   req.Header.Set("Connection", "keep-alive")
    
    resp, err := client.Do(req)		//3、使用client.Do(),真正发起请求
   if err != nil {
      log.Fatal(err)	//如果发送请求,出现断网等异常,会直接退出进程
   }
   defer resp.Body.Close()		//4、读取响应流
   bodyText, err := ioutil.ReadAll(resp.Body)	//5、读取响应流,并转换为byte数组
   if err != nil {
      log.Fatal(err)
   }
   fmt.Printf("%s\n", bodyText)
}
3.2.3 生成request body
3.2.4 解析response body

JSON结构体的生成:oktools.net/json2go

3.2.5 打印结果
3.2.6 完善代码
package main

import (
   "bytes"
   "encoding/json"
   "fmt"
   "io/ioutil"
   "log"
   "net/http"
   "os"
)

type DictRequest struct {
   TransType string `json:"trans_type"`
   Source    string `json:"source"`
   UserID    string `json:"user_id"`
}

type DictResponse struct {
   Rc   int `json:"rc"`
   Wiki struct {
      KnownInLaguages int `json:"known_in_laguages"`
      Description     struct {
         Source string      `json:"source"`
         Target interface{} `json:"target"`
      } `json:"description"`
      ID   string `json:"id"`
      Item struct {
         Source string `json:"source"`
         Target string `json:"target"`
      } `json:"item"`
      ImageURL  string `json:"image_url"`
      IsSubject string `json:"is_subject"`
      Sitelink  string `json:"sitelink"`
   } `json:"wiki"`
   Dictionary struct {
      Prons struct {
         EnUs string `json:"en-us"`
         En   string `json:"en"`
      } `json:"prons"`
      Explanations []string      `json:"explanations"`
      Synonym      []string      `json:"synonym"`
      Antonym      []string      `json:"antonym"`
      WqxExample   [][]string    `json:"wqx_example"`
      Entry        string        `json:"entry"`
      Type         string        `json:"type"`
      Related      []interface{} `json:"related"`
      Source       string        `json:"source"`
   } `json:"dictionary"`
}

func query(word string) {
   client := &http.Client{}
   request := DictRequest{TransType: "en2zh", Source: word}
   buf, err := json.Marshal(request)
   if err != nil {
      log.Fatal(err)
   }
   var data = bytes.NewReader(buf)
   req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)
   if err != nil {
      log.Fatal(err)
   }
   req.Header.Set("Connection", "keep-alive")
   req.Header.Set("DNT", "1")
   req.Header.Set("os-version", "")
   req.Header.Set("sec-ch-ua-mobile", "?0")
   req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36")
   req.Header.Set("app-name", "xy")
   req.Header.Set("Content-Type", "application/json;charset=UTF-8")
   req.Header.Set("Accept", "application/json, text/plain, */*")
   req.Header.Set("device-id", "")
   req.Header.Set("os-type", "web")
   req.Header.Set("X-Authorization", "token:qgemv4jr1y38jyq6vhvi")
   req.Header.Set("Origin", "https://fanyi.caiyunapp.com")
   req.Header.Set("Sec-Fetch-Site", "cross-site")
   req.Header.Set("Sec-Fetch-Mode", "cors")
   req.Header.Set("Sec-Fetch-Dest", "empty")
   req.Header.Set("Referer", "https://fanyi.caiyunapp.com/")
   req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
   req.Header.Set("Cookie", "_ym_uid=16456948721020430059; _ym_d=1645694872")
   resp, err := client.Do(req)
   if err != nil {
      log.Fatal(err)
   }
   defer resp.Body.Close()
   bodyText, err := ioutil.ReadAll(resp.Body)
   if err != nil {
      log.Fatal(err)
   }
   if resp.StatusCode != 200 {
      log.Fatal("bad StatusCode:", resp.StatusCode, "body", string(bodyText))
   }
   var dictResponse DictResponse
   err = json.Unmarshal(bodyText, &dictResponse)
   if err != nil {
      log.Fatal(err)
   }
   fmt.Println(word, "UK:", dictResponse.Dictionary.Prons.En, "US:", dictResponse.Dictionary.Prons.EnUs)
   for _, item := range dictResponse.Dictionary.Explanations {
      fmt.Println(item)
   }
}

func main() {
   if len(os.Args) != 2 {
      fmt.Fprintf(os.Stderr, `usage: simpleDict WORD
example: simpleDict hello
      `)
      os.Exit(1)
   }
   word := os.Args[1]
   query(word)
}

3.3 SOCKS5代理介绍

image-20230115233042781

SOCKS5协议相当于在防火墙内部开了个口子,让授权的用户可以通过单个端口,可以访问内部的所有资源

image-20230115233311148

crul --socks5 端口 -v url

命令行配置端口和访问的url,会返回对应的端口信息和响应体信息

原理image-20230115233601417

3.3.1 TCP echo server

发送什么就回复什么,用于测试server写的是否正确

net.Listen监听一个端口,返回server

package main

import (
   "bufio"
   "log"
   "net"
)

func main() {
   server, err := net.Listen("tcp", "127.0.0.1:1080")		//1、监听端口,返回server
   if err != nil {
      panic(err)
   }
   for {		
      client, err := server.Accept()  //2、在一个死循环中,利用server.Accept()接受一个请求,成功就返回一个连接client
      if err != nil {
         log.Printf("Accept failed %v", err)
         continue
      }
      go process(client)		//3、go关键字,类似其它语言启动一个子线程处理该连接;go的开销比子线程小很多,可以处理上万个并发
   }
}

func process(conn net.Conn) {
   defer conn.Close()		//4、在函数退出的时候,一定要把这个连接关掉
   reader := bufio.NewReader(conn)	//5、基于连接,创建一个只读的流
   for {//看似是一个字节一个字节的读,实际底层实现会将其合并,在读前一个字节时,已经开始读后面的字节了
      b, err := reader.ReadByte() //6、在死循环中,每次读一个字节
      if err != nil {
         break
      }go
       _, err = conn.Write([]byte{b}) //7、并使用连接的write()方法将读取的字节写入
      if err != nil {
         break
      }
   }
}
3.3.2 auth-协议的第一步认证阶段
package main

import (
   "bufio"
   "fmt"
   "io"
   "log"
   "net"
)

const socks5Ver = 0x05
const cmdBind = 0x01
const atypIPV4 = 0x01
const atypeHOST = 0x03
const atypeIPV6 = 0x04

func main() {
   server, err := net.Listen("tcp", "127.0.0.1:1080")
   if err != nil {
      panic(err)
   }
   for {
      client, err := server.Accept()
      if err != nil {
         log.Printf("Accept failed %v", err)
         continue
      }
      go process(client)
   }
}

func process(conn net.Conn) {
   defer conn.Close()
   reader := bufio.NewReader(conn)
   err := auth(reader, conn)	//删掉死循环,调用auth认证函数
   if err != nil {
      log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
      return
   }
   log.Println("auth success")
}

func auth(reader *bufio.Reader, conn net.Conn) (err error) {  //auth(只读流,原始的tcp连接)
   //发送的报文
   // +----+----------+----------+
   // |VER | NMETHODS | METHODS  |
   // +----+----------+----------+
   // | 1  |    1     | 1 to 255 |
   // +----+----------+----------+
   // VER: 协议版本,socks5为0x05
   // NMETHODS: 支持认证的方法数量
   // METHODS: 对应NMETHODS,NMETHODS的值为多少,METHODS就有多少个字节。RFC预定义了一些值的含义,内容如下:
   // X’00’ NO AUTHENTICATION REQUIRED
   // X’02’ USERNAME/PASSWORD

    //读取报文
   ver, err := reader.ReadByte()	//因为报文版本号是单字节的,所以读取字节
   if err != nil {
      return fmt.Errorf("read ver failed:%w", err)
   }
   if ver != socks5Ver {	//第一个读取的是版本号,判断版本号是否正确
      return fmt.Errorf("not supported ver:%v", ver)//出错,返回错误信息,上面的process函数就会关闭连接
   }
   methodSize, err := reader.ReadByte()	// methodSize也是单字节的,所以读取字节
   if err != nil {
      return fmt.Errorf("read methodSize failed:%w", err)
   }
   method := make([]byte, methodSize)	//利用methodSize创建一个methods的缓冲池
   _, err = io.ReadFull(reader, method)	//利用io.ReadFull()将缓冲池填充
   if err != nil {
      return fmt.Errorf("read method failed:%w", err)
   }
   log.Println("ver", ver, "method", method)	//用日志打印将其出来
   // +----+--------+
   // |VER | METHOD |
   // +----+--------+
   // | 1  |   1    |
   // +----+--------+
    //返回一个包,告诉服务器选择了哪种鉴权方式
    _, err = conn.Write([]byte{socks5Ver, 0x00})	//直接把包构造出来{协议版本号,鉴权方式}
   if err != nil {
      return fmt.Errorf("write failed:%w", err)
   }
   return nil
}
3.3.3 请求阶段
func connect(reader *bufio.Reader, conn net.Conn) (err error) {
    //报文中包含6个字段
   // +----+-----+-------+------+----------+----------+
   // |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
   // +----+-----+-------+------+----------+----------+
   // | 1  |  1  | X'00' |  1   | Variable |    2     |
   // +----+-----+-------+------+----------+----------+
   // VER 版本号,socks5的值为0x05
   // CMD 0x01表示CONNECT请求
   // RSV 保留字段,值为0x00
   // ATYP 目标地址类型,DST.ADDR的数据对应这个字段的类型。
   //   0x01表示IPv4地址,DST.ADDR为4个字节
   //   0x03表示域名,DST.ADDR是一个可变长度的域名
   // DST.ADDR 一个可变长度的值
   // DST.PORT 目标端口,固定2个字节
//挨个读6个字段
   buf := make([]byte, 4)	//1、创建一个长度为4的缓冲池
   _, err = io.ReadFull(reader, buf)	//2、利用io.ReadFull把其填充满,然后就能一次性读取4个字段
   if err != nil {
      return fmt.Errorf("read header failed:%w", err)
   }
    //3、对于每个字段,验证其合法性
   ver, cmd, atyp := buf[0], buf[1], buf[3]
   if ver != socks5Ver {
      return fmt.Errorf("not supported ver:%v", ver)
   }
   if cmd != cmdBind {
      return fmt.Errorf("not supported cmd:%v", ver)
   }
   addr := ""
    //对于atyp有不同的类型,使用switch分支进行选择
   switch atyp {
   case atypIPV4:
      _, err = io.ReadFull(reader, buf)		//把缓冲池填充满
      if err != nil {
         return fmt.Errorf("read atyp failed:%w", err)
      }
       //再一起读取,并打印成ip地址
      addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
   case atypeHOST:
      hostSize, err := reader.ReadByte()	//1、先读一个字节host的长度
      if err != nil {
         return fmt.Errorf("read hostSize failed:%w", err)
      }
      host := make([]byte, hostSize)	//2、再make一个对应长度的字符串
      _, err = io.ReadFull(reader, host)//3、再将其填充满
      if err != nil {
         return fmt.Errorf("read host failed:%w", err)
      }
      addr = string(host)//4、最后转换成一个字符串即可
   case atypeIPV6:
      return errors.New("IPv6: no supported yet")
   default:
      return errors.New("invalid atyp")
   }
    //读端口号,2个字节
   _, err = io.ReadFull(reader, buf[:2])//1、复用前面长度为4的缓冲池,利用切片语法,将其切成一个长度为2的缓冲池;把其填充满
   if err != nil {
      return fmt.Errorf("read port failed:%w", err)
   }
   port := binary.BigEndian.Uint16(buf[:2])//因为新的切片缓冲池是复用的,所以能够读到端口数据;再利用binary.BigEndian.Uint16(buf[:2])按照大纲数字解析出来端口号

   log.Println("dial", addr, port)

   // +----+-----+-------+------+----------+----------+
   // |VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
   // +----+-----+-------+------+----------+----------+
   // | 1  |  1  | X'00' |  1   | Variable |    2     |
   // +----+-----+-------+------+----------+----------+
   // VER socks版本,这里为0x05
   // REP Relay field,内容取值如下 X’00’ succeeded
   // RSV 保留字段
   // ATYPE 地址类型
   // BND.ADDR 服务绑定的地址
   // BND.PORT 服务绑定的端口DST.PORT
   _, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})//打包对应数据
   if err != nil {
      return fmt.Errorf("write failed: %w", err)
   }
   return nil
3.3.4 relay阶段
package main

import (
   "bufio"
   "context"
   "encoding/binary"
   "errors"
   "fmt"
   "io"
   "log"
   "net"
)

const socks5Ver = 0x05
const cmdBind = 0x01
const atypIPV4 = 0x01
const atypeHOST = 0x03
const atypeIPV6 = 0x04

func main() {
   server, err := net.Listen("tcp", "127.0.0.1:1080")
   if err != nil {
      panic(err)
   }
   for {
      client, err := server.Accept()
      if err != nil {
         log.Printf("Accept failed %v", err)
         continue
      }
      go process(client)
   }
}

func process(conn net.Conn) {
   defer conn.Close()
   reader := bufio.NewReader(conn)
   err := auth(reader, conn)
   if err != nil {
      log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
      return
   }
   err = connect(reader, conn)
   if err != nil {
      log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
      return
   }
}

func auth(reader *bufio.Reader, conn net.Conn) (err error) {
   // +----+----------+----------+
   // |VER | NMETHODS | METHODS  |
   // +----+----------+----------+
   // | 1  |    1     | 1 to 255 |
   // +----+----------+----------+
   // VER: 协议版本,socks5为0x05
   // NMETHODS: 支持认证的方法数量
   // METHODS: 对应NMETHODS,NMETHODS的值为多少,METHODS就有多少个字节。RFC预定义了一些值的含义,内容如下:
   // X’00’ NO AUTHENTICATION REQUIRED
   // X’02’ USERNAME/PASSWORD

   ver, err := reader.ReadByte()
   if err != nil {
      return fmt.Errorf("read ver failed:%w", err)
   }
   if ver != socks5Ver {
      return fmt.Errorf("not supported ver:%v", ver)
   }
   methodSize, err := reader.ReadByte()
   if err != nil {
      return fmt.Errorf("read methodSize failed:%w", err)
   }
   method := make([]byte, methodSize)
   _, err = io.ReadFull(reader, method)
   if err != nil {
      return fmt.Errorf("read method failed:%w", err)
   }

   // +----+--------+
   // |VER | METHOD |
   // +----+--------+
   // | 1  |   1    |
   // +----+--------+
   _, err = conn.Write([]byte{socks5Ver, 0x00})
   if err != nil {
      return fmt.Errorf("write failed:%w", err)
   }
   return nil
}

func connect(reader *bufio.Reader, conn net.Conn) (err error) {
   // +----+-----+-------+------+----------+----------+
   // |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
   // +----+-----+-------+------+----------+----------+
   // | 1  |  1  | X'00' |  1   | Variable |    2     |
   // +----+-----+-------+------+----------+----------+
   // VER 版本号,socks5的值为0x05
   // CMD 0x01表示CONNECT请求
   // RSV 保留字段,值为0x00
   // ATYP 目标地址类型,DST.ADDR的数据对应这个字段的类型。
   //   0x01表示IPv4地址,DST.ADDR为4个字节
   //   0x03表示域名,DST.ADDR是一个可变长度的域名
   // DST.ADDR 一个可变长度的值
   // DST.PORT 目标端口,固定2个字节

   buf := make([]byte, 4)
   _, err = io.ReadFull(reader, buf)
   if err != nil {
      return fmt.Errorf("read header failed:%w", err)
   }
   ver, cmd, atyp := buf[0], buf[1], buf[3]
   if ver != socks5Ver {
      return fmt.Errorf("not supported ver:%v", ver)
   }
   if cmd != cmdBind {
      return fmt.Errorf("not supported cmd:%v", ver)
   }
   addr := ""
   switch atyp {
   case atypIPV4:
      _, err = io.ReadFull(reader, buf)
      if err != nil {
         return fmt.Errorf("read atyp failed:%w", err)
      }
      addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
   case atypeHOST:
      hostSize, err := reader.ReadByte()
      if err != nil {
         return fmt.Errorf("read hostSize failed:%w", err)
      }
      host := make([]byte, hostSize)
      _, err = io.ReadFull(reader, host)
      if err != nil {
         return fmt.Errorf("read host failed:%w", err)
      }
      addr = string(host)
   case atypeIPV6:
      return errors.New("IPv6: no supported yet")
   default:
      return errors.New("invalid atyp")
   }
   _, err = io.ReadFull(reader, buf[:2])
   if err != nil {
      return fmt.Errorf("read port failed:%w", err)
   }
   port := binary.BigEndian.Uint16(buf[:2])
    //建立TCP连接,利用net.Dial()建立,使用fmt.Sprintf("%v:%v", addr, port)往tcp连接中加入url和端口
   dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port))
   if err != nil {
      return fmt.Errorf("dial dst failed:%w", err)
   }
   defer dest.Close()//建立连接,如果没有出错,在函数结束的第一时间关闭连接
   log.Println("dial", addr, port)
   

   // +----+-----+-------+------+----------+----------+
   // |VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
   // +----+-----+-------+------+----------+----------+
   // | 1  |  1  | X'00' |  1   | Variable |    2     |
   // +----+-----+-------+------+----------+----------+
   // VER socks版本,这里为0x05
   // REP Relay field,内容取值如下 X’00’ succeeded
   // RSV 保留字段
   // ATYPE 地址类型
   // BND.ADDR 服务绑定的地址
   // BND.PORT 服务绑定的端口DST.PORT
   _, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
   if err != nil {
      return fmt.Errorf("write failed: %w", err)
   }
    
   ctx, cancel := context.WithCancel(context.Background())//5、使用WithCancel创建context
   defer cancel()
    
 //1、建立浏览器和服务器之间的双向数值转换
    //2、启动两个go ruti,都调用io.copy()
    //问题:启动go 是不耗时间的,已启动就会直接返回,函数连接也就关闭了?
   	//需要等待任何一个方向的copy失败,代表某一方关闭连接了,此时才终止整个连接
   go func() {
      _, _ = io.Copy(dest, reader)	//3、从用户的浏览器拷贝数值到底层服务器
      cancel()
   }()
   go func() {
      _, _ = io.Copy(conn, dest)	//4、从底层服务器拷贝数值到用户的浏览器
      cancel()
   }()
   <-ctx.Done()//6、等待ctx执行的完成,等待的时期也就是cancel等待被调用的时期
    //任何一个连接copy失败,就会调用cancel(),最后把双向的连接给关闭掉
   return nil
}