字节青训营第一课之go语言入门的笔记和总结

有幸参加了字节跳动举办的青训营活动,主要是go语言的编程实践,我将会整理课程的笔记和总结,欢迎关注!

1Go语言简介

由于Go语言有语法简单、高性能等特点,因此国内外各大公司如谷歌、腾讯、字节等都在使用,特别是字节全面拥抱Go,原因是最初因性能问题将Python换成Go,而Go学习简单,性能高,且部署简单。总的来说Go语言特性如下:

  • 语法简单、学习曲线平缓

  • 高性能、高并发

  • 丰富的标准库

  • 完善的工具链

  • 静态链接

  • 快速编译

  • 跨平台

  • 垃圾回收

个人而言,因我是C++出身,对C++复杂的语法感到麻木,因此特别喜欢Go语法简单、上手快的特点,几小时就能上手,再加上Go天生高并发,有丰富标准库等特点,几近完美,因此建议大家学下Go

2入门

2.1开发环境

本地编译环境:Golang、VSCode

云端开发环境:gitpod

开发命令准备:go mod init [module-path] 生成go.mod,注意这里module-path不用跟实际文件名相同。go mod tidy添加或删除不用包。go mod help查看其他命令。
同目录的多项目使用go work init和go work use pro1,参考链接。其他命令go work help。

2.2基础语法

Hello World

package main

import (
	"fmt"
)

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

第一行代表属于main包,它是程序入口包,程序从main包开始运行(若go run xxx.go报错package command-line-arguments is not a main package则将xxx.go中改成package main),import导入fmt包,并再main函数中调用其Println函数。
也可分组导入或多个导入

import (
	"fmt"
	"math"
)
import "fmt"
import "math"

导入包时,只能引用其中已导出(大写字母开头)名字

package main

import (
	"fmt"
	"math"
)

func main() {
	//fmt.Println(math.pi)//小写未导出
	fmt.Println(math.Pi)
}

使用go build xxx.go或go run xxx.go,注意xxx必须是包含package main的go文件,当然这里xxx不用是main,编译或编译并运行代码

变量

go是强类型,变量都有类型,声明变量时常用方式:

  • var varname type = value:需var关键字且类型放后面
  • varname := value:会自行推到类型

var 语句可以出现在包或函数级别,函数外的每个语句都必须以关键字开始(var, func 等等),因此 := 结构不能在函数外使用。

func main() {
	var i int
	fmt.Println(i, c, python, java)
	//0 false false false
	//不能再函数外
	c, python, java := true, false, "no!"
}
var i, j int = 1, 2

零值

没有明确初始值的变量声明会被赋予它们的零值。零值是:

数值类型为 0,
布尔类型为 false,
字符串为 “”(空字符串)。

func main() {
	var i int
	var f float64
	var b bool
	var s string
	fmt.Printf("%v %v %v %q\n", i, f, b, s)
	//0 0 false ""
}

基本类型

同导入语句一样,变量声明也可以“分组”成一个语法块。int, uint 和 uintptr在32位系统上通常为 32 位,在64 位系统上则为64位宽。当需要一个整数值时应使用 int 类型,除非你有特殊的理由使用固定大小或无符号的整数类型

bool
string
int  int8  int16  int32  int64
uint uint8 uint16 uint32 uint64 uintptr

byte // uint8 的别名

rune // int32 的别名
    // 表示一个 Unicode 码点
    
float32 float64
complex64 complex128

表达式 T(v) 将值 v 转换为类型 T。与 C 不同的是,Go 在不同类型的项之间赋值时需要显式转换

func main() {
	var x, y int = 3, 4
	var f float64 = math.Sqrt(float64(x*x + y*y))
	//cannot use f (variable of type float64) as uint value in variable declaration
	//var z uint = f
	var z uint = uint(f)
	fmt.Println(x, y, z)
	//3 4 5
}

常量

常量的声明与变量类似,只不过是使用 const 关键字。常量可以是字符、字符串、布尔值或数值。常量不能用 := 语法声明。

const Pi = 3.14
func main() {
	const World = "世界"
	fmt.Println("Hello", World)
	fmt.Println("Happy", Pi, "Day")
	const Truth = true
	fmt.Println("Go rules?", Truth)
	//Hello 世界
	//Happy 3.14 Day
	//Go rules? true
}

字符串

go的strings包有很多常用函数:contains判断是否包含另一字符串、count字符串计数、index查找字符串位置、join连接多字符串、repeat重复多个字符串、replace替换字符串

字符串格式化

fmt包中有很多字符串格式化相关的方法,但常用“%v”打印任意类型变量,不需区分类型,“%+v”和“%#v”则更详细

数字解析

字符串和数字间转换通过strconv包的函数,Atoi表示字符串转数字相反则Itoa

if else判断

if后无括号,但后面必须接大括号。同 for 一样, if 语句可在条件表达式前执行一个简单语句。该语句声明的变量作用域仅在 if 之内

func sqrt(x float64) string {
	if x < 0 {
		return sqrt(-x) + "i"
	}
	return fmt.Sprint(math.Sqrt(x))
}

func pow(x, n, lim float64) float64 {
	if v := math.Pow(x, n); v < lim {
		return v
	}
	fmt.Println(v)//undefined: v
	return lim
}

func main() {
	fmt.Println(sqrt(2), sqrt(-4))
	//1.4142135623730951 2i
	fmt.Println(
		pow(3, 2, 10),
		pow(3, 3, 20),
	)
	//9 20
}

switch

switch后变量名无括号,case中不加break,Go只运行选定的 case,而非之后所有的 case,Go 自动提供每个 case 后面所需的 break 语句, 除非以 fallthrough 语句结束,否则分支会自动匹配直终止。且可以使用任意变量类型,当判断较多时可用switch完全替代if else

