Day1 Go语言基础|青训营笔记

96 阅读22分钟

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

1.配置Go语言环境

1.1 Windows

配置步骤

  • 下载安装文件
  • 修改环境变量路径
  • 校验配置结果

1.1.1 下载安装

在官网下载目前最新的安装程序。

golang.google.cn

image-20230113072637144

image-20230113072520291

下载之后会得到一个.msi后缀的安装文件,点击安装之后可以自行选择安装的路径。然后一路Next即可。

image-20230113072910491


1.1.2 配置修改(可选非必须)

通过.msi的方式安装的一个方便之处就是安装完成之后会默认配置好系统环境变量。由于变量的路径默认是在C盘用户目录下的,如果需要可以手动进行修改。

image-20230113073814826

修改之后:

image-20230113074125813


1.1.3 检测配置结果

打开命令行黑窗口输入下面的命令检测是否安装和配置成功。

go version

image-20230113074334504

出现上述的显示结果,表示成功在自己的计算机上安装并配置好了Go语言的运行环境。


1.1.4 目录结构

  • src存储go的源代码
  • pkg存储编译之后生成的包文件
  • bin存储生成的可执行文件

2.Go开发工具

2.1下载&安装

这里使用Goland作为Go语言的额开发工具。

官方下载地址:www.jetbrains.com.cn/go/

直接选择.exe文件进行下载,安装过程比较简单,默认安装路径在C盘,建议修改,其他的没什么特别的,一路按照提示安装即可。

2.2 配置&使用

打开Goland之后,选择创建一个新的项目,等待软件加载结束之后配置自己go的安装目录也就是SDK位置。

image-20230113093543438

配置全局PATH,选择go的环境变量位置。

image-20230113093642679

配置代码格式化规范。

image-20230113093818101


选择File->Go File创建一个go程序。

package main
import "fmt"
func mian() {
    /*万恶之源:hello world*/
    fmt.Println("hello world!")
}

如何运行?

  • 点击IDE内的三角形绿色图标直接运行。
  • 在命令行输入go run first.go运行。
  • 使用命令go build first.go编译为二进制文件
  • ./first

image-20230113094332837


2.3 一些插件

觉得不错的几个插件。

  • CodeGlance3 代码缩略图
  • ideaVim Vim的IDE集成
  • GitToolBox git 管理增强
  • Rainbow Brackets 五颜六色的花括号
  • ChatGPT 不用我解释了吧

3.Go语言基础

3.1 Hello World

Go是一种由Google开发的编程语言。它是一种静态类型、编译型、并发型语言。Go语言被设计成简单易用,并且具有高效的编译运行性能。Go语言的设计目标是使其成为一种适用于网络和多核计算机的系统编程语言。简单总结一下就是:

  • 高性能、高并发
  • 语法简单、学习曲线平缓
  • 丰富得标准库
  • 完善得工具链
  • 静态链路
  • 快速编译
  • 跨平台
  • 垃圾回收

诸如此类,目前国内外很多大厂已经全面拥抱或者正在拥抱了Go。

image-20230115130534298

说了概念,按照编程语言得学习规矩,还是得放一段经典的Hello World!示例,直观简单的感受一下!

package main
​
import "fmt"func main() {
    /*万恶之源:hello world*/
    fmt.Println("hello world!")
}

Go语言的代码通过(package)组织,包类似于其它语言里的库(libraries)或者模块(modules)。一个包由位于单个目录下的一个或多个.go源代码文件组成,目录定义包的作用。每个源文件都以一条package声明语句开始,这个例子里就是package main,表示该文件属于哪个包,紧跟着一系列导入(import)的包,之后是存储在这个文件里的程序语句。

Go的标准库提供了100多个包,以支持常见功能,如输入、输出、排序以及文本处理。比如fmt包,就含有格式化输出、接收输入的函数。Println是其中一个基础函数,可以打印以空格间隔的一个或多个值,并在最后添加一个换行符,从而输出一整行。

main包比较特殊。它定义了一个独立可执行的程序,而不是一个库。在main里的main 函数 也很特殊,它是整个程序执行时的入口(译注:C系语言差不多都这样)。main函数所做的事情就是程序做的。当然了,main函数一般调用其它包里的函数完成很多工作(如:fmt.Println)。

