ycgg的GO语言之路Day01 ——GO基础语法与小项目| 青训营笔记

70 阅读16分钟

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

Go Day01

1.1什么是Go语言

image-20230115214939089.png

  • 使用标准库即可开发高性能 、高并发应用程序(标准库功能十分强大,稳定)
  • 基于C语言,且比C语言容易
  • 循环只有for,遍历循环较为容易
  • 适配多系统,树莓派等
  • 垃圾回收,无须考虑内存释放

实例:

image-20230115215322666.png

简单的静态页面的代码

2.1开发环境

直接在golang官网安装,或者下载vsc编辑器后下载GO插件

goproxy.cn网址按照教程配置第三方包,可以提高依赖的下载速度,该网站还有自托管 Go 模块代理的配置教程

golang学生可以免费申请使用,具体操作和idea免费申请类似

2.2.1基础语法--hello world!

package main
​
import (
    "fmt"
)
​
func main() {
    fmt.Println("hello world")
}
​

package main表示该文件属于main包的一部分,mian包即程序的入口包,该文件即程序的入口文件

导入标准库的fmt包:主要往屏幕输入输出字符串,格式化字符串(该操作类似C的导入头文件)

func main即主函数,调用fmt的println输出

运行该程序使用go run命令

生成二进制文件使用go build命令

鼠标放在Println上可以链接跳转包的官方文档,查看其他方法的使用方法,类似idea

2.2.2基础语法--变量

字符串是内置类型,类似string,可以直接用 + 拼接,也可以用 = 去比较两个字符串

变量的声明:

var 【name】 = 【value】

比如: var a = "initial"

会根据value自动匹配变量的类型,如果想直接确定类型,可以在【name】后直接将类型名标注

比如: var b, c int = 1, 2

或者

【变量名】 := 【value】

声明常量时,将var改为const

可以声明多个类型不同的变量(类型由初始化表达式推导):

var i, j, k int                 // int, int, int
var b, f, s = true, 2.3, "four" // bool, float64, string
package main
​
import (
    "fmt"
    "math"
)
​
func main() {
​
    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)                // initialappleconst s string = "constant"
    const h = 500000000
    const i = 3e20 / h
    fmt.Println(s, h, i, math.Sin(h), math.Sin(i))
}

2.2.3 基础语法 -- if else

和C基本类似

不同:

if和else if后没有条件判断的括号(小括号)且必须有花括号括住