func main() {
	fmt.Print("Go runs on ")
	switch os := runtime.GOOS; os {
	case "darwin":
		fmt.Println("OS X.")
	case "linux":
		fmt.Println("Linux.")
		fallthrough//不加则后面不会运行直接break
	default:
		fmt.Printf("%s.\n", os)
	}
	//Go runs on Linux.
	//linux.
}

没有条件的 switch 同 switch true 一样

func main() {
	t := time.Now()
	switch {
	case t.Hour() < 12:
		fmt.Println("Good morning!")
	case t.Hour() < 17:
		fmt.Println("Good afternoon.")
	default:
		fmt.Println("Good evening.")
	}
	//Good evening.
}

for循环

只有唯一的for循环,for中三语句任何语句都可省略且也没括号,循环中可用break或continue跳出或继续循环。初始化语句通常为一句短变量声明,该变量声明仅在 for 语句的作用域中可见。

func main() {
	sum := 0
	for i := 0; i < 10; i++ {
		//i只在for作用域中
		sum += i
	}
	fmt.Println(sum)
	//45
}

注意:和 C、Java、JavaScript 之类的语言不同,Go 的 for 语句后面的三个构成部分外没有小括号, 大括号 { } 则是必须的

range

常用range快速遍历数组、slice、map等,返回索引和值,可用“_”忽略索引,每次迭代都会返回两个值。第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本

func main() {
	pow := make([]int, 3)
	for i := range pow {
		pow[i] = 1 << uint(i) // == 2**i
	}
	for _, value := range pow {
		fmt.Printf("%d\n", value)
	}
	//1
	//2
	//4
}

数组

类似其他语言中固定长度数组,实际很少使用,更多使用切片。类型 [n]T 表示拥有 n 个 T 类型的值的数组。数组的长度是其类型的一部分,因此数组不能改变大小

func main() {
	var a [2]string
	a[0] = "Hello"
	a[1] = "World"
	fmt.Println(a[0], a[1])
	fmt.Println(a)

	primes := [6]int{2, 3, 5, 7, 11, 13}
	fmt.Println(primes)
	//Hello World
	//[Hello World]
	//[2 3 5 7 11 13]
}

切片

slice长度不固定,类型 []T 表示一个元素类型为 T 的切片。用make创建,append时自动扩容,可像python中切片一样截取。切片通过两个下标来界定,即一个上界和一个下界,二者以冒号分隔,半开区间,包括第一个元素,但排除最后一个元素。

func main() {
	primes := [6]int{2, 3, 5, 7, 11, 13}

	var s []int = primes[1:4]
	fmt.Println(s)
	//[3 5 7]
}

切片并不存储任何数据,它只是描述了底层数组中的一段。更改切片的元素会修改其底层数组中对应的元素。与它共享底层数组的切片都会观测到这些修改。

func main() {
	names := [4]string{
		"John",
		"Paul",
		"George",
		"Ringo",
	}
	fmt.Println(names)
	//[John Paul George Ringo]

	a := names[0:2]
	b := names[1:3]
	fmt.Println(a, b)
	//[John Paul] [Paul George]

	b[0] = "XXX"
	fmt.Println(a, b)
	//[John XXX] [XXX George]

	fmt.Println(names)
	//[John XXX George Ringo]

}

切片文法类似没长度的数组文法。本质是先创建一个数组,再构建一个引用它的切片

[3]bool{true, true, false}
//会创建一个和上面相同的数组,然后构建一个引用了它的切片:
[]bool{true, true, false}

切片的长度就是它所包含的元素个数,切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数。切片 s 的长度和容量可通过表达式 len(s) 和 cap(s) 来获取。

func main() {
	s := []int{2, 3, 5, 7, 11, 13}
	printSlice(s)

	// 截取切片使其长度为 0
	s = s[:0]
	printSlice(s)

	// 拓展其长度
	s = s[:4]
	printSlice(s)

	// 舍弃前两个值
	s = s[2:]
	printSlice(s)
}