必须告诉编译器源文件需要哪些包,这就是跟随在package声明后面的import声明扮演的角色。hello world例子只用到了一个包,大多数程序需要导入多个包。

必须恰当导入需要的包,缺少了必要的包或者导入了不需要的包,程序都无法编译通过。这项严格要求避免了程序开发过程中引入未使用的包(译注:Go语言编译过程没有警告信息,争议特性之一)。注意,如果使用Goland来编写go,那么工具会根据你写的代码来自动为你导入所需要的包。

就这短短的几行,我以为我在写Java、C、javascript的结合体哈哈哈!


3.2 变量

在Go中,可以使用var关键字来给变量命名,基本格式如下:

var 变量名 变量类型  =  表达式
---------------------------------
var name string = "字节跳动"
var age int  = 18

也可以在一个声明语句中同时声明一组变量,或用一组初始化表达式声明并初始化一组变量。如果省略每个变量的类型,将可以声明多个类型不同的变量(类型由初始化表达式推导)

var b, f, s = true, 2.3, "four"

除此之外,还支持一种简短的变量声明方式。

//通过 变量名 := 表达式 来声明变量,变量的类型根据表达式来自动推导。
a := 100
freq := rand.Float64() * 3.0

这种方式也是可以用来声明和初始化一组变量的:

i,j := 1,2

如果是常量的声明和使用,只需要将var替换为const即可。

const FLAG  string = "YES"

接下来看一段示例代码,加深理解。

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

3.3 for&if

Go的循环在C的基础上简化不少,除去了外在的括号以及whiledo while循环方式,在go中,只有for循环,但却很强大。下面是一些基本用法:

package main
​
import "fmt"func main() {
    i := 1
    for {
        fmt.Println("loop start")
        break
    }
    for j := 1; j < 10; 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++
    }
}

Go中的if语句不需要()来包裹,直接写出条件即可:

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

综合:使用嵌套for循环输出2-100之间的素数。

package main
​
import "fmt"
​
func main() {
    /*以下实例使用循环嵌套来输出 2 到 100 间的素数*/
    var i, j int
    for i = 2; i <= 100; i++ {
        for j = 2; j <= (i / j); j++ {
            if i%j == 0 {
                break
            }
        }
        if j > (i / j) {
            fmt.Printf("%d 是素数\n", i)
        }
    }
}

3.4 switch

switch的用法和也是在其他语言的基础上的加强版本,switch 语句用于基于不同条件执行不同动作,每一个 case 分支都是唯一的,从上至下逐一测试,直到匹配为止。

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

switch 语句执行的过程从上至下,直到找到匹配项,匹配项后面也不需要再加 break。

switch 默认情况下 case 最后自带 break 语句,匹配成功后就不会执行其他 case,如果我们需要执行后面的 case,可以使用 fallthrough

package main
​
import "fmt"
​
func main() {
   /* 定义局部变量 */
   var grade string = "B"
   var marks int = 90
​
   switch marks {
      case 90: grade = "A"
      case 80: grade = "B"
      case 50,60,70 : grade = "C"
      default: grade = "D"  
   }
​
   switch {
      case grade == "A" :
         fmt.Printf("优秀!\n" )    
      case grade == "B", grade == "C" :
         fmt.Printf("良好\n" )      
      case grade == "D" :
         fmt.Printf("及格\n" )      
      case grade == "F":
         fmt.Printf("不及格\n" )
      default:
         fmt.Printf("差\n" );
   }
   fmt.Printf("你的等级是 %s\n", grade );      
}

使用 fallthrough 会强制执行后面的 case 语句,fallthrough 不会判断下一条 case 的表达式结果是否为 true。

package main
​
import "fmt"func main() {
​
    switch {
    case false:
            fmt.Println("1、case 条件语句为 false")
            fallthrough
    case true:
            fmt.Println("2、case 条件语句为 true")
            fallthrough
    case false:
            fmt.Println("3、case 条件语句为 false")
            fallthrough
    case true:
            fmt.Println("4、case 条件语句为 true")
    case false:
            fmt.Println("5、case 条件语句为 false")
            fallthrough
    default:
            fmt.Println("6、默认 case")
    }
}

3.5 数组

在Go中,数组的定义和大多数编程语言类似,是一个定长的的特定类型元素组成的序列。数组的每个元素可以通过索引下标来访问,索引下标的范围是从0开始到数组长度减1的位置。内置的len函数将返回数组中元素的个数。