package main
​
import "fmt"
​
func main() {
​
    if 7%2 == 0 {
        fmt.Println("7 is even")
    } else {
        fmt.Println("7 is odd")
    }
​
    if 8%4 == 0 {
        fmt.Println("8 is divisible by 4")
    }
​
    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.2.4 基础语法 -- 循环

go 语言只有for循环一种

for后面什么都不写即代表死循环

可以使用经典的C语言的for循环,格式相同,同样也有continue和break的跳出循环方式

以下是集中for循环的使用方法

package main
​
import "fmt"func main() {
​
    i := 1
    for {
        fmt.Println("loop")
        break
    }
    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.2.5 基础语法 -- switch

go语言中Switch后面的变量名和if一样不需要括号

和C语言不同的是,C语言case语句后没有break默认走完所有case,

而go语言默认是不走的

在go中Switch还有更加高级的用法,可以在Switch后不加任何的变量名,而 在case后添加条件判断,可以代替if语句使条件的选择更加清晰,代码更加美观

package main
​
import (
    "fmt"
    "time"
)
​
func main() {
​
    a := 2
    switch a {
    case 1:
        fmt.Println("one")
    case 2:
        fmt.Println("two")
    case 3:
        fmt.Println("three")
    case 4, 5:
        fmt.Println("four or five")
    default:
        fmt.Println("other")
    }
​
    t := time.Now()
    switch {
    case t.Hour() < 12:
        fmt.Println("It's before noon")
    default:
        fmt.Println("It's after noon")
    }
}
​

2.2.6 基础语法 -- 数组,切片

1.普通数组

和C语法和使用方法基本相同

var a [5]int
a[4] = 100
b := [5]int{1, 2, 3, 4, 5}
var twoD [2][3]int
for i := 0; i < 2; i++ {
    for j := 0; j < 3; j++ {
        twoD[i][j] = i + j
    }
}

2.切片

切片是一个可变长度的数组,类似java的list集合,可以随时追加元素

切片使用make创建,用append进行追加,需要注意追加后要返回切片(会自动进行扩容并返回新的长度)

使用append函数追加元素可以一次性追加多个元素,甚至可以直接追加一个切片

同样可以用copy函数拷贝切片

go也有类似Python的切片操作

fmt.Println(s[2:5]) // [c d e],打印2~5(不包括5)

package main
​
import "fmt"func main() {
​
    s := make([]string, 3)
    s[0] = "a"
    s[1] = "b"
    s[2] = "c"
    fmt.Println("get:", s[2])   // c
    fmt.Println("len:", len(s)) // 3
​
    s = append(s, "d")
    s = append(s, "e", "f")
    fmt.Println(s) // [a b c d e f]
​
    c := make([]string, len(s))
    copy(c, s)
    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.2.7 基础语法 -- map

使用过程中用到的最多的数据结构

  • 同样,使用make创建
m := make(map[string]int)
m["one"] = 1
m["two"] = 2

其中string是key的类型,int是value的类型

  • 初始化一些map中的值
ages := map[string]int{
    "alice":   31,
    "charlie": 34,
}
  • 使用delete去删除
delete(m, "one")
  • 在使用时可以在变量后面加一个ok来获取对应key索引的value是否存在如果存在变量为value,ok为true,否则,变量为对应类型的零值,ok为false
r, ok := m["unknow"]
fmt.Println(r, ok) // 0 false
  • map是无序的,多次迭代会得到不同的结果

2.2.8 基础语法 -- range

  • 一个迭代工具,可以快速遍历数组,切片,map等,类似java的foreach快速遍历

  • 在迭代时可以同时输出对应的key和value

  • 如果不需要索引可以用下划线free

    package main
    ​
    import "fmt"func main() {
        nums := []int{2, 3, 4}
        sum := 0
        for i, num := range nums {
            sum += num
            if num == 2 {
                fmt.Println("index:", i, "num:", num) // index: 0 num: 2
            }
        }
        fmt.Println(sum) // 9
    ​
        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.2.9 基础语法 -- 函数

  • go语言的函数形参列表和参数类型与C语言位置相反,类型写在形参的后面
  • go语言函数的返回值可以是多个,并且在开发时通常返回一个本该返回的值而另一个为错误信息(类似于状态码,message)
package main
​
import "fmt"func add(a int, b int) int {
    return a + b
}
​
func add2(a, b int) int {
    return a + b
}
​
func exists(m map[string]string, k string) (v string, ok bool) {
    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.2.10 基础语法 -- 指针

  • go语言也有指针,其使用形式大致和Cpp相同,但功能相对简单,一般用于函数间传参时改变变量的值,传参时用&,解引用时用*
  • 但要注意的是,go语言是不支持指针运算的,如果想进行指针运算,需要引入特殊包,直接对内存进行操作
func add2ptr(n *int) {
    *n += 2
}
​
func main() {
    n := 5
    add2(n)
    fmt.Println(n) // 5
    add2ptr(&n)
    fmt.Println(n) // 7

2.2.11 基础语法 -- 结构体

  • 结构体的使用和C语言类似,创建结构体:
type user struct {
    name     string
    password string
}
  • 结构体变量的赋值可以用key:value的方式,有时key也可以省略不写,

也可以只对结构体中的部分变量进行赋值

    a := user{name: "wang", password: "1024"}
    b := user{"wang", "1024"}
    c := user{name: "wang"}
    c.password = "1024"
  • 结构体也可以作为形参类型,但需要注意只有指针结构体才能改变结构体变量的值
  • 结构体还有结构体方法,类似于内成员变量,使用方法为在func后加 结构体变量名 结构体名
  • 同样在此处如果要改变变量的值,使用的函数的参数必须为指针变量

具体实例代码如下:

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
}

2.2.12 基础语法 -- 错误处理

错误,一种类似java中的异常却不完全相同的类型

  • go语言通常将错误作为返回值
  • 不同于java的异常,go的错误会清楚的显示错误发生的行与列,并能通过简单的if else语句初期错误
func findUser(users []user, name string) (v *user, err error) {
    for _, u := range users {
        if u.name == name {
            return &u, nil
        }
    }
    return nil, errors.New("not found")
}

例如,在参数列表中加入err类型,如果返回正常,error即返回nil,否则,new一个错误并返回

  • 在主函数中同样也要有变量来接收err的值,并主函数中对错误进行相应的处理以保证程序的稳定性
func main() {
    u, err := findUser([]user{{"wang", "1024"}}, "wang")
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(u.name) // wangif u, err := findUser([]user{{"wang", "1024"}}, "li"); err != nil {
        fmt.Println(err) // not found
        return
    } else {
        fmt.Println(u.name)
    }
}
​

2.2.13 基础语法 -- 字符串操作

在go语言的strings包里包含了许多有关于字符串的操作,需要使用时导包即可

  • Contains:查找是否包含某一字符串,返回布尔值
  • Count:统计某个字符串在原字符串中出现的次数
  • Index:定位
  • Join:将一个字符串拼接到另一个字符串后面
  • Repeat:重复多次字符串
  • len:内置函数,统计字符串中的字符个数,主义中文可能一个字对应多个字符
    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.2.14 基础语法 -- 字符串格式化

  • 在标准库fmt下就有字符串格式化函数
  • 常见的有Println,打印并换行
  • 还有熟悉的C语言中的Printf,其语法和C语言类似,比较方便的一点是,go语言只需要%v即可输出任意类型数据
  • 可以用%+v , %#v来得到更加详细的结构
    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}
  • 当然,如果需要打印一定精度的浮点数,也可以用%.nf的方式控制
    f := 3.141592653
    fmt.Println(f)          // 3.141592653
    fmt.Printf("%.2f\n", f) // 3.14

2.2.15 基础语法 -- JSON处理

  • 定义结构体时,所有变量用大写
  • 给结构体赋值后,用Marshal进行序列化,或者用MarshalIndent函数产生整齐缩进的序列化
  • 输出时,用string做强制类型转换,否则输出为一串16进制编码
  • 也可以用Unmarshal函数进行反序列化,将json数据解码为字节切片
  • 如果在输出时想让输出与结构体成员名不同,需要在结构体声明时在对应结构体成员后加tag标签进行标注
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")
    if err != nil {
        panic(err)
    }
    fmt.Println(string(buf))
​
    var b userInfo
    err = json.Unmarshal(buf, &b)
    if err != nil {
        panic(err)
    }
    fmt.Printf("%#v\n", b) // main.userInfo{Name:"wang", Age:18, Hobby:[]string{"Golang", "TypeScript"}}

2.2.16 基础语法 -- 时间处理

  • 时间的处理要引入time包
  • 通过time.Now()获取当前时间
  • 通过time.Date去构造一个带时区的时间
  • 构造完成后可以用【变量名】.Year , 【变量名】.Month 等等去获取时间中的年月日时分秒
  • Sub函数将两个时间相减获得到时间差 用.Minutes(),.Seconds()可以将时间差转换为分,秒
  • 时间格式化,必须将"2006-01-02 15:04:05"放入,否则不成功

这里有两种格式化方法:

t.Format("2006-01-02 15:04:05")
t3, err := time.Parse("2006-01-02 15:04:05", "2022-03-27 01:25:36")
  • 在系统交互时,可以用.Unix()去获取一个时间戳

    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
        fmt.Println(t.Format("2006-01-02 15:04:05"))                    // 2022-03-27 01:25:36
        diff := t2.Sub(t)
        fmt.Println(diff)                           // 1h5m0s
        fmt.Println(diff.Minutes(), diff.Seconds()) // 65 3900
        t3, err := time.Parse("2006-01-02 15:04:05", "2022-03-27 01:25:36")
        if err != nil {
            panic(err)
        }
        fmt.Println(t3 == t)    // true
        fmt.Println(now.Unix()) // 1648738080
    }
    

2.2.17 基础语法 -- 数字解析

  • 对于字符串和数字的转化解析,需要引入strconv包
  • 使用ParseFloat将字符串转化为浮点数,64代表精度
f, _ := strconv.ParseFloat("1.234", 64)
fmt.Println(f) // 1.234
  • 使用ParseInt将字符串转化为整数,第二个参数代表进制,如果为0代表自动推测,64代表精度
n, _ := strconv.ParseInt("111", 10, 64)
fmt.Println(n) // 111
​
n, _ = strconv.ParseInt("0x1000", 0, 64)
fmt.Println(n) // 4096
  • 使用Atoi进行快速转化,也可以Itoa转化
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.2.18 基础语法 -- 进程信息

要导入对应的os,os/exec包

os.Args:获取当前进程的命令行参数,命令行参数包括了程序路径本身,以及通常意义上的参数。 程序中os.Args的类型是 []string

os.Getenv:检索由键命名的环境变量的值。它返回值,如果变量不存在,该值将为空。

os.Setenv: 函数可以设置名为 key 的环境变量,如果出错会返回该错误。

// 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猜数字游戏

游戏介绍:每次游戏程序会自动生成一个0~100的随机数,玩家输入自己猜的数字,程序会告诉玩家你猜的数字大于或小于正确答案,直到用户输入正确的答案为止(具体流程如图)

image-20230116183051954.png

开始实战:

1.首先去生成随机数

这里要导入math/rand包,用maxNum声明并变量赋值,并调用rand包下的Intn函数产生随机数(最大为maxNum)

func main() {
   maxNum := 100
   secretNumber := rand.Intn(maxNum)
   fmt.Println("The secret number is ", secretNumber)
}

但是运行后出现了问题,每次运行都会显示随机数为81

image-20230116184012647.png

原因是没有设置随机数的种子,导致每次产生的随机数相同

解决方法:通常用时间戳来初始化随机数种子

修改代码如下:

func main() {
   maxNum := 100
   rand.Seed(time.Now().UnixNano())
   secretNumber := rand.Intn(maxNum)
   fmt.Println("The secret number is ", secretNumber)
}

修改后每次运行都会产生不同的随机数结果

2.接收用户的输入并输出

go接收输入可以通过scanf这种比较简单的方式来实现,此处为后续项目的学习做准备,使用较麻烦的一种方式

首先new一个reader,通过reader的ReadString方法去读一个字符串

    reader := bufio.NewReader(os.Stdin)
    input, err := reader.ReadString('\n')

注意使用ReadString方法得到的字符串再结尾会有一个换行符,要通过strings包中的Trim方法消去换行

input, err := reader.ReadString('\n')

得到一个数字字符串后,在利用前面的Atoi方法将字符串转换为数字

最后输出用户的输入进行检验

代码如下:

    fmt.Println("Please input your guess")
    reader := bufio.NewReader(os.Stdin)
    input, err := 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")
​
    guess, err := strconv.Atoi(input)
    if err != nil {
        fmt.Println("Invalid input. Please enter an integer value")
        return
    }
    fmt.Println("You guess is", guess)
​

此处要注意包的引入,要包含我们用到的函数,并且每次对数据进行处理时都要进行 必要的错误校验,如果发生异常要进行相应处理。

3.逻辑的判断与循环

  • 逻辑 的判断使用if else语句比较即可
  • 需要注意的是,整个用户输入,提示语的输出应该是在大循环内的,以保证游戏是一直玩下去的
  • 当用户输入了正确答案的时候,要break退出循环
  • 用户在输入,程序处理代码时如果出现错误,不是卡死退出,而要continue跳出该回合,用户继续游戏

游戏完整代码如下:

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
      }
   }
}