func printSlice(s []int) {
	fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
//len=6 cap=6 [2 3 5 7 11 13]
//len=0 cap=6 []
//len=4 cap=6 [2 3 5 7]
//len=2 cap=4 [5 7]

切片的零值是 nil。nil 切片的长度和容量为 0 且没有底层数组

func main() {
	var s []int
	fmt.Println(s, len(s), cap(s))
	if s == nil {
		fmt.Println("nil!")
	}
	//[] 0 0
	//nil!
}

用内建函数make创建切片,make 函数会分配一个元素为零值的数组并返回一个引用它的切片

a := make([]int, 5)  // len(a)=5
b := make([]int, 0, 5) // len(b)=0, cap(b)=5

b = b[:cap(b)] // len(b)=5, cap(b)=5
b = b[1:]      // len(b)=4, cap(b)=4

切片可包含任何类型,甚至包括其它的切片
append 的结果是一个包含原切片所有元素加上新添加元素的切片。当 s 的底层数组太小,不足以容纳所有给定的值时,它就会分配一个更大的数组。返回的切片会指向这个新分配的数组,地址会发生变化

func main() {
	var s []int
	printSlice(s)

	// 添加一个空切片
	s = append(s, 0)
	printSlice(s)

	// 这个切片会按需增长
	s = append(s, 1)
	printSlice(s)

	// 可以一次性添加多个元素
	s = append(s, 2, 3, 4)
	printSlice(s)
}

func printSlice(s []int) {
	fmt.Printf("len=%d cap=%d %v addr=%p\n", len(s), cap(s), s,&s)
}
//len=0 cap=0 [] addr=0xc0000ac000
//len=1 cap=1 [0] addr=0xc0000ac018
//len=2 cap=2 [0 1] addr=0xc0000ac048
//len=5 cap=6 [0 1 2 3 4] addr=0xc0000ac078

map

实际使用最频繁的数据结构,用make创建,常用val,ok:=m[key]写法,通过判断ok判断是否存在key对应的val。make 函数会返回给定类型的映射,并将其初始化备用。映射的零值为 nil 。nil 映射既没有键,也不能添加键。

func main() {
	m := make(map[string]int)

	m["Answer"] = 42 //插入 
	fmt.Println("The value:", m["Answer"])

	m["Answer"] = 48 //修改
	fmt.Println("The value:", m["Answer"])

	delete(m, "Answer") //删除
	fmt.Println("The value:", m["Answer"])

	v, ok := m["Answer"] //判断是否存在
	fmt.Println("The value:", v, "Present?", ok)
	//The value: 42
	//The value: 48
	//The value: 0
	//The value: 0 Present? false
}
package main

import (
	"golang.org/x/tour/wc"
	"strings"
)

func WordCount(s string) map[string]int {
	r:=strings.Fields(s)
	res:=make(map[string]int)
	for _,v:=range(r){
		res[v]++	
	}
	return res
}

func main() {
	wc.Test(WordCount)
}

函数

函数可没或多个参数,当连续两个或多个函数的已命名形参类型相同时,除最后一个类型以外,其它都可以省略。

//等价
x int, y int
x, y int

参数和返回值类型后置,支持返回多个值,实际代码中几乎所有函数返回两个值:结果和错误信息,注意参数是都值传递,若想修改参数,需传递指针。首字母大写表示公共函数,类似C++中public函数,可包外访问。其次返回值的命名可省略,一般是短函数中。

func split(sum int) (x, y int) {
	x = sum * 4 / 9
	y = sum - x
	return
}

func main() {
	fmt.Println(split(17))
}

函数也是值。它们可以像其它值一样传递。函数值可以用作函数的参数或返回值

func compute(fn func(float64, float64) float64) float64 {
	return fn(3, 4)
}

func main() {
	hypot := func(x, y float64) float64 {
		return math.Sqrt(x*x + y*y)
	}
	fmt.Println(hypot(5, 12))
	//13
	fmt.Println(compute(hypot))
	//5
	fmt.Println(compute(math.Pow))
	//81
}

Go 函数可以是一个闭包。闭包是一个函数值,它引用了其函数体之外的变量。该函数可以访问并赋予其引用的变量的值,换句话说,该函数被这些变量“绑定”在一起。

func adder() func(int) int {
	sum := 0
	return func(x int) int {
		sum += x//引用sum
		return sum
	}
}

func main() {
	pos, neg := adder(), adder()
	for i := 0; i < 4; i++ {
		fmt.Println(
			pos(i),
			neg(-2*i),
		)
	}
}
//0 0
//1 -2
//3 -6
//6 -12

// 返回一个“返回int的函数”
func fibonacci() func() int {
	i,j:=0,1
	return func() int {
		res:=i+j
		i=j
		j=res
		return res
	}
}

func main() {
	f := fibonacci()
	for i := 0; i < 3; i++ {
		fmt.Println(f())
	}
	//1
	//2
	//3
}

指针

类似C/C++指针,但操作有限,指针主要用途是对传入参数修改。这也就是通常所说的**“间接引用”或“重定向”。与 C 不同,Go 没有指针运算**。
类型 *T 是指向 T 类型值的指针。其零值为 nil。& 操作符会生成一个指向其操作数的指针。* 操作符表示指针指向的底层值。

func main() {
	i, j := 42, 2701

	p := &i         // 指向 i
	fmt.Println(*p) // 通过指针读取 i 的值
	//42
	*p = 21         // 通过指针设置 i 的值
	fmt.Println(i)  // 查看 i 的值
	//21
	
	p = &j         // 指向 j
	*p = *p / 37   // 通过指针对 j 进行除法运算
	fmt.Println(j) // 查看 j 的值
	//73
}

结构体

一个结构体(struct)就是一组字段(field),结构体初始化时需传入每个字段初始值,也可键值对的方式初始化,用点号来访问字段。通常用结构体的指针作为参数,既能修改结构体又能避免结构体的拷贝开销,字段首字母大写表示公开字段,类似C++中public成员。
(*p).X 来访问其字段 X。不过这么写太啰嗦了,所以语言也允许我们使用隐式间接引用,直接写 p.X 就可以

type Vertex struct {
	X int
	Y int
}

func main() {
	v := Vertex{1, 2}
	p := &v
	p.X = 1e9
	fmt.Println(v)
}

结构体文法通过直接列出字段的值来新分配一个结构体,使用 Name: 语法可以仅列出部分字段

var (
	v1 = Vertex{1, 2}  // 创建一个 Vertex 类型的结构体
	v2 = Vertex{X: 1}  // Y:0 被隐式地赋予
	v3 = Vertex{}      // X:0 Y:0
	p  = &Vertex{1, 2} // 创建一个 *Vertex 类型的结构体(指针)
)

func main() {
	fmt.Println(v1, p, v2, v3)
	//{1 2} &{1 2} {1 0} {0 0}
}

结构体方法

再函数名前带上结构体参数和括号,就实现类似其他语言中的类成员函数。方法就是一类带特殊的 接收者 参数的函数,方法接收者在它自己的参数列表内,位于 func 关键字和方法名之间方法只是个带接收者参数的函数。接收者的类型定义和方法声明必须在同一包内;不能为内建类型声明方法。

type Vertex struct {
	X, Y float64
}
//Abs 方法拥有一个名为 v,类型为 Vertex 的接收者。
func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func Abs(v Vertex) float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
	v := Vertex{3, 4}
	fmt.Println(Abs(v))
	fmt.Println(v.Abs())
	//5
	//5
}