package mainimport "fmt"
​
func main() {
​
    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)
}
package main

import "fmt"

func main() {
   var n [10]int /* n 是一个长度为 10 的数组 */
   var i,j int

   /* 为数组 n 初始化元素 */        
   for i = 0; i < 10; i++ {
      n[i] = i + 100 /* 设置元素为 i + 100 */
   }

   /* 输出每个数组元素的值 */
   for j = 0; j < 10; j++ {
      fmt.Printf("Element[%d] = %d\n", j, n[j] )
   }
}

由于数组固定长度的局限,在go中使用的更多的是下面即将出场的切片slice


3.6 切片

Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。一个slice类型一般写作[]T,其中T代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度而已。内置的lencap函数分别返回slice的长度和容量。

package main
​
import "fmt"func main() {
    var numbers = make([]int, 3, 5)
    printSlice1(numbers)
}
func printSlice1(x []int) {
    fmt.Printf("len=%d cap=%d slice=%v\n", len(x), cap(x), x)
}

go的切片和python的很类似,可以说是集成了众多编程语言的优势。

package main
​
import "fmt"func main() {
    //创建切片
    numbers := []int{0, 1, 2, 3, 4, 5, 6, 7, 8}
    printSlice(numbers)
​
    //打印原始切片
    fmt.Println("numbers == ", numbers)
​
    //切片索引1到4
    fmt.Println("numbers[1:4]==", numbers[1:4])
​
    //默认下限为0
    fmt.Println("numbers[:3]==", numbers[:3])
​
    //默认上限len(s)
    fmt.Println("numbers[4:]==", numbers[4:])
​
    numbers1 := make([]int, 0, 5)
    printSlice(numbers1)
​
    /* 打印子切片从索引  0(包含) 到索引 2(不包含) */
    number2 := numbers[:2]
    printSlice(number2)
​
    /* 打印子切片从索引 2(包含) 到索引 5(不包含) */
    number3 := numbers[2:5]
    printSlice(number3)
}
func printSlice(x []int) {
    fmt.Printf("len=%d cap=%d slice=%v\n", len(x), cap(x), x)
}

如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来。下面的代码描述了从拷贝切片的 copy 方法和向切片追加新元素的 append 方法。

package main

import "fmt"

func main() {
	var numbers []int
	printSlice2(numbers)
	//允许追加空切片
	numbers = append(numbers, 0)
	printSlice2(numbers)
	//向切片添加一个元素
	numbers = append(numbers, 1)
	printSlice2(numbers)
	//同时添加多个元素
	numbers = append(numbers, 2, 3, 4)
	printSlice2(numbers)
	//创建切片numbers1是之前切片的两倍容量
	numbers1 := make([]int, len(numbers), (cap(numbers))*2)
	//拷贝numbers的内容到numbers1
	copy(numbers1, numbers)
	printSlice2(numbers1)
}

func printSlice2(x []int) {
	fmt.Printf("len=%d cap=%d slice=%v\n", len(x), cap(x), x)
}

注意,切片遵循左闭右开的区间规则。再看一个例子:

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

3.7 哈希表

在Go语言中,一个map就是一个哈希表的引用,map类型可以写为map[K]V,其中K和V分别对应key和value。map中所有的key都有相同的类型,所有的value也有着相同的类型,但是key和value之间可以是不同的数据类型。其中K对应的key必须是支持==比较运算符的数据类型,所以map可以通过测试key是否相等来判断是否已经存在。

  • 内置的make函数可以创建一个map
ages := make(map[string]int)
  • 用map字面值的语法创建map,同时还可以指定一些最初的key/value
ages := map[string]int {
    "爱丽丝": 31,
    "德鲁克": 23,
}
//上面的写法相当于
ages := make(map[string]int)
ages["alice"] = 31
ages["charlie"] = 34
  • 同时,map也是支持自增操作,