3.2 在线命令行词典

在命令行输入一个单词会输出这个单词的音标,词性和释义,具体如下图

1.初步尝试

首先打开一个彩云小译的网页,输入要翻译的单词good,点击翻译后,打开网页开发者工具

选择network(网络),找到一个请求方法为POST的 dict,从负载和预览中可以看到一些请求的详细信息,我们在用golang开发时,也要使用对应的api

image-20230116204242503.png

image-20230116205610308.png

image-20230116205620310.png

这种请求代码一般比较复杂,这里介绍一种可以代码生成的方法,首先右键dict选择复制----copy as cURL

打开代码生成网址:curlconverter.com/#go

image-20230116210457571.png

将我们刚刚复制的一串代码输入到上方curl command(bash),会自动生成代码,我们将其复制到编辑器,代码解析参考注释

package main
​
import (
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "strings"
)func main() {
    client := &http.Client{}//创建一个httpclient,此处可以指定最大请求时间
    var data = strings.NewReader(`{"trans_type":"en2zh","source":"good"}`)
    req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)//创建http请求,参数:methodurldata(此处用了流,避免数据过大占用太多内存),因此上面用strings将字符串转换为流
    if err != nil {
        log.Fatal(err)
    }
    req.Header.Set("authority", "api.interpreter.caiyunai.com")
    req.Header.Set("accept", "application/json, text/plain, */*")
    req.Header.Set("accept-language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6")
    req.Header.Set("app-name", "xy")
    req.Header.Set("content-type", "application/json;charset=UTF-8")
    req.Header.Set("device-id", "")
    req.Header.Set("origin", "https://fanyi.caiyunapp.com")
    req.Header.Set("os-type", "web")
    req.Header.Set("os-version", "")
    req.Header.Set("referer", "https://fanyi.caiyunapp.com/")
    req.Header.Set("sec-ch-ua", `"Not?A_Brand";v="8", "Chromium";v="108", "Microsoft Edge";v="108"`)
    req.Header.Set("sec-ch-ua-mobile", "?0")
    req.Header.Set("sec-ch-ua-platform", `"Windows"`)
    req.Header.Set("sec-fetch-dest", "empty")
    req.Header.Set("sec-fetch-mode", "cors")
    req.Header.Set("sec-fetch-site", "cross-site")
    req.Header.Set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 Edg/108.0.1462.76")
    req.Header.Set("x-authorization", "token:qgemv4jr1y38jyq6vhvi")
    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)
    }
    fmt.Printf("%s\n", bodyText)//打印出最后的json字符串
}

