走进Go语言基础语法|青训营笔记

123 阅读11分钟

简介

Go(又称Golang)是Google开发的一种静态强类型 、编译型、并发型,并具有垃圾回收功能的编程语言。

罗伯特·格瑞史莫(Robert Griesemer),罗布·派克(Rob Pike)及肯·汤普逊(Ken Thompson)于2007年9月开始设计Go,稍后Ian Lance Taylor、Russ Cox加入项目。Go是基于Inferno操作系统所开发的。Go于2009年11月正式宣布推出,成为开放源代码项目,并在Linux及Mac OS X平台上进行了实现,后来追加了Windows系统下的实现。在2016年,Go被软件评价公司TIOBE 选为“TIOBE 2016 年最佳语言”。 目前,Go每半年发布一个二级版本(即从a.x升级到a.y)。

Go语言具有以下特点:

  1. 高性能、高并发
  2. 语法简单、学习曲线平缓
  3. 丰富的标准库
  4. 完善的工具链
  5. 静态编译
  6. 支持跨平台
  7. 拥有垃圾回收机制

入门

环境

  1. 第一步:安装Golang

访问Golang官网,点击download即可下载安装Golang,如果网络不稳定,可以尝试访问Go语言中文网进行下载。

如果访问github的速度非常慢的话,可以配置go mod proxy,打开Goproxy按照提示操作即可,配置完成后下载第三方包的速度会大大加快。

  1. 第二步:配置集成开发环境

GoLand是JetBrains公司推出的一款针对Go语言开发的IDE(集成开发环境),基于IntelliJ平台开发。它提供了丰富的功能和工具,帮助开发人员在使用Go语言开发应用程序时提高效率。

GoLand的功能包括代码自动完成、调试器、测试运行、版本控制等等。其中,代码自动完成功能可以帮助开发人员更快速、更准确地编写代码,调试器可以帮助开发人员找出程序中的错误和问题,测试运行工具可以帮助开发人员进行单元测试和性能测试,版本控制工具可以帮助开发人员管理代码库和版本。

此外,GoLand还提供了各种插件和工具,可以帮助开发人员更好地开发和调试Go语言应用程序。例如,GoLand可以集成各种常用的构建工具和包管理器,如go build、go get和dep,还可以支持Docker、Kubernetes和AWS等云服务。

下载完毕之后注意检查GOROOT环境:

我们也可以使用在线编辑器调试代码,访问gitpod.io/ ,根据提示登录并创建项目,即可进行编码:

也可以直接访问由课程提供的项目地址go-by-example调试代码:

如果不是因为电脑上没有环境,个人不是很推荐使用在线编辑器,很卡顿😥

语法

  1. Hello, world
// 包声明
package main

// 导入fmt包,fmt 包实现了格式化IO(输入/输出)的函数
import "fmt"

func main() {
   // 将字符串输出到控制台,并在最后自动增加换行字符\n
   fmt.Println("Hello, world!!")
}
  1. 变量

Go语言是—门强类型语言,每一个变量都有它自己的变量类型。常见的变量类型包括字符串整型、浮点型、布尔型等。

func main() {

   // 1.指定类型声明变量,若不赋值则为默认值
   var a int
   fmt.Println(a) // 0

   // 2.类型推导,根据值自行判定变量类型
   var b = 1
   fmt.Println(b) // 1

   // 3.类型推导,可省略var关键字
   c := 1
   fmt.Println(c) //1

   // 4.一次性声明多个变量
   d, e := 1, 2
   fmt.Println(d, e) // 1,2

   // 5.const常量声明,可类型推导
   const f = "const"
   fmt.Println(f) //const
}

整数类型:

类型符号空间范围
int81字节-128~127
int162字节-2^15~2^15-1
int324字节-2^31~2^31-1
int648字节-2^63~2^63-1
uint81字节0~255
uint162字节0~2^16-1
uint324字节0~2^32-1
uint648字节0~2^64-1

其他类型:

类型符号空间范围备注
int32位系统4个字节 64位系统8个字节-2^31~2^31-1或-2^63~2^63-1
uint32位系统4个字节 64位系统8个字节0~2^32-1或0~2^64-1
rune与int32一样-2^31~2^31-1等价 int32,表示 一个 Unicode码
byte与uint8等价0~255当要存储字符时 选用byte

浮点类型:

类型空间范围
单精度float324字节-3.403e38~3.403e38
双精度float648字节-1.798e38~1.798e308

字符类型:

Golang 中没有专门的字符类型,如果要存储单个字符(字母),一般使用byte来保存

字符串就是一串固定长度的字符连接起来的字符序列。Go的字符串是由单个字节连接起来的。也就是说对于传统的字符串是由字符组成的,而Go的字符串不同,它是由字节组成的。

布尔类型:

布尔类型也叫 bool类型,bool类型数据只允许取值 true和 false,bool类型占1个字节,bool类型适于逻辑运算,一般用于程序流程控制。

字符串类型:

字符串就是一串固定长度的字符连接起来的字符序列。Go的字符串是由单个字节连接起来的。Go语言的字符串的字节使用UTF-8编码标识 Unicode文本。

  1. if else

Golang中的条件判断有两个特点:

  • 条件表达式不需要带括号
  • 可在if判断中声明变量,变量作用域为if块级作用域
package main

import "fmt"

func main() {
   // if条件判断,不需要带括号
   if 7%2 == 0 {
      fmt.Println("7 is even")
   } else {
      fmt.Println("7 is odd")
   }

   // if条件判断,可声明变量
   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")
   }
}
  1. 循环

Golang里面没有while 循环、do while循环,只有唯一的一种for循环,通过使用for循环可以实现while、do while循环的效果。

package main

import "fmt"

func main() {

   // for循环
   for j := 7; j < 9; j++ {
      fmt.Println(j)
   }

   // for+continue
   for n := 0; n < 5; n++ {
      if n%2 == 0 {
         continue
      }
      fmt.Println(n)
   }

   // while循环效果
   i := 1
   for i <= 3 {
      fmt.Println(i)
      i = i + 1
   }
}
  1. switch

Golang中的switch不需要显示添加break;switch中的条件变量可以为空,因此可以替代复杂的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")
   }

}
  1. 数组

Golang中的数组和其他语言中的数组特性基本一致。

package main

import "fmt"

func main() {

   // 定义长度为5数组
   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)
}
  1. 切片

切片是Golang中的动态数组,可以自动扩容。我们可以通过append函数添加元素,注意需要将append的结果赋值为原数组。

我们还可以通过len获取切片长度,copy深拷贝数组,还能对数组进行切片操作。

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]
}
  1. map

Golang的map是完全无序的,遍历的时候不会按照字母顺序,也不会按照插入顺序输出,而是随机顺序。

package main

import "fmt"

func main() {
   // 创建一个map
   m := make(map[string]int)
   m["one"] = 1
   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

   // 获取key为"unknow"的值,ok代表是否存在
   r, ok := m["unknow"]
   fmt.Println(r, ok) // 0 false

   // 删除key为"one"的键值对
   delete(m, "one")

   // 创建并初始化map
   m2 := map[string]int{"one": 1, "two": 2}
   var m3 = map[string]int{"one": 1, "two": 2}
   fmt.Println(m2, m3)
}
  1. range

对于一个sice或者一个map的话,我们可以用range来快速遍历,这样代码能够更加简洁。range遍历的时候,对于数组会返回两个值

  • 第一个是索引
  • 第二个是对应位置的值

如果我们不需要索引的话,我们可以用下划线来忽略。

package main

import "fmt"

func main() {
   nums := []int{2, 3, 4}
   sum := 0
   // 变量nums数组,i为下标,num为元素值
   for i, num := range nums {
      sum += num
      if num == 2 {
         fmt.Println("index:", i, "num:", num) // index: 0 num: 2
      }
   }
   fmt.Println(sum) // 9

   // 变量nums数组,忽略下标
   for _, num := range nums {
      fmt.Println(num)
   }

   m := map[string]string{"a": "A", "b": "B"}
   // 遍历m哈希表,k为键,v为值
   for k, v := range m {
      fmt.Println(k, v) // b 8; a A
   }

   // 遍历m哈希表,忽略v值
   for k := range m {
      fmt.Println("key", k) // key a; key b
   }
}
  1. 函数

Golang里面的函数原生支持返回多个值。在实际的业务逻辑代码里面几乎所有的函数都返回两个值,第一个是真正的返回结果,第二个值是一个错误信息

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
}
  1. 指针

使用指针可以对参数的真实值进行修改,如果是结构比较大的情况下,传递指针还能减少参数值的拷贝,因为指针大小根据不同的操作系统是固定的。

package main

import "fmt"

func add2(n int) {
   n += 2
}

func add2ptr(n *int) {
   *n += 2
}