package main
​
import (
    "fmt"
)
​
func main() {
    //内置make函数创建map
    map1 := make(map[string]int)
    map1["age"] = 21
    map1["id"] = 10086
    //我们也可以用map字面值的语法创建map,同时还可以指定一些最初的key/value:
    //相当于:
    //ages := make(map[string]int)
    //ages["alice"] = 31
    //ages["charlie"] = 34
    map2 := map[string]int{
        "张三": 21,
        "王五": 43,
        "李四": 67,
    }
    //所以,创建一个空map的方式:
    //map[string]int{}
    //Map中的元素通过key对应的下标语法访问:
    fmt.Println(map2["李四"])
    fmt.Println(map2["张三"])
    fmt.Println(map1["id"])
    //实用内置的delete函数输出元素
    delete(map2, "李四")
    fmt.Println(map2["李四"])
    //对mao使用自增语法,但是map中的元素并不是一个变量,因此我们不能对map的元素进行取址操作
    map1["id"]++
    fmt.Printf("自增之后的map1中id的值=%d", map1["id"])
    fmt.Println()
    //要想遍历map中全部的key/value对的话,可以使用range风格的for循环实现,和之前的slice遍历语法类似。
    //下面的迭代语句将在每次迭代时设置name和age变量,它们对应下一个键/值对
    for name, age := range map2 {
        fmt.Printf("姓名:%s\t年龄:%d\n", name, age)
    }
}
  • 和slice一样,map之间也不能进行相等比较;唯一的例外是和nil进行比较。要判断两个map是否包含相同的key和value,我们必须通过一个循环实现。
package main

import "fmt"

func main() {
	//和slice一样,map之间也不能进行相等比较;
	//唯一的例外是和nil进行比较。要判断两个map是否包含相同的key和value,我们必须通过一个循环实现:
	x := make(map[string]int)
	x["点赞"] = 100
	x["评论"] = 20
	x["收藏"] = 80

	y := make(map[string]int)
	y["点赞"] = 100
	y["评论"] = 20
	y["收藏"] = 80
	res := equal(x, y)
	fmt.Println(res)

}
func equal(x, y map[string]int) bool {
	if len(x) != len(y) {
		return false
	}
	for k, xv := range x {
		if yv, ok := y[k]; !ok || yv != xv {
			return false
		}
	}
	return true
}

禁止对map元素取址。原因是map可能随着元素数量的增长而重新分配更大的内存空间,从而可能导致之前的地址无效。


3.8 range

Go 语言中 range 关键字用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素。在数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value 对。

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

3.9 函数

函数是基本的代码块,用于执行一个任务,Go 语言最少有个 main() 函数。

你可以通过函数来划分不同功能,逻辑上每个函数执行的是指定的任务,函数声明告诉了编译器函数的名称,返回类型,和参数。

  • 函数声明包括函数名、形式参数列表、返回值列表(可省略)以及函数体。
func name(parameter-list) (result-list) {
    body
}
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
}

3.10 指针

我们都知道,变量是一种使用方便的占位符,用于引用计算机内存地址。Go语言的取地址符是 &,放到一个变量前使用就会返回相应变量的内存地址。

package main
​
import "fmt"func main() {
    a := 10
    fmt.Printf("变量的地址:%x\n", &a)
​
    b := 20
    var ip *int
​
    ip = &b
    fmt.Printf("b的变量的地址:%x\n", &b)
    /*指针变量的存储地址*/
    fmt.Printf("ip变量存储的指针地址:%x\n", ip)
    /*使用指针访问值*/
    fmt.Printf("*ip变量的值:%d\n", *ip)
​
    /**
    当一个指针被定义后没有分配到任何变量时,它的值为 nil。
    nil 指针也称为空指针。
    nil在概念上和其它语言的null、None、nil、NULL一样,都指代零值或空值。
    一个指针变量通常缩写为 ptr
    */
    var ptr *int
    fmt.Printf("ptr 的值为 : %x\n", ptr)
}
  • 指针在数组中的使用
package main

import "fmt"

func main() {
	/* 定义局部变量 */
	var a int = 100
	var b int = 200

	fmt.Printf("交换前 a 的值 : %d\n", a)
	fmt.Printf("交换前 b 的值 : %d\n", b)

	/* 调用函数用于交换值
	 * &a 指向 a 变量的地址
	 * &b 指向 b 变量的地址
	 */
	swap(&a, &b)

	fmt.Printf("交换后 a 的值 : %d\n", a)
	fmt.Printf("交换后 b 的值 : %d\n", b)
}

func swap(x *int, y *int) {
	var temp int
	temp = *x /* 保存 x 地址的值 */
	*x = *y   /* 将 y 赋值给 x */
	*y = temp /* 将 temp 赋值给 y */
}