client := &http.Client{}//创建一个httpclient,此处可以指定最大请求时间

var data = strings.NewReader({"trans_type":"en2zh","source":"good"})

req, err := http.NewRequest("POST", "api.interpreter.caiyunai.com/v1/dict", data)//创建http请求,参数:method,url,data(此处用了流,避免数据过大占用太多内存),因此上面用strings将字符串转换为流

resp, err := client.Do(req)//发起请求

defer resp.Body.Close()//关闭返回的流

bodyText, err := ioutil.ReadAll(resp.Body)//把流读到内存中变成byte数组

fmt.Printf("%s\n", bodyText)//打印出最后的json字符串

2.生成request body

完成上述代码可以得到一串json数据,但good是固定的,我们需要到用json序列化

首先根据上节基础语法的知识,先构建一个json结构体

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

然后new一个结构体变量并赋值,并用json序列化请求在将其转化为byte数组

request := DictRequest{TransType: "en2zh", Source: "good"}
buf, err := json.Marshal(request)
var data = bytes.NewReader(buf)

接下来就和之前一样创建请求等等

req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)

代码如下:

type DictRequest struct {
    TransType string `json:"trans_type"`
    Source    string `json:"source"`
    UserID    string `json:"user_id"`
}
​
func main() {
    client := &http.Client{}
    request := DictRequest{TransType: "en2zh", Source: "good"}
    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)
    }
    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)
    }
    fmt.Printf("%s\n", bodyText)
}
​