func main() {
   n := 5
   add2(n)
   fmt.Println(n) // 5
   add2ptr(&n)
   fmt.Println(n) // 7
}
  1. 结构体

结构体是由一系列具有相同类型或不同类型的数据构成的数据集合,Golang中的结构体有以下特点:

  • 值传递
  • 自动解引用,可通过指针名.成员名访问
  • 如果是多级指针需要手动解引用
package main

import "fmt"

type user struct {
   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
}
  1. 结构体方法

结构体可以定义方法,在实现结构体的方法的时候也有两种写法,一种是带指针,一种是不带指针,如果带指针的话,那么就可以对结构体属性进行修改。如果不带指针的活,实际上操作的是一个拷贝,则无法对结构体进行修改。

package main

import "fmt"

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
}
  1. 错误处理

错误处理在go语言里面符合语言习惯的做法就是使用一个单独的返回值来传递错误信息。

package main

import (
   "errors"
   "fmt"
)

type user struct {
   name     string
   password string
}

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

func main() {
   u, err := findUser([]user{{"wang", "1024"}}, "wang")
   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)
   }
}
  1. 字符串操作

在标准库strings包里面有很多常用的字符串工具函数,

package main

import (
   "fmt"
   "strings"
)

func main() {
   a := "hello"
   // 是否包含子串
   fmt.Println(strings.Contains(a, "ll")) // true
   // 统计子字符串数量
   fmt.Println(strings.Count(a, "l")) // 2
   // 是否以he前缀开头
   fmt.Println(strings.HasPrefix(a, "he")) // true
   // 是否以llo后缀结尾
   fmt.Println(strings.HasSuffix(a, "llo")) // true
   // 第一个出现ll子串的位置
   fmt.Println(strings.Index(a, "ll")) // 2
   // 使用-连接字符串
   fmt.Println(strings.Join([]string{"he", "llo"}, "-")) // he-llo
   // 重复n字符串count次
   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 := "你好"
   // 中文字符一个字符3个字节
   fmt.Println(len(b)) // 6
}
  1. 字符串格式化

fmt包下提供各种各样的输入输出格式化:

%v        值的默认格式表示
%+v        类似%v,但输出结构体时会添加字段名
%#v        值的Go语法表示
%T        值的类型的Go语法表示
%%        百分号

代码示例:

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
}
  1. JSON处理

Go语言里面的JSON操作非常简单,对于一个已有的结构体,我们可以什么都不做,只要保证每个字段的第一个字母是大写也就是是公开字段。那么这个结构体就能用JSON.marshaler去序列化,变成—个JSON的字符串。

序列化之后的字符串也能够用JSON.unmarshaler去反序列化到一个空的变量里面。这样默认序列化出来的字符串风格是大写字母开头,而不是下划线。我们可以在后面用json tag等语法来去修改输出JSON结果里面的字段名。

package main

import (
   "encoding/json"
   "fmt"
)