3.11 结构体

结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体。每个值称为结构体的成员。用结构体的经典案例是处理公司的员工信息,每个员工信息包含一个唯一的员工编号、员工的名字、家庭住址、出生日期、工作岗位、薪资、上级领导等等。所有的这些信息都需要绑定到一个实体中,可以作为一个整体单元被复制,作为函数的参数或返回值,或者是被存储到数组中,等等。

  • 下面两个语句声明了一个叫Employee的命名的结构体类型,并且声明了一个Employee类型的变量dilbert。
type Employee struct {
    ID        int
    Name      string
    Address   string
    DoB       time.Time
    Position  string
    Salary    int
    ManagerID int
}

var dilbert Employee
  • 结构体的基本操作
package main
​
import "fmt"
​
func main() {
    //创建一个新的结构体
    fmt.Println(Books{"Go语言", "www.waer.ltd", "Go语言教程", 2321321})
    //也可以使用key=>value格式
    fmt.Println(Books{title: "Go语言", author: "www.waer.ltd", subject: "教程", book_id: 12121})
    //忽略的字段为0 或者空
    fmt.Println(Books{title: "Go语言", author: "www.waer.ltd"})
​
    var Book1 Books /* 声明 Book1 为 Books 类型 */
    var Book2 Books /* 声明 Book2 为 Books 类型 *//* book 1 描述 */
    Book1.title = "Go 语言"
    Book1.author = "www.runoob.com"
    Book1.subject = "Go 语言教程"
    Book1.book_id = 6495407/* book 2 描述 */
    Book2.title = "Python 教程"
    Book2.author = "www.runoob.com"
    Book2.subject = "Python 语言教程"
    Book2.book_id = 6495700/* 打印 Book1 信息 */
    fmt.Printf("Book 1 title : %s\n", Book1.title)
    fmt.Printf("Book 1 author : %s\n", Book1.author)
    fmt.Printf("Book 1 subject : %s\n", Book1.subject)
    fmt.Printf("Book 1 book_id : %d\n", Book1.book_id)
​
    /* 打印 Book2 信息 */
    fmt.Printf("Book 2 title : %s\n", Book2.title)
    fmt.Printf("Book 2 author : %s\n", Book2.author)
    fmt.Printf("Book 2 subject : %s\n", Book2.subject)
    fmt.Printf("Book 2 book_id : %d\n", Book2.book_id)
​
    /* 打印 Book1 信息 */
    printBook(Book1)
​
    /* 打印 Book2 信息 */
    printBook(Book2)
​
}
func printBook(book Books) {
    fmt.Printf("Book title : %s\n", book.title)
    fmt.Printf("Book author : %s\n", book.author)
    fmt.Printf("Book subject : %s\n", book.subject)
    fmt.Printf("Book book_id : %d\n", book.book_id)
}
​
type Books struct {
    title   string
    author  string
    subject string
    book_id int
}
  • 结构体也可以作为函数的参数传递
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
}
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
}

3.12 错误处理

Go 语言通过内置的错误接口提供了非常简单的错误处理机制。

error类型是一个接口类型,这是它的定义:

type error interface {
    Error() string
}

对于那些将运行失败看作是预期结果的函数,它们会返回一个额外的返回值,通常是最后一个,来传递错误信息。如果导致失败的原因只有一个,额外的返回值可以是一个布尔值,通常被命名为ok。我们可以在编码中通过实现 error 接口类型来生成错误信息。

函数通常在最后的返回值中返回错误信息。使用errors.New 可返回一个错误信息:

  • 下面的例子中,将err作为函数的一个参数传入,如果处理的结果出现意料之外的错误情况,直接返回一个自定义的error信息。
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) // wangif u, err := findUser([]user{{"wang", "1024"}}, "li"); err != nil {
        fmt.Println(err) // not found
        return
    } else {
        fmt.Println(u.name)
    }
}

3.13 json

考虑一个应用程序,该程序负责收集各种电影评论并提供反馈功能。它的Movie数据类型和一个典型的表示电影的值列表如下所示。(在结构体声明中,Year和Color成员后面的字符串面值是结构体成员Tag;我们稍后会解释它的作用。)