到此为止,修改后的代码运行结果与刚才应该是完全相同的,都生成了一串json的字符串

3.解析response body

首先打开(oktools.net/json2go),该网站可以实现json到go的结构体转化

将刚刚翻译出dict的响应处代码复制到网站的json框,可以得到一个超大的go语言结构体

(此处不展示)

然后去修改刚刚的代码,把原本最后的直接打印json串修改为反序列化到我们刚刚获得的结构体中,并用最详细的%#v去打印出来

var dictResponse DictResponse
err = json.Unmarshal(bodyText, &dictResponse)
if err != nil {
   log.Fatal(err)
}
fmt.Printf("%#v\n", dictResponse)

4.打印结果

拿到结果后其实有很多返回内容是我们不需要的,只需要音标,词性和释义即可,可以从结构体中找到这几个东西把他们打印出来

fmt.Println(word, "UK:", dictResponse.Dictionary.Prons.En, "US:", dictResponse.Dictionary.Prons.EnUs)
for _, item := range dictResponse.Dictionary.Explanations {
   fmt.Println(item)
}

5.代码完善

(1)对响应码进行判断,如果不为200,说明可能发生了错误,会导致后续反序列化为空

if resp.StatusCode != 200 {
   log.Fatal("bad StatusCode:", resp.StatusCode, "body", string(bodyText))
}

(2)主函数

刚刚仅仅是对一个单词的查询,可以将刚刚写好的函数作为功能函数query,再去编写主函数,将good变成一个变量传入,这样就可以任意查词了

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 代理

什么是Socks5

socks5协议是一款广泛使用的代理协议,它在使用TCP/IP协议通讯的前端机器和服务器机器之间扮演一个中介角色,使得内部网中的前端机器变得能够访问Internet网中的服务器,或者使通讯更加安全。SOCKS5 服务器通过将前端发来的请求转发给真正的目标服务器, 模拟了一个前端的行为。在这里,前端和SOCKS5之间也是通过TCP/IP协议进行通讯,前端将原本要发送给真正服务器的请求发送给SOCKS5服务器,然后SOCKS5服务器将请求转发给真正的服务器。