type userInfo struct {
   Name  string
   Age   int `json:"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")
   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"}}
}
  1. 时间处理

time包提供了时间的显示和测量用的函数。日历的计算采用的是公历。

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
   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
}
  1. 数字解析

strconv包实现了基本数据类型和其字符串表示的相互转换。

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
}
  1. 进程信息

os包提供了操作系统函数的不依赖平台的接口。设计为Unix风格的,虽然错误处理是go风格的;失败的调用会返回错误值而非错误码。通常错误值里包含更多信息。例如,如果某个使用一个文件名的调用(如Open、Stat)失败了,打印错误时会包含该文件名,错误类型将为*PathError,其内部可以解包获得更多信息。

os包的接口规定为在所有操作系统中都是一致的。非公用的属性可以从操作系统特定的syscall包获取。

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
}

总结:

Golang语法较为简单,可以参考官方文档,也可查看中文文档翻译

项目

猜谜游戏

功能:

程序随机生成一个范围为[0,100)的数字n,用户通过键盘输入的方式猜测此数字,设猜测的数字为a

  • 若a小于n,则程序提示用户猜的数字太小
  • 若a大于n,则程序提示用户猜的数字太大
  • 若a等于n,则程序提示用户猜的数字正确

代码:

package main

import (
   "bufio"
   "fmt"
   "math/rand"
   "os"
   "strconv"
   "strings"
   "time"
)

func main() {
   // 生成数字的上限(不包含)
   maxNum := 100
   // 设置当前时间为随机种子
   rand.Seed(time.Now().UnixNano())
   // 生成[0,100)的随机数字
   secretNumber := rand.Intn(maxNum)
   // 输入提示语句
   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
      }

   }
}

总结:

  • rand包实现了伪随机数生成器。随机数从源生成。包水平的函数都使用的默认的公共源。该源会在程序每次运行时都产生确定的序列。如果需要每次运行产生不同的序列,应使用Seed函数进行初始化。默认源可以安全的用于多go程并发。
  • 用户输入可以使用bufio包中的Reader,Reader具有默认缓冲,可以从传入参数读取数据。
  • Reader的ReadString函数读取直到第一次遇到delim字节,返回一个包含已读取的数据和delim字节的字符串。因此我们需要手动去除delim字符。在一般程序开发中,我们也会对用户输入进行处理,包括前后空白字符,输入格式校验等等,这是一个很好的编程习惯。
  • 为什么在处理字符串时需要去除"\r\n",我查了一下,windows中的enter键包括两种字符,回车和换行,也就对应着"\r\n",回车指的是将光标移动到当前行的最左边,换行指的是移动到下一行。
  • 程序具有一定的容错能力,当用户输入错误的数字,例如输入一个字符串,程序不会因此而崩溃,而是提醒用户重新输入,这也启示我们在日常开发过程中需要考虑各种失败场景以及应对措施。
  • 用户可以根据程序的提示一步一步地猜到正确数字,想要最快地猜到数字用户可以使用二分思想,每次猜中间的数字,这样能以最快的速度逼近目标数字。

在线词典

功能:

程序运行后,用户可以在命令行里面查询单词,程序能通过调用第三方的API查询到单词的翻译并打印出来。

代码:

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
   client := &http.Client{}
   // 封装请求参数
   request := DictRequest{TransType: "en2zh", Source: word}
   // 序列化请求参数
   buf, err := json.Marshal(request)
   if err != nil {
      log.Fatal(err)
   }
   // 使用reader封装buf
   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")
   // 发送请求,获取response
   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
   // 将返回值反序列化到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]
   // 调用api查询单词翻译
   query(word)
}

总结:

  • 翻译接口可以使用彩云小译,具体接口为https://api.interpreter.caiyunai.com/v1/dict
  • 请求与相应的代码比较通用,可以使用curlconverter生成请求与响应的代码,此网站还支持c、c#、java、c++等主流语言。
  • json结构复杂可以通过oktools来生成为go的结构体,只需要json文件即可。
  • 这里我们使用了os包来获取程序启动的参数。

代理服务器

功能:

我们来写一个socks5代理服务器,对于大家来说,一提到代理服务器,第一想到的是翻墙。不过很遗憾的是,socks5协议它虽然是代理协议,但它并不能用来翻墙,它的协议都是明文传输。

这个协议历史比较久远,诞生于互联网早期。它的用途是,比如某些企业的内网为了确保安全性,有很严格的防火墙策略,但是带来的副作用就是访问某些资源会很麻烦。socks5相当于在防火墙开了个口子,让授权的用户可以通过单个端口去访问内部的所有资源。实际上很多翻墙软件,最终暴露的也是一个socks5协议的端口.

如果有同学开发过爬虫的话,就知道,在爬取过程中很容易会遇到P访问频率超过限制。这个时候很多人就会去网上找一些代理IP池,这些代理P池里面的很多代理的协议就是socks5。

我们启动这个程序,然后在浏览器里面配置使用这个代理,此时我们打开网页。代理服务器的日志,会打印出访问的网站的域名或者IP,这说明我们的网络流量是通过这个代理服务器的。

我们也能在命令行去测试我们的代理服务器。我们可以用curl+socks5+代理服务器地址,后面加一个可访问的URL(例如 curl --socks5 127.0.0.1:1080 -v ``http://www.qq.com),如果代理服务器工作正常的话,那么 curl命令就会正常返回。

socks5协议交互流程RFC官方文档SOCKS Protocol Version 5,以下为socks5简易版交互流程如下:

image.png

  1. 握手阶段,客户端向代理服务器发出请求信息,用以协商版本和认证方法。随后代理服务器应答,将选择的方法发送给客户端。
  2. 认证阶段,客户端和代理服务器进入由选定认证方法所决定的子协商过程。
  3. 请求阶段,客户端发送请求信息,其中包含目标服务器的IP地址和端口。
  4. relay阶段,代理服务器验证客户端身份,通过后会与目标服务器连接,目标服务器经过代理服务器向客户端返回状态响应。

认证阶段client发送的数据格式:

+----+----------+----------+
|VER | NMETHODS | METHODS  |
+----+----------+----------+
| 1  |    1     | 1 to 255 |
+----+----------+----------+
VER: 协议版本,socks5为0x05
NMETHODS: 支持认证的方法数量
METHODS: 对应NMETHODS,NMETHODS的值为多少,METHODS就有多少个字节。RFC预定义了一些值的含义,内容如下:
    1.0x00: 不需要认证
    2.0x01: GSSAPI认证
    3.0x02: 用户名和密码方式认证
    4.0x03: IANA认证
    5.0x80-0xfe: 保留的认证方式
    6.0xff: 不支持任何认证方式,当客户端收到此信息必须关闭连接。

认证阶段socks5 server需要返回的数据格式:

+----+--------+
|VER | METHOD |
+----+--------+
| 1  |   1    |
+----+--------+

请求阶段client发送的数据格式:

+----+-----+-------+------+----------+----------+
|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的数据对应这个字段的类型。
  1.0x01表示IPv4地址,DST.ADDR为4个字节
  2.0x03表示域名,DST.ADDR是一个可变长度的域名
DST.ADDR 一个可变长度的值
DST.PORT 目标端口,固定2个字节

请求阶段socks5 server需要返回数据格式:

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

我们可以根据client与socks5 server交互的数据格式处理具体逻辑,包括解析数据,请求转发,返回值封装等等逻辑。

代码:

package main

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

// socks5版本信息
const socks5Ver = 0x05

// 请求类型
const cmdBind = 0x01

// IPV4地址类型
const atypeIPV4 = 0x01

// 域名类型
const atypeHOST = 0x03

// IPV6地址类型
const atypeIPV6 = 0x04

func main() {
   // 创建服务器,监听1080端口
   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
   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)
   // 读取数据到method中
   _, err = io.ReadFull(reader, method)
   if err != nil {
      return fmt.Errorf("read method failed:%w", err)
   }

   //+----+--------+
   //|VER | METHOD |
   //+----+--------+
   //| 1  |   1    |
   //+----+--------+

   // 写回0x00,不需要鉴权
   _, 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)
   // 读取前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", cmd)
   }
   addr := ""
   // 针对不同的地址类型做不同处理
   switch atyp {
   case atypeIPV4:
      _, 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:
      //b := make([]byte, 16)
      //_, err = io.ReadFull(reader, b)
      //if err != nil {
      // return fmt.Errorf("read atyp failed:%w", err)
      //}
      //addr = fmt.Sprintf("%d.%d.%d.%d.%d.%d.%d.%d", b[0], b[1], b[2], b[3],
      // b[4], b[5], b[6], b[7])

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

   // 转换为uint16类型
   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)

   //+----+-----+-------+------+----------+----------+
   //|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())
   defer cancel()

   // 使用context.WithCancel使得connect立即返回
   go func() {
      _, _ = io.Copy(dest, reader)
      fmt.Println("请求数据")
      cancel()
   }()
   go func() {
      _, _ = io.Copy(conn, dest)
      fmt.Println("返回数据")
      cancel()
   }()

   <-ctx.Done()
   return nil
}

总结:

  • windows使用nc命令需要下载软件,访问此链接下载,下载完毕后解压并配置path环境变量,使用命令行时需要以管理员方式运行,使用nc -nv ip port命令连接程序。
  • socks5协议交互流程RFC官方文档SOCKS Protocol Version 5,可以阅读原文参考一下。

思考:

  • 为什么io.copy(dest, reader)与io.Copy(conn, dest)都需要放在协程中执行,而不直接同步执行?
_, _ = io.Copy(dest, reader)
_, _ = io.Copy(conn, dest)

总的来说,TCP是双向的流量,需要处理两个方向的数据流动,我们不知道哪个方向会先传递数据,因此需要将至少一个io.copy操作放在协程中处理,否则程序会处于阻塞状态。

  • 为什么需要任意一个io.Copy()结束后,才返回connect函数?
//使用context.WithCancel使得connect立即返回
go func() {
   _, _ = io.Copy(dest, reader)
}()
go func() {
   _, _ = io.Copy(conn, dest)
}()

如果直接异步执行的话,connect函数返回之后,在process函数中将调用defer conn.Close()关闭连接,有可能数据还没有返回conn就已经关闭了,造成错误。

作业

作业一

内容:

修改猜谜游戏的最终代码,使用fmt.Scanf来简化代码实现

实现:

查看fmt中的Scanf函数:

Scanf从标准输入扫描文本,根据format参数指定的格式将成功读取的空白分隔的值保存进成功传递给本函数的参数。返回成功扫描的条目个数和遇到的任何错误。

也就是说个可以根据给定的format对输入进行格式化,那么我们就可以考虑不用手动强制类型转换,而借助Scanf进行类型转换。需要注意的是,我们要在format后加一个"\r\n",因为输入时会按enter键。

代码如下:

package main

import (
   "fmt"
   "math/rand"
   "time"
)

func main() {
   // 生成数字的上限(不包含)
   maxNum := 5
   //maxNum := 100
   // 设置当前时间为随机种子
   rand.Seed(time.Now().UnixNano())
   // 生成[0,100)的随机数字
   secretNumber := rand.Intn(maxNum)
   // 输入提示语句
   fmt.Println("Please input your guess")
   for {
      var guess int
      // 使用fmt读入数据
      _, err := fmt.Scanf("%d\r\n", &guess)
      // 如果出错,报错并继续下一个循环
      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
      }

   }
}

比较奇怪的是format为"%d\r\n"、"%d\r"、"%d\n"都可以,我查的资料是windows的enter键对应的应该是"\r\n",不太理解上述现象。

作业二

内容:

修改在线词典的最终代码,增加另一种翻译引擎的支持并在此基础上,修改代码实现并行请求两个翻译引擎来提高响应速度

实现:

我们再加一个百度翻译的实现,接口为https://fanyi.baidu.com/sug,我们使用curlconverter生成请求与响应的代码,使用oktools生成response的结构体。

定义函数统一的返回值:

type Result struct {
   Source string `json:"source"` //来源
   Data   string `json:"data"` // 翻译内容
}

使用协程与channel实现并行请求:

c := make(chan Result, 1)

go func() {
   // 调用彩云小译查询单词翻译
   c <- queryByCaiYun(word)
}()

go func() {
   // 调用百度翻译查询单词翻译
   c <- queryByBaidu(word)
}()
result := <-c

fmt.Println("翻译来源为:" + result.Source)
fmt.Println("翻译内容为:\n" + result.Data)

完整代码如下:

package main

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

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

type AutoGenerated struct {
   Errno int `json:"errno"`
   Data  []struct {
      K string `json:"k"`
      V string `json:"v"`
   } `json:"data"`
}

type Result struct {
   Source string `json:"source"`
   Data   string `json:"data"`
}

func queryByCaiYun(word string) Result {
   // 创建client
   client := &http.Client{}
   // 封装请求参数
   request := DictRequest{TransType: "en2zh", Source: word}
   // 序列化请求参数
   buf, err := json.Marshal(request)
   if err != nil {
      log.Fatal(err)
   }
   // 使用reader封装buf
   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")
   // 发送请求,获取response
   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
   // 将返回值反序列化到DictResponse中
   err = json.Unmarshal(bodyText, &dictResponse)
   if err != nil {
      log.Fatal(err)
   }
   return Result{
      Source: "彩云小译",
      Data:   strings.Join(dictResponse.Dictionary.Explanations, "\n"),
   }
}

func queryByBaidu(word string) Result {
   client := &http.Client{}
   var data = strings.NewReader(`kw=` + word)
   req, err := http.NewRequest("POST", "https://fanyi.baidu.com/sug", data)
   if err != nil {
      log.Fatal(err)
   }
   req.Header.Set("Accept", "application/json, text/javascript, */*; q=0.01")
   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("Connection", "keep-alive")
   req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
   req.Header.Set("Cookie", "PSTM=1642161902; BIDUPSID=382C8BFFD304885234562CEAA2452566; __yjs_duid=1_741d7178e0ac644c236038177c9f9f221642166330484; BDUSS=pCcjJOdExkYkFqZ35CRX5CczN6Z0h3dzZSMHVHQ0pDdTlPVlAwVmIzTE94TjFqSVFBQUFBJCQAAAAAAAAAAAEAAAA3eGivwejAxzUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM43tmPON7ZjZF; APPGUIDE_10_0_2=1; BDUSS_BFESS=pCcjJOdExkYkFqZ35CRX5CczN6Z0h3dzZSMHVHQ0pDdTlPVlAwVmIzTE94TjFqSVFBQUFBJCQAAAAAAAAAAAEAAAA3eGivwejAxzUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM43tmPON7ZjZF; BAIDUID=D6F55F292CE6200979818B78A25EC269:FG=1; REALTIME_TRANS_SWITCH=1; FANYI_WORD_SWITCH=1; HISTORY_SWITCH=1; SOUND_SPD_SWITCH=1; SOUND_PREFER_SWITCH=1; MCITY=-132%3A; Hm_lvt_64ecd82404c51e03dc91cb9e8c025574=1682424449; BDORZ=B490B5EBF6F3CD402E515D22BCDA1598; BDSFRCVID=Sg4OJexroG076I7fDLSWbiE3neKK0PQTDYLEOwXPsp3LGJLVFjgyEG0PtjjhfxA-oxe4ogKKKmOTH1uF_2uxOjjg8UtVJeC6EG0Ptf8g0M5; H_BDCLCKID_SF=tJPqoCtKtCvDqTrP-trf5DCShUFsW6KOB2Q-XPoO3KJAfhC6bxOdytAVMt_e2tQiW279oMbgylRp8P3y0bb2DUA1y4vp5bjr0mTxoUJ2fIoYKtJJqtoIhj_ebPRiB-r9Qg-qahQ7tt5W8ncFbT7l5hKpbt-q0x-jLTnhVn0MBCK0hC-CDj0-DjPVKgTa54cbb4o2WbCQLfTO8pcN2b5oQT8l5-FfKf3rQTbaBlKbaJ5vOpOODpOUWfAkXpJvQnJjt2JxaqRCK-odEq5jDh3Me5DIqq3me4QGaR6y0hvcWb6cShPCyUjrDRLbXU6BK5vPbNcZ0l8K3l02V-bIe-t2XjQhDHt8JTtqtb3aQ5rtKRTffjrnhPF3ytAmXP6-hnjy3b4f_qI55qbdSKQPMf_5h4LUyN3TQl3RymJ42-39LPO2hpRjyxv4Q4Is2toxJpOJ2DQ30tnpHR7WeDbvbURvyPug3-AD-x5dtjTO2bc_5KnlfMQ_bf--QfbQ0hOhqP-jBRIEoC0XtI8-hIvPKITD-tFO5eT22-usQ5vW2hcHMPoosIt6jxubbx0T-RnaaxLeaDTiaKJxLxbUoqRHXnJi0btQDPvxBf7p3DQMXl5TtUJMjRcdbR7Nqt4bbHOyKMniWKj9-pPhblQrh459XP68bTkA5bjZKxtq3mkjbPbDfn028DKuDjtBD5JBDNRabK6aKC5bL6rJabC3Vn0mXU6q2bDeQN0ftfFf5enrbM3t2lK5DR5oyT3m0q0vWtv4WbbvLT7johRTWqR4sRcT2fonDh83Mx63KJvCHCOO0CbO5hvvOn6O3MAa5fKmDloOW-TB5bbPLUQF5l8-sq0x0bOte-bQXH_E5bj2qRIO_I_y3f; H_PS_PSSID=38516_36557_38672_38610_38597_38380_38486_38414_38636_26350_38624_38681_38546; BA_HECTOR=2k2l84a4812l0l218g218ge11i69ou51m; delPer=0; PSINO=3; BAIDUID_BFESS=D6F55F292CE6200979818B78A25EC269:FG=1; BDSFRCVID_BFESS=Sg4OJexroG076I7fDLSWbiE3neKK0PQTDYLEOwXPsp3LGJLVFjgyEG0PtjjhfxA-oxe4ogKKKmOTH1uF_2uxOjjg8UtVJeC6EG0Ptf8g0M5; H_BDCLCKID_SF_BFESS=tJPqoCtKtCvDqTrP-trf5DCShUFsW6KOB2Q-XPoO3KJAfhC6bxOdytAVMt_e2tQiW279oMbgylRp8P3y0bb2DUA1y4vp5bjr0mTxoUJ2fIoYKtJJqtoIhj_ebPRiB-r9Qg-qahQ7tt5W8ncFbT7l5hKpbt-q0x-jLTnhVn0MBCK0hC-CDj0-DjPVKgTa54cbb4o2WbCQLfTO8pcN2b5oQT8l5-FfKf3rQTbaBlKbaJ5vOpOODpOUWfAkXpJvQnJjt2JxaqRCK-odEq5jDh3Me5DIqq3me4QGaR6y0hvcWb6cShPCyUjrDRLbXU6BK5vPbNcZ0l8K3l02V-bIe-t2XjQhDHt8JTtqtb3aQ5rtKRTffjrnhPF3ytAmXP6-hnjy3b4f_qI55qbdSKQPMf_5h4LUyN3TQl3RymJ42-39LPO2hpRjyxv4Q4Is2toxJpOJ2DQ30tnpHR7WeDbvbURvyPug3-AD-x5dtjTO2bc_5KnlfMQ_bf--QfbQ0hOhqP-jBRIEoC0XtI8-hIvPKITD-tFO5eT22-usQ5vW2hcHMPoosIt6jxubbx0T-RnaaxLeaDTiaKJxLxbUoqRHXnJi0btQDPvxBf7p3DQMXl5TtUJMjRcdbR7Nqt4bbHOyKMniWKj9-pPhblQrh459XP68bTkA5bjZKxtq3mkjbPbDfn028DKuDjtBD5JBDNRabK6aKC5bL6rJabC3Vn0mXU6q2bDeQN0ftfFf5enrbM3t2lK5DR5oyT3m0q0vWtv4WbbvLT7johRTWqR4sRcT2fonDh83Mx63KJvCHCOO0CbO5hvvOn6O3MAa5fKmDloOW-TB5bbPLUQF5l8-sq0x0bOte-bQXH_E5bj2qRIO_I_y3f; ZFY=rPcXLWl:BL8eVPMRimMcVzKK9915SBmKmTGAkHq5jl3I:C; Hm_lpvt_64ecd82404c51e03dc91cb9e8c025574=1684379140; ab_sr=1.0.1_ZDRlODBhY2VmYjQ0NTMyNmJlNDQzMGNiMTUzNzRmNDc0ODFlM2ExYzZjN2E3MmZjNzBhYWIwMmM1M2I2YTQ4NzVkZjRiNzY2MjM3ZjM0MGVkYTI2Y2QxOWMxOTgwMGE1NTcxMGQ1YTdmYzE4N2Q2NmFkOWQ3M2JkYTE3ZGNkMGI4YmJhNDgzODQxZTEwMzA1OTE4MjYwMmQzNjc0NThkMjgxMTZiMThjOTNmMTI0YWZkYTgyMDAxNDgyNzcyYTI3")
   req.Header.Set("Origin", "https://fanyi.baidu.com")
   req.Header.Set("Referer", "https://fanyi.baidu.com/?aldtype=16047")
   req.Header.Set("Sec-Fetch-Dest", "empty")
   req.Header.Set("Sec-Fetch-Mode", "cors")
   req.Header.Set("Sec-Fetch-Site", "same-origin")
   req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.35")
   req.Header.Set("X-Requested-With", "XMLHttpRequest")
   req.Header.Set("sec-ch-ua", `"Microsoft Edge";v="113", "Chromium";v="113", "Not-A.Brand";v="24"`)
   req.Header.Set("sec-ch-ua-mobile", "?0")
   req.Header.Set("sec-ch-ua-platform", `"Windows"`)
   resp, err := client.Do(req)
   if err != nil {
      log.Fatal(err)
   }
   defer resp.Body.Close()
   bodyText, err := io.ReadAll(resp.Body)
   if err != nil {
      log.Fatal(err)
   }
   var autoGenerated AutoGenerated
   err = json.Unmarshal(bodyText, &autoGenerated)
   if err != nil {
      log.Fatal(err)
   }
   stringSlice := make([]string, len(autoGenerated.Data))
   for _, d := range autoGenerated.Data {
      stringSlice = append(stringSlice, d.K+":"+d.V)
   }
   if len(autoGenerated.Data) == 0 {
      return Result{
         Source: "百度翻译",
         Data:   "未查找到相关词汇",
      }
   }
   return Result{
      Source: "百度翻译",
      Data:   autoGenerated.Data[0].V,
   }

}

func main() {
   // 获取启动参数
   if len(os.Args) != 2 {
      fmt.Fprintf(os.Stderr, `usage: simpleDict WORD
example: simpleDict hello
      `)
      os.Exit(1)
   }
   // 获取需要查询的单词
   word := os.Args[1]

   c := make(chan Result, 1)

   go func() {
      // 调用彩云小译查询单词翻译
      c <- queryByCaiYun(word)
   }()

   go func() {
      // 调用百度翻译查询单词翻译
      c <- queryByBaidu(word)
   }()
   result := <-c

   fmt.Println("翻译来源为:" + result.Source)
   fmt.Println("翻译内容为:\n" + result.Data)
}

学习路线

image.png