type Movie struct {
    Title  string
    Year   int  `json:"released"`
    Color  bool `json:"color,omitempty"`
    Actors []string
}
​
var movies = []Movie{
    {Title: "Casablanca", Year: 1942, Color: false,
        Actors: []string{"Humphrey Bogart", "Ingrid Bergman"}},
    {Title: "Cool Hand Luke", Year: 1967, Color: true,
        Actors: []string{"Paul Newman"}},
    {Title: "Bullitt", Year: 1968, Color: true,
        Actors: []string{"Steve McQueen", "Jacqueline Bisset"}},
    // ...
}

这样的数据结构特别适合JSON格式,并且在两者之间相互转换也很容易。将一个Go语言中类似movies的结构体slice转为JSON的过程叫编组(marshaling)。编组通过调用json.Marshal函数完成:

data, err := json.Marshal(movies)
if err != nil {
    log.Fatalf("JSON marshaling failed: %s", err)
}
fmt.Printf("%s\n", data)

也就是说,go自带的结构体类似于Java中的实体类,如果需要,只需要使用结构体构造一个与JSON数据类似的结构,注意首字母大写,再通过Marshal方法即可得到一个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"}}
}

4.Go语言标准库

4.1 strings

个字符串是一个不可改变的字节序列。字符串可以包含任意的数据,包括byte值0,但是通常是用来包含人类可读的文本。

  • 字符串中一些常用的API。
package main
​
import (
    "fmt"
    "strings"
)
​
func main() {
    a := "hello"
    //判断字符串s中是否包含子串"ll"
    fmt.Println(strings.Contains(a, "ll")) // true
    //统计字符串中有多少个指定子串"l"
    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
    //将一系列字符串连接为一个字符串,之间用sep来分隔。
    fmt.Println(strings.Join([]string{"he", "llo"}, "-")) // he-llo
    //返回2个s串拼接之后的字符串
    fmt.Println(strings.Repeat(a, 2)) // hellohello
    //返回将s中前n个不重叠old子串都替换为new的新字符串,如果n<0会替换所有old子串。
    fmt.Println(strings.Replace(a, "e", "E", -1)) // hEllo
    //返回使用sep分割的字符串s的切片
    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
}

更多API参考


4.2 time

time是go用来提供时间的显示和测量用的函数,日历的计算采用的是公历方式。

从一段代码入手:

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
}
  • time.Now()方法用来获取当前的本地时间,格式如下:

2023-01-15 16:37:07.586606 +0800 CST m=+0.0112677012022-03-27 01:25:36 +0000 UTC

  • time.Date()可以返回指定的日期,可以指定时区。特别的,如果需要获取日期中的天、日、小时、分钟等信息,可以使用下面的几个方法:

t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute()

  • Format日期格式化

Format根据layout指定的格式返回t代表的时间点的格式化文本表示。那为什么格式化提供的格式不像Java提供的yyyy-MM:dd类似的格式而是一个具体的日期?

这其实是为了方便记忆,记忆一个抽象日期的格式不如记忆一个具体日期来的快,并且如果你观察够仔细会发现,提供的默认日期格式2006-01-02 15:04:05,拆解之后会发现除了年份之外,其他部分就是1,2,3,4,5的有序数,加上年份之后便是1-6的顺序,如此一来,就会很快的根据这些数字直接联想到200612345秒,转换之后就是2006-01-02 15:04:05

  • time.Sub()返回一个时间段(间隔)
  • time.Parse() Parse解析一个格式化的时间字符串并返回它代表的时间,layout定义了参考时间,该方法和前面的Format

方法类似。

  • time.Unix():Unix创建一个本地时间,对应sec和nsec表示的Unix时间(从January 1, 1970 UTC至该时间的秒数和纳秒数)。

    nsec的值在[0, 999999999]范围外是合法的。

1673773044

更多API参考


4.3 strconv

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
}
​
  • func ParseFloat(s string, bitSize int) (f float64, err error)

解析一个表示浮点数的字符串并返回其值。

如果s合乎语法规则,函数会返回最为接近s表示值的一个浮点数(使用IEEE754规范舍入)。bitSize指定了期望的接收类型,32是float32(返回值可以不改变精确值的赋值给float32),64是float64;返回值err是*NumErr类型的,语法有误的,err.Error=ErrSyntax;结果超出表示范围的,返回值f为±Inf,err.Error= ErrRange。

  • func ParseInt(s string, base int, bitSize int) (i int64, err error)