socks5协议交互过程

第一步,客户端向代理服务器发送代理请求,其中包含了代理的版本和认证方

握手完成后,客户端要把需要执行的操作指令发给客户端,表明自己要执行代理的请求

客户端发完上面的请求连接后,服务端会发起连接到DST.ADDR:DST.PORT,然后返回响应到客户端

当连接建立后,客户端就可以和正常一样访问服务端通信了,此时通信的数据除了目的地址是发往代理程序以外,所有内容都是和普通连接一模一样。对代理程序而言,后面所有收到的来自客户端的数据都会原样转发到服务读端。

image-20230116223455961.png

1.TCP echo server

首先写一个主函数去监听一个端口,再在一个死循环中去接受一个请求,如果成功会返回一个连接,使用go关键字去在process函数中处理这个连接,go可以类比为开启一个子线程

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)
   }
}

process函数的实现

首先创建一个流,再在死循环中去每次读一个字节,并写入slice,如果出错就关闭连接

func process(conn net.Conn) {
   defer conn.Close()
   reader := bufio.NewReader(conn)
   for {
      b, err := reader.ReadByte()
      if err != nil {
         break
      }
      _, err = conn.Write([]byte{b})
      if err != nil {
         break
      }
   }
}

使用nc命令去测试

nc 127.0.0.1:1080

输入什么,返回什么

2.auth

修改process死循环中的读写,改为调用auth函数去读报文获取认证方式,并返回方式

// +----+----------+----------+
// |VER | NMETHODS | METHODS  |
// +----+----------+----------+
// | 1  |    1     | 1 to 255 |
// +----+----------+----------+
  • 0x00: 不需要认证
  • 0x01: GSSAPI认证
  • 0x02: 用户名和密码方式认证
  • 0x03: IANA认证
  • 0x80-0xfe: 保留的认证方式
  • 0xff: 不支持任何认证方式
func auth(reader *bufio.Reader, conn net.Conn) (err error) {
   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)
   }
   log.Println("ver", ver, "method", method)
​
   _, err = conn.Write([]byte{socks5Ver, 0x00})
   if err != nil {
      return fmt.Errorf("write failed:%w", err)
   }
   return nil
}

3.请求阶段

在proces函数中调用connection函数去读取代理请求信息

// +----+-----+-------+------+----------+----------+
// |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
// +----+-----+-------+------+----------+----------+
// | 1  |  1  | X'00' |  1   | Variable |    2     |
// +----+-----+-------+------+----------+----------+

创建缓冲区读取六个字段

  • VER: 代理版本信息

  • CMD
    

    : 代理指令

    • 0x01: connect指令,tcp代理时使用。
    • 0x02: bind,很少使用,类似FTP协议中主动连接场景,服务端后服务端会主动连接到客户端。
    • 0x03: udp代理时使用。
  • RSV: 保留字段

  • ATYP
    

    : 地址类型

    • 0x01: IPv4地址类型
    • 0x03: unix域socket类型代理
    • 0x04: IPv6地址类型
  • DST.ADDR: 需要连接的目的地址

  • DST.PORT: 需要连接的目的端口

获取到的六个字段,地址和端口号不用,全部填为0

具体代码如下

func connect(reader *bufio.Reader, conn net.Conn) (err error) {
   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])
​
   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
}

4.relay阶段建立TCP连接

使用net包下Dial函数去选择对应地址和端口建立TCP连接

建立连接后没有出错,关闭流

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()

双向数据转化,从浏览器拷贝到底层服务器,在从底层服务器拷贝到浏览器,并且开启一个ctx,有一方关闭后返回

ctx, cancel := context.WithCancel(context.Background())
go func() {
   _, _ = io.Copy(dest, reader)
   cancel()
}()
go func() {
   _, _ = io.Copy(conn, dest)
   cancel()
}()
<-ctx.Done()

最终代码如下:

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 = 0x04func 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, 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)
   }
​
   _, 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) {
​
​
   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])
​
   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)
​
​
   _, 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())
   defer cancel()
​
   go func() {
      _, _ = io.Copy(dest, reader)
      cancel()
   }()
   go func() {
      _, _ = io.Copy(conn, dest)
      cancel()
   }()
​
   <-ctx.Done()
   return nil
}

完成后调命令测试,会出现详细的代理信息

curl --socks5 127.0.0.1:1080 -v http: //www.qq.com