可以为指针接受者声明方法。这意味着对于某类型 T,接收者的类型可以用 *T 的文法。(此外,只能为在同一包内定义的类型的接收者声明方法,而不能为其它包内定义的类型,包括T 不能是内建类型声明的接收者声明方法。)指针接收者的方法可以修改接收者指向的值,指针接收者比值接收者更常用。值接收者只是对原始值的副本进行操作,原始值接收值不变

type Vertex struct {
	X, Y float64
}

func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

//值接受者的副本
func (v Vertex) Scale1(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func (v *Vertex) Scale2(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func main() {
	v := Vertex{3, 4}
	v.Scale1(10)
	fmt.Println(v.Abs())//5
 	v.Scale2(10)
	fmt.Println(v.Abs())//50
}

注意和函数的参数是指针区别,下面函数的参数是指针,因此传参必须是取地址,而上面的方法则不用

type Vertex struct {
	X, Y float64
}

func Abs(v Vertex) float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func Scale(v *Vertex, f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func main() {
	v := Vertex{3, 4}
	Scale(&v, 10)
	fmt.Println(Abs(v))
}

上面的方法叫指针重定向,对于语句 v.Scale(5),即便 v 是个值而非指针,带指针接收者的方法也能被直接调用。 也就是说,由于 Scale 方法有一个指针接收者,为方便起见,Go 会将语句 v.Scale(5) 解释为 (&v).Scale(5)

type Vertex struct {
	X, Y float64
}

func (v *Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func ScaleFunc(v *Vertex, f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func main() {
	v := Vertex{3, 4}
	v.Scale(2)
	ScaleFunc(&v, 10)

	p := &Vertex{4, 3}
	p.Scale(3)
	ScaleFunc(p, 8)

	fmt.Println(v, p)
	//{60 80} &{96 72}

}

注意:接受一个值作为参数的函数必须接受一个指定类型的值,以值为接收者的方法被调用时接收者既能为值又能为指针

type Vertex struct {
	X, Y float64
}

func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func AbsFunc(v Vertex) float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
	v := Vertex{3, 4}
	fmt.Println(v.Abs())
	fmt.Println(AbsFunc(v))
	//fmt.Println(AbsFunc(&v))//错误

	p := &Vertex{4, 3}
	fmt.Println(p.Abs())
	fmt.Println(AbsFunc(*p))
}

使用指针接收者的原因:方法能够修改其接收者指向的值。可避免每次调用时复制该值。若值的类型为大型结构体时这样会更高效。通常来说,所有给定类型的方法都应该有值或指针接收者,但并不应该二者混用。

接口

接口类型 是由一组方法签名定义的集合。接口类型的变量可以保存任何实现了这些方法的值

type Abser interface {
	Abs() float64
}

func main() {
	var a Abser
	f := MyFloat(-math.Sqrt2)
	v := Vertex{3, 4}

	a = f  // a MyFloat 实现了 Abser
	a = &v // a *Vertex 实现了 Abser

	// 下面一行,v 是一个 Vertex(而不是 *Vertex)
	// 所以没有实现 Abser。报错:cannot use v (variable of type Vertex) as Abser value in assignment: Vertex does not implement Abser (method Abs has pointer receiver)
	// a = v
	a = &v

	fmt.Println(a.Abs())
}

type MyFloat float64

func (f MyFloat) Abs() float64 {
	if f < 0 {
		return float64(-f)
	}
	return float64(f)
}

type Vertex struct {
	X, Y float64
}

func (v *Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

接口与隐式实现,类型通过实现一个接口的所有方法来实现该接口,无需显式声明。隐式接口从接口的实现中解耦了定义,这样接口的实现可以出现在任何包中,无需提前准备。因此,也就无需在每一个实现上增加新的接口名称,这样同时也鼓励了明确的接口定义。

type I interface {
	M()
}

type T struct {
	S string
}

// 此方法表示类型 T 实现了接口 I,但我们无需显式声明此事。
func (t T) M() {
	fmt.Println(t.S)
}

func main() {
	var i I = T{"hello"}
	i.M()
}

接口也是值。它们可以像其它值一样传递。接口值可以用作函数的参数或返回值。在内部,接口值可以看做包含值和具体类型的元组:(value, type)。接口值保存了一个具体底层类型的具体值。接口值调用方法时会执行其底层类型的同名方法。注意实现该接口的类型如结构体,可以隐式转为该接口类型。例如describe(&T{“world”})等价于t:=&T{“world”} describe((I)(t))或者显示转换describe((I)(&T{“world”}))

type I interface {
	M()
}

type T struct {
	S string
}

func (t *T) M() {
	fmt.Println(t.S)
}

type F float64

func (f F) M() {
	fmt.Println(f)
}

func main() {
	var i I

	i = &T{"Hello"}
	describe(i)
	i.M()

	i = F(math.Pi)
	describe(i)
	i.M()
	//(&{Hello}, *main.T)
	//Hello
	//(3.141592653589793, main.F)
	//3.141592653589793
}

func describe(i I) {
	fmt.Printf("(%v, %T)\n", i, i)
}

即便接口内的具体值为 nil,方法仍然会被 nil 接收者调用。注意: 保存nil 具体值的接口其自身并不为 nil。

type I interface {
	M()
}

type T struct {
	S string
}

//
func (t *T) M() {
	if t == nil {
		fmt.Println("<nil>")
		return
	}
	fmt.Println(t.S)
}

func main() {
	var i I

	var t *T
	i = t
	describe(i)
	i.M()

	i = &T{"hello"}
	describe(i)
	i.M()
	//(<nil>, *main.T)
	//<nil>
	//(&{hello}, *main.T)
	//hello
}

func describe(i I) {
	fmt.Printf("(%v, %T)\n", i, i)
}

nil 接口值既不保存值也不保存具体类型。为 nil 接口调用方法会产生运行时错误,因为接口的元组内并未包含能够指明该调用哪个 具体 方法的类型

type I interface {
	M()
}

func main() {
	var i I
	describe(i)
	i.M()
	//(<nil>, <nil>)
	//panic: runtime error: invalid memory address or nil pointer dereference
	//[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x481bf9]
}

func describe(i I) {
	fmt.Printf("(%v, %T)\n", i, i)
}

空接口指定了零个方法的接口值被称为空接口:interface{}。空接口可保存任何类型的值。(因为每个类型都至少实现了零个方法。)空接口被用来处理未知类型的值。例如,fmt.Print 可接受类型为 interface{} 的任意数量的参数。

func main() {
	var i interface{}
	describe(i)

	i = 42
	describe(i)

	i = "hello"
	describe(i)
	//(<nil>, <nil>)
	//(42, int)
	//(hello, string)
}

func describe(i interface{}) {
	fmt.Printf("(%v, %T)\n", i, i)
}

常用接口

  • Stringer:fmt 包中定义的 Stringer 是最普遍的接口之一,是个可以用字符串描述自己的类型。fmt 包(还有很多包)都通过此接口来打印值。只需实现String方法即可。
type Stringer interface {
    String() string
}
type Person struct {
	Name string
	Age  int
}

func (p Person) String() string {
	return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}

func main() {
	a := Person{"Arthur Dent", 42}
	z := Person{"Zaphod Beeblebrox", 9001}
	fmt.Println(a, z)
	//Arthur Dent (42 years) Zaphod Beeblebrox (9001 years)
}
func (p IPAddr) String() string {
	return fmt.Sprintf("%d.%d.%d.%d",p[0],p[1],p[2],p[3])
}

func main() {
	hosts := map[string]IPAddr{
		"loopback":  {127, 0, 0, 1},
		"googleDNS": {8, 8, 8, 8},
	}
	for name, ip := range hosts {
		fmt.Printf("%v: %v\n", name, ip)
	}
}
  • error:用 error 值来表示错误状态。与 fmt.Stringer 类似,error 类型是一个内建接口,函数会返回一 error 值,调用的它的代码应当判断该错误是否等于 nil 来进行错误处理。error 为 nil 时表示成功;非 nil 的 error 表示失败。
type error interface {
    Error() string
}
type MyError struct {
	When time.Time
	What string
}

func (e *MyError) Error() string {
	return fmt.Sprintf("at %v, %s",
		e.When, e.What)
}

func run() error {
	return &MyError{
		time.Now(),
		"it didn't work",
	}
}

func main() {
	if err := run(); err != nil {
		fmt.Println(err)
	}
	//at 2009-11-10 23:00:00 +0000 UTC m=+0.000000001, it didn't work
}

type ErrNegativeSqrt float64

func (e ErrNegativeSqrt) Error() string {
	return fmt.Sprint("cannot Sqrt negative number: %f",float64(e))
}

func Sqrt(x float64) (float64, error) {
	if x<0 {
		return 0,ErrNegativeSqrt(-2)
	}
	r:=1.0
	for i:=0;i<100;i++{
		r-=(r*r-x)/(2*r)
	}
	return r,nil
}


func main() {
	fmt.Println(Sqrt(2))
	fmt.Println(Sqrt(-2))
}

  • Reader:io 包指定 io.Reader 接口,它表示从数据流末尾读取。Read 用数据填充给定的字节切片并返回填充的字节数和错误值。在遇到数据流的结尾时,它会返回 io.EOF 错误。io.Reader 接口的 Read 方法:
func (T) Read(b []byte) (n int, err error)
func main() {
	r := strings.NewReader("HiReader!")

	b := make([]byte, 5)
	for {
		n, err := r.Read(b)
		fmt.Printf("n = %v err = %v b = %v\n", n, err, b)
		fmt.Printf("b[:n] = %q\n", b[:n])
		if err == io.EOF {
			break
		}
	}
	//n = 5 err = <nil> b = [72 105 82 101 97]
	//b[:n] = "HiRea"
	//n = 4 err = <nil> b = [100 101 114 33 97]
	//b[:n] = "der!"
	//n = 0 err = EOF b = [100 101 114 33 97]
	//b[:n] = ""
}
import "golang.org/x/tour/reader"

type MyReader struct{}

// TODO: 给 MyReader 添加一个 Read([]byte) (int, error) 方法
func (r MyReader) Read(b []byte) (int, error) {
	a := 'A'
	for i := 0; i < len(b); i++ {
		b[i] = byte(a)
	}
	return len(b), nil
}

func main() {
	reader.Validate(MyReader{})
	//ok
}

类型断言和类型选择

类型断言 提供了访问接口值底层具体值的方式。t := i.(T)该语句断言接口值 i 保存了具体类型 T,并将其底层类型为 T 的值赋予变量 t。若 i 并未保存 T 类型的值,该语句就会触发panic。为判断接口值是否保存一特定的类型,类型断言可返回两个值:其底层值以及一个报告断言是否成功的布尔值。t, ok := i.(T)若 i 保存一个 T,那么 t 将会是其底层值,而 ok 为 true。否则,ok 将为 false 而 t 将为 T 类型的零值,程序并不会产生panic。

func main() {
	var i interface{} = "hello"

	s := i.(string)
	fmt.Println(s)

	s, ok := i.(string)
	fmt.Println(s, ok)

	f, ok := i.(float64)
	fmt.Println(f, ok)

	f = i.(float64) // 报错(panic)
	fmt.Println(f)
	//hello
	//hello true
	//0 false
	//panic: interface conversion: interface {} is string, not float64
}

类型选择 是一种按顺序从几个类型断言中选择分支的结构。类型选择与一般的 switch 语句相似,不过类型选择中的 case 为类型(而非值), 它们针对给定接口值所存储的值的类型进行比较。

func do(i interface{}) {
	switch v := i.(type) {
	case int:
		fmt.Printf("Twice %v is %v\n", v, v*2)
	case string:
		fmt.Printf("%q is %v bytes long\n", v, len(v))
	default:
		fmt.Printf("I don't know about type %T!\n", v)
	}
}

func main() {
	do(21)
	do("hello")
	do(true)
	//Twice 21 is 42
	//"hello" is 5 bytes long
	//I don't know about type bool!
}

类型选择中的声明与类型断言 i.(T) 的语法相同,只是具体类型 T 被替换成了关键字 type。

错误处理

go常见做法是再传递结果的返回值的基础上,新增一个传递错误信息的返回值,这样能清晰知道哪个函数出错,且能用简单的if else处理错误

defer处理

go中为避免资源泄露,需用defer手动关闭资源,会将函数推迟到外层函数返回之后执行。也就是会在函数运行结束后执行。推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。

func main() {
	defer fmt.Println("world")

	fmt.Println("hello")
	//hello
	//world
}

推迟的函数调用会被压入一个栈中。当外层函数返回时,被推迟的函数会按照后进先出的顺序调用。

func main() {
	fmt.Println("counting")

	for i := 0; i < 3; i++ {
		defer fmt.Println(i)
	}

	fmt.Println("done")
	//counting
	//done
	//2
	//1
	//0
}

json处理

对已有结构体,保证每个字段首字母大写,是公开字段,就能用marshaler序列化变成json字符串,unmarshaler反序列化,可用json tag修改输出结果的字段名

时间处理

time包包含时间处理各种函数,用Date构造带时区时间,Now获取当前时间

进程处理

os包包含进程处理,Args得到命令参数,Getenv读环境变量,Command执行命令

goroutine

Go 程(goroutine)是由 Go 运行时管理的轻量级线程。

go f(x, y, z)

会启动一个新的 Go 程并执行f(x, y, z),f, x, y 和 z 的求值发生在当前的 Go 程中,而 f 的执行发生在新的 Go 程中。Go 程在相同的地址空间中运行,因此在访问共享的内存时必须进行同步。sync 包提供了这种能力,不过在 Go 中并不经常用到。常用信道信道是带有类型的管道,可以通过它用信道操作符 <- 来发送或者接收值。(“箭头”就是数据流的方向。)

ch <- v    // 将 v 发送至信道 ch。
v := <-ch  // 从 ch 接收值并赋予 v。

和映射与切片一样,信道在使用前必须创建:

ch := make(chan int)

默认情况下,发送和接收操作在另一端准备好之前都会阻塞。这使得 Go 程可以在没有显式的锁或竞态变量的情况下进行同步。

func sum(s []int, c chan int) {
	sum := 0
	for _, v := range s {
		sum += v
	}
	c <- sum // 将和送入 c
}

func main() {
	s := []int{7, 2, 8, -9, 4, 0}

	c := make(chan int)
	go sum(s[:len(s)/2], c)
	go sum(s[len(s)/2:], c)
	x, y := <-c, <-c // 从 c 中接收

	fmt.Println(x, y, x+y)
	//-5 17 12
}

信道可以是 带缓冲的。将缓冲长度作为第二个参数提供给 make 来初始化一个带缓冲的信道:

ch := make(chan int, 100)

仅当信道的缓冲区填满后,向其发送数据时才会阻塞。当缓冲区为空时,接受方会阻塞。

func main() {
	ch := make(chan int, 2)
	ch <- 1
	ch <- 2
	fmt.Println(<-ch)
	fmt.Println(<-ch)
	//1
	//2
}

若缓冲区满再发送就会报错:fatal error: all goroutines are asleep - deadlock!,因为容量是1,第二次发送就报错。

func main() {
	ch := make(chan int, 1)
	ch <- 1
	ch <- 2
	fmt.Println(<-ch)
	fmt.Println(<-ch)
}

发送方可通过close关闭信道表示没有需要发送的值了。接收者可以通过为接收表达式分配第二个参数ok来测试信道是否被关闭:若没有值可以接收且信道已被关闭,那么在执行完

v, ok := <-ch

之后 ok 会被设置为 false。循环 for i := range c 会不断从信道接收值,直到它被关闭。注意:只有发送者才能关闭信道,而接收者不能。向一个已经关闭的信道发送数据会引发程序panic。还要注意信道与文件不同,通常情况下无需关闭它们。只有在必须告诉接收者不再有需要发送的值时才有必要关闭。

func fibonacci(n int, c chan int) {
	x, y := 0, 1
	for i := 0; i < n; i++ {
		c <- x
		x, y = y, x+y
	}
	close(c)
}

func main() {
	c := make(chan int, 3)
	go fibonacci(cap(c), c)
	for i := range c {
		fmt.Println(i)
	}
	//0
	//1
	//1
}

select 语句使一个 Go 程可以等待多个通信操作。select 会阻塞到某个分支可以继续执行为止,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。当 select 中的其它分支都没有准备好时,default 分支就会执行。

func fibonacci(c, quit chan int) {
	x, y := 0, 1
	for {
		select {
		case c <- x:
			x, y = y, x+y
		case <-quit:
			fmt.Println("quit")
			return
		}
	}
}

func main() {
	c := make(chan int)
	quit := make(chan int)
	go func() {
		for i := 0; i < 3; i++ {
			fmt.Println(<-c)
		}
		quit <- 0
	}()
	fibonacci(c, quit)
	//0
	//1
	//1
}
// TreeNode 表示二叉树的节点
type TreeNode struct {
	Left  *TreeNode
	Value int
	Right *TreeNode
}

// Walk 遍历二叉查找树并将值发送到 ch 信道中
func Walk(t *TreeNode, ch chan int) {
	if t == nil {
		return
	}
	Walk(t.Left, ch)
	ch <- t.Value
	Walk(t.Right, ch)
}

// New 创建一个随机结构的已排序二叉查找树
func New(k int) *TreeNode {
	return &TreeNode{
		Left:  nil,
		Value: k,
		Right: nil,
	}
}

// Same 检测 t1 和 t2 是否存储了相同的值
func Same(t1, t2 *TreeNode) bool {
	ch1, ch2 := make(chan int), make(chan int)
	go func() {
		Walk(t1, ch1)
		close(ch1)
	}()
	go func() {
		Walk(t2, ch2)
		close(ch2)
	}()

	for {
		v1, ok1 := <-ch1
		v2, ok2 := <-ch2
		if !ok1 || !ok2 {
			return ok1 == ok2
		}
		if v1 != v2 {
			return false
		}
	}
}

func main() {
	ch := make(chan int)
	go func() {
		Walk(New(1), ch)
		close(ch)
	}()

	for v := range ch {
		fmt.Println(v)
	}
	fmt.Println(Same(New(1), New(1)))
	fmt.Println(Same(New(1), New(2)))
	//1
	//true
	//false
}

WaitGroup

在实际开发中我们并不能保证每个协程执行的时间,如果需要等待多个协程,全部结束任务后,再执行某个业务逻辑。下面我们介绍处理这种情况的方式。

WaitGroup 有几个方法:

Add:初始值为 0 ,这里直接传入子协程的数量,你传入的值会往计数器上加。
Done:当某个子协程完成后,可调用此方法,会从计数器上减一,即子协程的数量减一,通常使用 defer 来调用。
Wait:阻塞当前协程,直到实例里的计数器归零。

  1. 用信道,可以实现多个协程间的通信,于是乎可以定义一个信道,在任务执行完成后,往信道中写入 true ,然后在主协程中获取到 true ,就可以认为子协程已经执行完毕
package main

import "fmt"

func main() {
	isDone := make(chan bool)
	go func() {
		for i := 0; i < 5; i++{
			fmt.Println(i)
		}
		isDone <- true
	}()
	<- isDone
}
  1. 信道虽然可行,但在程序中使用很多协程时,代码就会看起来很复杂,使用 sync 包中提供的 WaitGroup 类型更方便。WaitGroup 用于等待一批 Go 协程执行结束。程序控制会一直阻塞,直到这些协程全部执行完毕。
package main

import (
	"fmt"
	"sync"
)

func task(taskNum int, wg *sync.WaitGroup) {
	// 延迟调用 执行完子协程计数器减一
	defer wg.Done()
	// 输出任务号
	for i := 0; i < 3; i++ {
		fmt.Printf("task %d: %d\n", taskNum, i)
	}
}

func main() {
	// 实例化 sync.WaitGroup
	var waitGroup sync.WaitGroup
	// 传入子协程的数量
	waitGroup.Add(3)
	// 开启一个子协程 协程 1 以及 实例 waitGroup
	go task(1, &waitGroup)
	// 开启一个子协程 协程 2 以及 实例 waitGroup
	go task(2, &waitGroup)
	// 开启一个子协程 协程 3 以及 实例 waitGroup
	go task(3, &waitGroup)
	// 实例 waitGroup 阻塞当前协程 等待所有子协程执行完
	waitGroup.Wait()
}

sync.Mutex

可看到信道非常适合在各个 Go 程间进行通信。但是如果并不需要通信?比如只是想保证每次只有一个 Go 程能够访问一个共享的变量,从而避免冲突?需要 *互斥(mutual exclusion),通常使用 互斥锁(Mutex) 提供这种机制。Go 标准库中提供 sync.Mutex 互斥锁类型及其两个方法:

Lock
Unlock

可以通过在代码前调用 Lock 方法,在代码后调用 Unlock 方法来保证一段代码的互斥执行。
也可以用 defer 语句来保证互斥锁一定会被解锁。

// SafeCounter 的并发使用是安全的。
type SafeCounter struct {
	v   map[string]int
	mux sync.Mutex
}

// Inc 增加给定 key 的计数器的值。
func (c *SafeCounter) Inc(key string) {
	c.mux.Lock()
	// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
	c.v[key]++
	c.mux.Unlock()
}

// Value 返回给定 key 的计数器的当前值。
func (c *SafeCounter) Value(key string) int {
	c.mux.Lock()
	// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
	defer c.mux.Unlock()
	return c.v[key]
}

func main() {
	c := SafeCounter{v: make(map[string]int)}
	for i := 0; i < 1000; i++ {
		go c.Inc("somekey")
	}

	time.Sleep(time.Second)
	fmt.Println(c.Value("somekey"))
	//1000
}

RWMutex

sync.RWMutex 类型实现读写互斥锁,适用于读多写少的场景,它规定了当还在读取数据(即读锁占用)时,不允许更新这个数据(即写锁会阻塞);为了保证程序的效率,多个协程读取数据(拥有读锁)时,互不影响不会造成阻塞,它不会像 Mutex 那样只允许有一个协程读取同一个数据。读锁与读锁兼容,读锁与写锁互斥,写锁与写锁互斥。

  1. 可以同时申请多个读锁;
  2. 有读锁时申请写锁将阻塞,有写锁时申请读锁将阻塞;
  3. 只要有写锁,后续申请读锁和写锁都将阻塞。
    读锁:调用 RLock 方法开启锁,调用 RUnlock 释放锁;
    写锁:调用 Lock 方法开启锁,调用 Unlock 释放锁。
    为避免死锁,方法应成对出现,必要时请使用 defer
package main

import (
    "fmt"
    "sync"
    "time"
)

type BankV3 struct {
    balance int
    rwMutex sync.RWMutex // read write lock
}

func (b *BankV3) Deposit(amount int) {
    b.rwMutex.Lock() // write lock
    b.balance += amount
    b.rwMutex.Unlock() // wirte unlock
}

func (b *BankV3) Balance() (balance int) {
    b.rwMutex.RLock() // read lock
    balance = b.balance
    b.rwMutex.RUnlock() // read unlock
    return
}

func main() {
    var wg sync.WaitGroup
    b := &BankV3{}

    n := 1000
    wg.Add(n)
    for i := 1; i <= n; i++ {
        go func() {
            b.Deposit(1000)
            wg.Done()
        }()
    }
    wg.Wait()
    fmt.Println(b.Balance())
}

sync.Cond

Cond 实现了一个条件变量,在 Locker 的基础上增加的一个消息通知的功能,保存了一个通知列表,用来唤醒一个或所有因等待条件变量而阻塞的 Go 程,以此来实现多个 Go 程间的同步。
在调用 Signal,Broadcast 之前,应确保目标 Go 程进入 Wait 阻塞状态。

type Cond struct {
    ...
    L Locker
    ...
}

// 创建一个带锁的条件变量,Locker 通常是一个 *Mutex 或 *RWMutex
func NewCond(l Locker) *Cond

// 唤醒所有因等待条件变量 c 阻塞的 goroutine
func (c *Cond) Broadcast()

// 唤醒一个因等待条件变量 c 阻塞的 goroutine
func (c *Cond) Signal()

// 等待 c.L 解锁并挂起 goroutine,在稍后恢复执行后,Wait 返回前锁定 c.L,
// 只有当被 Broadcast 和 Signal 唤醒,Wait 才能返回。
func (c *Cond) Wait()
func listen(name string, s []string, c *sync.Cond) {
    c.L.Lock()
    c.Wait()
    fmt.Println(name, " 报名:", s)
    c.L.Unlock()
}

func broadcast(event string, c *sync.Cond) {
    time.Sleep(time.Second)
    c.L.Lock()
    fmt.Println(event)
    c.Broadcast()
    c.L.Unlock()
}

func main() {
    s1 := []string{"张三"}
    s2 := []string{"赵四"}
    s3 := []string{"刘能"}
    var m sync.Mutex
    cond := sync.NewCond(&m)

    // listener 1
    go listen("Go语言极简一本通", s1, cond)

    // listener 2
    go listen("Go语言微服务核心架构22讲", s2, cond)

    // listener 3
    go listen("从0到Go语言微服务架构师", s3, cond)

    // broadcast
    go broadcast("秒杀开始:", cond)

    ch := make(chan os.Signal, 1)
    signal.Notify(ch, os.Interrupt)
    <-ch
}

其他参考

请参考如何编写 Go 代码。如果需要标准库方面的帮助,请参考包手册。如果是语言本身的帮助,阅读语言规范
进一步探索 Go 的并发模型,参阅 Go 并发模型(幻灯片)以及深入 Go 并发模型(幻灯片)并阅读通过通信共享内存的代码之旅。
想要开始编写 Web 应用,请参阅一个简单的编程环境(幻灯片)并阅读编写 Web 应用的指南
函数:Go 中的一等公民展示了有趣的函数类型。
Go 博客有着众多关于 Go 的文章和信息。
mikespook 的博客中有大量中文的关于 Go 的文章和翻译。
开源电子书 Go Web 编程Go 入门指南能够帮助你更加深入的了解和学习 Go 语言。

3实战项目

实战项目主要是为巩固基础语法,并且是逐步开发、快速迭代改进,很有参考价值

猜数字游戏

设置随机数种子,随机生成100以内的数,提示用户输入,并和用户输入的数据比大小,根据大小提示信息,不断循环,最终猜正确才退出循环。通过该例子巩固变量循环、函数控制流和错误处理等知识

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.TrimSuffix(input, "\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
		}
	}
}

简单字典

抓包彩云小译翻译的api,并拷贝curl命令,通过网站转换成go代码,并拷贝服务端响应的json数据,通过网站生成go结构体,然后解析对应的字段输出,通过该例子学习发送http请求、解析json格式、使用代码生成提高开发效率等

package main

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

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

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

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

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

proxy

通过该例子,学习socks5协议工作原理,包含四个阶段:

  1. 握手阶段:浏览器向socks5代理发请求,服务器选一个认证方式返回给浏览器
  2. 认证阶段:开始认证流程,不概述
  3. 请求阶段:认证通过后浏览器向socks5服务器发请求,代理服务器收到响应后和服务器建立连接,然后一个响应
  4. replay阶段:浏览器发送请求,代理服务器接收到请求转发给服务器,接收到响应转发给浏览器
    在这里插入图片描述
package main

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

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

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

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

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

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

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

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

	buf := make([]byte, 4)
	_, err = io.ReadFull(reader, buf)
	if err != nil {
		return fmt.Errorf("read header failed:%w", err)
	}
	ver, cmd, atyp := buf[0], buf[1], buf[3]
	if ver != socks5Ver {
		return fmt.Errorf("not supported ver:%v", ver)
	}
	if cmd != cmdBind {
		return fmt.Errorf("not supported cmd:%v", ver)
	}
	addr := ""
	switch atyp {
	case atypIPV4:
		_, err = io.ReadFull(reader, buf)
		if err != nil {
			return fmt.Errorf("read atyp failed:%w", err)
		}
		addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
	case atypeHOST:
		hostSize, err := reader.ReadByte()
		if err != nil {
			return fmt.Errorf("read hostSize failed:%w", err)
		}
		host := make([]byte, hostSize)
		_, err = io.ReadFull(reader, host)
		if err != nil {
			return fmt.Errorf("read host failed:%w", err)
		}
		addr = string(host)
	case atypeIPV6:
		return errors.New("IPv6: no supported yet")
	default:
		return errors.New("invalid atyp")
	}
	_, err = io.ReadFull(reader, buf[:2])
	if err != nil {
		return fmt.Errorf("read port failed:%w", err)
	}
	port := binary.BigEndian.Uint16(buf[:2])

	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
}

4代码示例

以上语法和实战代码示例,都能再这里找到

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值