返回字符串表示的整数值,接受正负号。

base指定进制(2到36),如果base为0,则会从字符串前置判断,"0x"是16进制,"0"是8进制,否则是10进制;

bitSize指定结果必须能无溢出赋值的整数类型,0、8、16、32、64 分别代表 int、int8、int16、int32、int64;返回的err是*NumErr类型的,如果语法有误,err.Error = ErrSyntax;如果结果超出类型范围err.Error = ErrRange。

  • func Atoi(s string) (i int, err error)

Atoi是ParseInt(s, 10, 0)的简写。即默认转为十进制

更多API参考


4.4 fmt

fmt包实现了类似C语言printfscanf的格式化I/O。格式化动作('verb')源自C语言但更简单。

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
}
  • 显示规则
%v  值的默认格式表示
%+v 类似%v,但输出结构体时会添加字段名
%#v 值的Go语法表示
%T  值的类型的Go语法表示
%%  百分号
  • 整数
%b  表示为二进制
%c  该值对应的unicode码值
%d  表示为十进制
%o  表示为八进制
%q  该值对应的单引号括起来的go语法字符字面值,必要时会采用安全的转义表示
%x  表示为十六进制,使用a-f
%X  表示为十六进制,使用A-F
%U  表示为Unicode格式:U+1234,等价于"U+%04X"

可以直接使用%v会根据当前变量数据类型自动进行格式化输出。

更多API参考


4.5 os&exec

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

exec包执行外部命令。它包装了os.StartProcess函数以便更容易的修正输入和输出,使用管道连接I/O,以及作其它的一些调整。

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
}
  • func Getenv(key string) string

Getenv检索并返回名为key的环境变量的值。如果不存在该环境变量会返回空字符串。

  • Args[]string

Args保管了命令行参数,第一个是程序名。

  • func Setenv(key, value string) error

Setenv设置名为key的环境变量。如果出错会返回该错误。


5.Go案例实战

5.1 猜数字游戏-命令行版

需求概述

命令行输入一个数字,与游戏产生的随机数进行比较,如果大了或者小了给出玩家相应的提示,否则打印玩家获胜!

开发步骤

  • 随机数产生
  • 控制台读入用户猜测的数字
  • 判断目标数和输入数的大小关系

代码实现

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("目标数字是:", secretNumber)
    //用户输入数字
    fmt.Println("请输入你猜的数字:")
    //创建一个缓冲流,获取系统输入
    reader := bufio.NewReader(os.Stdin)
    for {
        //读取一行输入
        input, err := reader.ReadString('\n')
        if err != nil {
            fmt.Println("出现错误,请重试....", err)
            return
        }
        //出去换行符
        input = strings.Trim(input, "\r\n")
        //字符串转数字
        guess, err := strconv.Atoi(input)
        if err != nil {
            fmt.Println("An error occured while reading input. Please try again", err)
            continue
        }
        fmt.Println("你猜测的是", 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
        }
    }
}

5.2 在线词典-命令行版

需求概述

调用彩云科技的翻译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 := &http.Client{}
    request := DictRequest{TransType: "en2zh", Source: word}
    //序列化为json
    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
    //json反序列化
    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 {
        //Fprintf根据format参数生成格式化的字符串并写入w。返回写入的字节数和遇到的任何错误。
        fmt.Fprintf(os.Stderr, `usage: simpleDict WORD
example: simpleDict hello`)
        os.Exit(1)
    }
    word := os.Args[1]
    query(word)
}
  • url构造太复杂时可以使用curl格式的url;再前往www.curlconverter.com转化为go代码
  • 使用www.oktools.net/json2go对json进行转化,以快速生成结构体以解析response
  • 注意json.Unmarshal(bodyText, &dictResponse)时,需要传入结构体的指针!

5.3 SOCKS5实践

什么是sockes5

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


socks5代理原理

image-20230115220537626

代码实现

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

6. 本次作业

  1. 修改猜数字游戏里面的最终代码,使用fmt.Scanf来简化代码实现。
  2. 修改命令行词典案例的代码,增加另一种翻译引擎的支持
  3. 在上一步的基础上,修改代码实现并行请求两个翻译引擎来提提高响应速度

本来想把笔记写的细一点,但确实时间不够啊,所以只能草草结案,先发布再修改好了!