day01 Go 基础语法
这是我参与「第五届青训营 」伴学笔记创作活动的第 1 天
基础环境安装
go: go.dev/ ide: goland 基于云的开发环境:hi-hi.cn/gitpod
基础语法
hello world 示例
- 第一行 package main 代表这个文件属于main包的一部分,main包也就是持续的入口包
- 第三行导入了标准库里面的FMT包。这个包主要是用来往屏幕输入输出字符串,格式化字符串
- 直接运行 go run helloworld.go
-
- 二进制编译 go build 编译完成之后直接 ./helloworld 可以运行
package main
import (
"fmt"
)
func main() {
fmt.Println("hello world")
}
pkg.go.dev 可以查询函数文档
变量
go 语言是一门强类型语言,每一个变量都有它自己的变量类型 常见的变量类型包括 字符串 整数 浮点型 布尔型 go语言的字符串是内置类型 可以直接通过加号拼接 也能够直接用等于号去比较两个字符串 go语言中大部分运算符的使用和优先级都和C或者C++ 类似
package main
import (
"fmt"
"math"
)
func main() {
var a = "initial"
var b, c int = 1, 2
var d = true
var e float64
f := float32(e)
g := a + "foo"
fmt.Println(a, b, c, d, e, f) // initial 1 2 true 0 0
fmt.Println(g) // initialapple
const s string = "constant"
const h = 500000000
const i = 3e20 / h
fmt.Println(s, h, i, math.Sin(h), math.Sin(i))
}
变量声明
var a= "initial"
var e float32
f :=float32(e)
- var name string="" var 变量名字 类型=表达式
- var 变量名=表达式。自动推断变量类型
= 是一个变量赋值 := 是一个变量声明
数组是一个具有编号且长度固定的元素序列。声明数组的写法为 var a [5]int 或者 a[5] = 100。但是在真实的 codeing 中我们很少直接使用数组,因为数组长度无法扩展。我们更常用的是切片。
常量 const
const 没有确定的类型 会根据使用的上下文来自动确定类型
for 循环
go 没有while 循环 do while 循环 只有for 循环 同样 for 后面没有括号 接大括号 for 后面什么都不写 就是死循环 在循环里,可以用break或者continue来跳出循环
package main
import "fmt"
func main() {
i := 1
for {
fmt.Println("loop")
break
}
for j := 7; j < 9; j++ {
fmt.Println(j)
}
for n := 0; n < 5; n++ {
if n%2 == 0 {
continue
}
fmt.Println(n)
}
for i <= 3 {
fmt.Println(i)
i = i + 1
}
}
条件判断
if 后面没有括号 必须接大括号
func main() {
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")
}
}
switch 变量名不要括号 也不需要加break switch 变量可以使用任意变量类型 Go 语言不需要为每一个 case 添加 break,在执行完一个 case 后会自动跳出终止。Go 语言中的 swich 更加强大,判断条件可以是任意类型,采用以下写法可以替换掉大量 if else 的嵌套,使代码逻辑更加清晰。
func main() {
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("It's before noon")
default:
fmt.Println("It's after noon")
}
}
切片
声明切片 s := make([]string, 3)
slice可以像普通数组一样直接赋值,slice可以用 append 方法直接向slice最后添加元素 s = append(s, "e", "f")
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]
在创建时slice时也可以指定长度 c := make([]string, len(s)),可以用 copy(c, s) 进行复制。 slice还支持类似 python 中的slice操作,例如:s[2:5]//左闭右开。但与 python 不同的是 Go 中的slice不支持负数索引。
c := make([]string, len(s))
copy(c, s)
fmt.Println(c) // [a b c d e f]
slice 的原理是存储了一个长度和一个容量,加一个指向一个数组的指针,容量不够的话,会阔人并且返回新的slice
map
创建一个 map:m := make(map[string]int) 在这里方括号中是 key 类型,后面紧跟的是 value 类型。
完全无序
range
通过 Rang 可以快速遍历 Slice 和 map 使代码更加简洁 range 遍历的时候,对于数组会返回两个值,第一个是索引,第二个是对应位置的值,不需要索引的话,可以用下划线忽略
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
}
}
函数
函数以 func 关键字开头,后面紧跟函数签名。在 Go 语言中函数的返回类型写在函数参数列表之后 Go 语言中的函数可以返回多个值,可以使用这个特性在返回值的同时返回错误信息
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
}
指针
主要作用对传入参数进行修改
- 放在变量前修改成 指数类型 调用时加一个&号
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
}
结构体
- 声明一个结构体
type user struct {
name string
password string
}
- 初始化的两种方式 如果只指定一部分的值,对于没有初始化的值为空值。
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"
- 将结构体传入函数可以使用指针,也可以不用指针,如果使用指针,则可以对结构体的内容进行修改,而且可以避免一些对大结构体复制造成的性能损失。
func checkPassword(u user, password string) bool{...}
func checkPassword2(u *user, password string) bool{...}
- 可以给结构体定义方法,类似于其他面向对象的语言中的类成员方法。
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
}
错误处理
Go 语言中我们习惯用一个单独的返回值来传递错误信息,这样可以准确的定位到错误的位置,也很方便进行流程控制。
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() {
a := "hello"
fmt.Println(strings.Contains(a, "ll")) // true
fmt.Println(strings.Count(a, "l")) // 2
fmt.Println(strings.HasPrefix(a, "he")) // true
fmt.Println(strings.HasSuffix(a, "llo")) // true
fmt.Println(strings.Index(a, "ll")) // 2
fmt.Println(strings.Join([]string{"he", "llo"}, "-")) // he-llo
fmt.Println(strings.Repeat(a, 2)) // hellohello
fmt.Println(strings.Replace(a, "e", "E", -1)) // hEllo
fmt.Println(strings.Split("a-b-c", "-")) // [a b c]
fmt.Println(strings.ToLower(a)) // hello
fmt.Println(strings.ToUpper(a)) // HELLO
fmt.Println(len(a)) // 5
b := "你好"
fmt.Println(len(b)) // 6
}
格式化
可以用%v来打印任意类型的变量 %+v 打印详细结果 %#v 更详细
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
}
数字解析
关于字符串和数字类型之间的转换都在STR conv这个包下 可以用parseInt 或者 parseFloat 解析一个字符串 Atoi把一个十进制转成数字 itoA把数字转成字符串
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
}
Json 处理
对于一个现有的结构体我们只需要将结构体的每一个成员首字母大写(即让其变为公开字段)即可使用 json.Marshal 方法对其进行序列化。同时我们也可以对一个序列化后的字符串使用 json.Unmarshal 进行反序列化。在输出时要使用 string 类型转换,否则将输出为十六进制数字。如果不想要输出的字段名为大写,可以给字段后加加一个 tag 或者说是别名
type userInfo struct {
Name string
Age int `json:"age"`
Hobby []string
}
时间
- time.Now() 直接获取当前的时间
- time.Date(2022, 3, 27, 1, 25, 36, 0, time.UTC) 创建一个带时区的时间。
- t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute() 分别获取年月日时分
- t1.sub(t2) 用 t1 减去 t2 获取一个时间段
- t.Format("2006-01-02 15:04:05") 格式化时间,注意这里的格式化模板一定要使用这个 2006-01-02 15:04 :05
- time.Parse("2006-01-02 15:04:05", "2022-03-27 01:25:36") 将字符串按模板解析为时间
- t.Unix() 将时间转换为时间戳
进程信息
可以使用 os 包来获取进程和系统的一些信息,例如使用 os.Args 就可以获取到运行程序时的参数。 os.Getenv(key string) 和 os.Setenv(key string, balue string) 可以获取和设置环境变量。 exec.Command("grep", "127.0.0.1", "/etc/hosts").CombinedOutput() 可以快速启动子进程,并获取输出
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
}
实战
猜谜游戏
创建一个程序随机生成一个数字,程序要告诉玩家猜的数字是大于目标数字还是小于目标数字,然后玩家需要程序的提示尽快猜出数字是多少。
创建随机数
引入 math/rand 包,使用其中的 rand.Intn(n int) int 方法来创建随机数字,这个方法需要一个参数,来确定随机生成数字的最大值。直接使用 rand.Intn(n int) int 这个方法就会发现,每一次生成的随机数是相同的,不符合预期。 调用 rand.Intn(n int) int 之前应该为他设置一个随机种子,使用当前时间戳来作为这个种子 rand.Seed(time.Now().UnixNano()),再次调用时就可以看到每一次生成的数字时不同的了。
func main() {
maxNum := 100
rand.Seed(time.Now().UnixNano())
secretNumber := rand.Intn(maxNum)
fmt.Println("The secret number is ", secretNumber)
}
处理输入
使用标准库中的 bufio 和 os 包来读取用户输入,创建一个 bufio.Reader,从 os.Stdin 中读取输入流。使用 ReadString 方法来读取输入流,直到遇到换行符为止。将这个读取到的字符转换为数字,记得要检查是否有读取错误,是否符合预期,如果有错误打印错误信息并重新读取
func main() {
fmt.Println("Please input your guess")
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')
if err != nil {
fmt.Println("An error occured while reading input. Please try again", err)
return
}
input = strings.Trim(input, "\r\n") // 去掉换行符
guess, err := strconv.Atoi(input) //转换成数字
if err != nil {
fmt.Println("Invalid input. Please enter an integer value")
return
}
fmt.Println("You guess is", guess)
}
逻辑判断 和 循环
func main() {
// 生成随机数
for {
// 处理输入
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
}
}
}
课后作业:使用 scanner 替换 reader
// ...
scanner := bufio.NewScanner(os.Stdin)
for {
scanner.Scan()
input := scanner.Text()
guess, err := strconv.Atoi(input)
if err != nil {
fmt.Println("Invalid input. Please enter an integer value")
continue
}
fmt.Println("You guess is", guess)
// ...
}
命令行在线词典
这个程序需要做到在调用程序时,将要查询的英文单词作为程序参数传入,程序将调用第三方 API 查询出单词的音标与含义输出到控制台。
开发者工具抓包
以彩云科技提供的在线翻译为例,在彩云翻译的网页,打开浏览器的开发者工具。 network中有一个叫dict的post请求
{trans_type: "en2zh", source: "one"}
source: "one"
trans_type: "en2zh"
返回的响应体
dictionary
:
{prons: {en-us: "[wʌn]", en: "[wʌn]"},…}
antonym
:
[]
entry
:
"one"
explanations
:
["num.一", "pron.某一人(物);一致;协力", "n.一岁;一人(物);一个单位", "a.一个的;一致的;唯一的;整体的;同种的;特定的"]
0
:
"num.一"
1
:
"pron.某一人(物);一致;协力"
2
:
"n.一岁;一人(物);一个单位"
3
:
"a.一个的;一致的;唯一的;整体的;同种的;特定的"
prons
:
{en-us: "[wʌn]", en: "[wʌn]"}
en
:
"[wʌn]"
en-us
:
"[wʌn]"
related
:
[]
source
:
"wenquxing"
synonym
:
[]
type
:
"word"
wqx_example
:
[["one up on", "胜过,超过;优于"], ["one too many for", "对…来说是多余的,超过了…的能力"],…]
rc
:
0
wiki
:
{known_in_laguages: 17, description: {source: "Japanese aristocratic lady", target: null},…}
description
:
{source: "Japanese aristocratic lady", target: null}
source
:
"Japanese aristocratic lady"
target
:
null
id
:
"Q1146603"
image_url
:
"http://www.caiyunapp.com/imgs/link_default_img.png"
is_subject
:
"false"
item
:
{source: "Nene", target: "北の政所"}
source
:
"Nene"
target
:
"北の政所"
known_in_laguages
:
17
sitelink
:
"https://www.caiyunapp.com/read_mode/?id=6379935efa6a0b9368fc4365"
接下来用go程序发送一个这样的post的请求
生成go程序
右键 Copy as cURL 使用 Convert curl commands to Go快速获取这个请求的 Go 语言写法
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
)
func main() {
client := &http.Client{}
var data = strings.NewReader(`{"trans_type":"en2zh","source":"one"}`)// 字节数组 用的strings.newReader
// 创建请求
req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)
if err != nil {
log.Fatal(err)
}
// 设置请求头
req.Header.Set("authority", "api.interpreter.caiyunai.com")
req.Header.Set("accept", "application/json, text/plain, */*")
req.Header.Set("accept-language", "zh-CN,zh;q=0.9")
req.Header.Set("app-name", "xy")
req.Header.Set("content-type", "application/json;charset=UTF-8")
req.Header.Set("device-id", "")
req.Header.Set("origin", "https://fanyi.caiyunapp.com")
req.Header.Set("os-type", "web")
req.Header.Set("os-version", "")
req.Header.Set("referer", "https://fanyi.caiyunapp.com/")
req.Header.Set("sec-ch-ua", `"Not_A Brand";v="99", "Google Chrome";v="109", "Chromium";v="109"`)
req.Header.Set("sec-ch-ua-mobile", "?0")
req.Header.Set("sec-ch-ua-platform", `"macOS"`)
req.Header.Set("sec-fetch-dest", "empty")
req.Header.Set("sec-fetch-mode", "cors")
req.Header.Set("sec-fetch-site", "cross-site")
req.Header.Set("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36")
req.Header.Set("x-authorization", "token:qgemv4jr1y38jyq6vhvi")
// 发起请求
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
// 读取响应
defer resp.Body.Close()
bodyText, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", bodyText)
}
body同样是一个流,在golang 里面。为了避免资源泄露。需要加一个defer来手动关闭这个流,defer会在这个函数运行之后去执行。 接下来用ioutil.ReadAll 读取这个流,得到整个body
在golang里面,需要生成一段JSON,常用方式是先构造一个结构体,这个结构体和需要生成的json的结构是一一对应的
type userInfo struct{
Name string
Age int
Hobby []string
}
创建一个结构的实例,使用读取到的参数来初始化这个结构体的实例。再用 json.Marshal 方法序列化这个结构体变成一个 byte 数组。这次使用 bytes.NewReader 将 byte 数组转换成流,完成请求的构建。
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)
}
解析响应
最佳的处理方案是和请求体一样创建一个对应的结构体,将返回的 JSON 数据反序列化到这个结构体里面。
首先在浏览器开发工具中将返回的 JSON 数据复制,粘贴到 JSON转Golang Struct - 在线工具 - OKTools 中
https://oktools.net/json2go
(如果你使用 GoLand 开发那么可以直接将 JSON 粘贴到 GoLand 中,GoLand 将自动提示是否将 JSON 转换为 Go 结构体)。
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"`
}
将这个巨大的结构生成后,在程序的末尾添加反序列化
var dictResponse DictResponse
err = json.Unmarshal(bodyText, &dictResponse)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%#v\n", dictResponse)
总结
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)
}
课后作业
package main
import (...)
type DictResponse struct {
TranslationList []struct {
Translation string `json:"Translation"`
DetectedSourceLanguage string `json:"DetectedSourceLanguage"`
} `json:"TranslationList"`
ResponseMetadata struct {
RequestID string `json:"RequestId"`
// ...
Error interface{} `json:"Error"`
} `json:"ResponseMetadata"`
}
const (
kAccessKey = "..."
kSecretKey = "..."
kServiceVersion = "2020-06-01"
)
var (
ServiceInfo = &base.ServiceInfo{
// ...
}
ApiInfoList = map[string]*base.ApiInfo{
// ...
}
)
func newClient() *base.Client {
client := base.NewClient(ServiceInfo, ApiInfoList)
client.SetAccessKey(kAccessKey)
client.SetSecretKey(kSecretKey)
return client
}
func main() {
if len(os.Args) != 2 {
_, err := fmt.Fprintf(os.Stderr, `usage: simpleDict WORD example: simpleDict hello`)
if err != nil {
return
}
os.Exit(1)
}
word := os.Args[1]
client := newClient()
resp, code, err := client.Json("TranslateText", nil, `{"TargetLanguage":"zh","TextList":["`+word+`"]}`)
if err != nil {
log.Fatal(err)
}
if code != http.StatusOK {
log.Fatal("http status code:", code)
}
var dictResponse DictResponse
err = json.Unmarshal(resp, &dictResponse)
if err != nil {
log.Fatal(err)
}
fmt.Println(word, ":", dictResponse.TranslationList[0].Translation)
}
socket 代理服务器
正常访问网站,首先是浏览器和socket代理建立TCP连接,代理再和真正的服务器建立起TCP连接。可分为四个阶段:握手阶段,认证阶段,请求阶段,relay阶段
TCP echo server
main函数里 net.listen监听一个端口,返回一个server 死循环里 accept一个请求 返回一个连接 process函数处理连接
package main
import (
"bufio"
"log"
"net"
)
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)
for {
b, err := reader.ReadByte()
if err != nil {
break
}
_, err = conn.Write([]byte{b})
if err != nil {
break
}
}
}
代理 认证阶段
已经有一个返回输入信息的TCP sever 首先实现一个空的auth函数 在process函数里调用 认证阶段的逻辑,浏览器会给代理服务器发一个包,有三个字段,第一个version,第二个methods,第三个每个method的编码 代理服务器返回的response 包括两个字段 一个是version 一个是method 鉴传方式
package main
import (
"bufio"
"fmt"
"io"
"log"
"net"
)
const socks5Ver = 0x05
const cmdBind = 0x01
const atypIPV4 = 0x01
const atypeHOST = 0x03
const atypeIPV6 = 0x04
func main() {
server, err := net.Listen("tcp", "127.0.0.1:1080")
if err != nil {
panic(err)
}
for {
client, err := server.Accept()
if err != nil {
log.Printf("Accept failed %v", err)
continue
}
go process(client)
}
}
func process(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
err := auth(reader, conn)
if err != nil {
log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
return
}
log.Println("auth success")
}
func auth(reader *bufio.Reader, conn net.Conn) (err error) {
// +----+----------+----------+
// |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)
}
log.Println("ver", ver, "method", method)
// +----+--------+
// |VER | METHOD |
// +----+--------+
// | 1 | 1 |
// +----+--------+
_, err = conn.Write([]byte{socks5Ver, 0x00})
if err != nil {
return fmt.Errorf("write failed:%w", err)
}
return nil
}
代理 请求阶段
实现一个和auth函数类似的connect函数,读取携带URL或者Ip地址+端口的包
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])
log.Println("dial", addr, port)
// +----+-----+-------+------+----------+----------+
// |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
// +----+-----+-------+------+----------+----------+
// | 1 | 1 | X'00' | 1 | Variable | 2 |
// +----+-----+-------+------+----------+----------+
// VER socks版本,这里为0x05
// REP Relay field,内容取值如下 X’00’ succeeded
// RSV 保留字段
// ATYPE 地址类型
// BND.ADDR 服务绑定的地址
// BND.PORT 服务绑定的端口DST.PORT
_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
if err != nil {
return fmt.Errorf("write failed: %w", err)
}
return nil
}
代理 relay阶段
net.dail 建立网络连接
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)
建立浏览器和下游服务器的双向数据转发 标准库的io.copy 单向转发 双向转发 需要启动两个goroutinue
_, 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
用context连with cancel来创建一个context 在最后等待ctx.Done(),只要cancel被调用,ctx.Done就会立刻返回。然后两个goroutinue里面调用一次cancel就可以了