这是我参与「第五届青训营」笔记创作活动的第1天。
Golang基础知识
Hello World
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello World")
}
main包是Golang的入口包,main函数是Golang入口函数。此处使用fmt中的Println函数进行输出。
变量
常见的变量类型有整数、浮点、布尔、字符串。
变量的定义有两种方式:
var a int = 1
var b = 2
c := 3
使用var定义变量时如果可以推导出类型就可以省略类型。定义常量时使用const关键字,且必须进行初始化。
运算符优先级与C语言基本相同,在此省略。
if语句
Golang中if-else语句与C语言大体相同,但是条件不使用()进行包含,且if后必须跟{。
使用示例如下:
if a == 0 {
fmt.Println(a)
}else {
fmt.Println(b)
}
if a == 1 {
fmt.Println(a)
}else if b == 2 {
fmt.Println(b)
}else {
fmt.Println(c)
}
循环语句
Golang中仅有for一种循环,格式与C语言大体相同。可以使用break和continue进行操作。for后分为三段,三段中任意一段都可以省略,全部省略即为死循环。
使用示例如下:
for {
fmt.Println("Hello")
break
}
for i := 0; i < 5; i++ {
if i%3 == 0 {
continue
}
fmt.Println(i)
}
i := 0
for i < 10 {
fmt.Println(i)
i++
}
switch分支
Golang中switch分支格式与C语言类似。但是csae语句触发时只执行当前分支的内容,不会像C语言一样继续向下执行。而且功能更为强大,支持表达式和各种数据类型。
使用示例如下:
a := 1
switch a {
case 1:
fmt.Println(2)
case 2:
fmt.Println(4)
default:
fmt.Println(100)
}
switch {
case a == 1:
fmt.Println(2)
case a > 5:
fmt.Println(4)
default:
fmt.Println(100)
}
数组
类似C语言中的数组,长度固定无法改变,使用方式相同,均使用索引访问。
使用示例如下:
var a [5]int
a[0] = 1
b := [5]int{1, 2, 3, 4, 5}
c := [...]int{1, 2, 3}
var d [2][3]int
切片
类似于可变长度的数组,同样通过索引直接访问。同时可以通过append函数进行扩充,copy进行拷贝。同时还满足类似python的切片操作。
使用示例如下:
a := make([]int, 3)
b := []int{1, 2, 3}
copy(a,b)
a[0] = 2
a = append(a, 1, 2)
// 0到2,不包括2
a = a[:2]
map
代表一种映射关系。用键值对表示,使用建访问值。使用delete函数删除一个键值对。使用range进行遍历。
使用示例如下:
a := make(map[int]string)
b := map[int]string{1: "12", 2: "23"}
a[1] = "a"
a[2] = "b"
delete(a, 2)
// false
v, l := a[0]
// a true
v, l = a[1]
for k, v := range a {
fmt.Println(k, v)
}
函数
函数定义使用func关键字。支持多返回值,且常用多返回值以表示是否出现错误。
使用示例如下:
func add(a int, b int) int {
return a+b
}
func add2(a int, b int) (int,bool){
return a+b,true
}
func add3(a int, b int) (c int,o bool){
c = a + b
o = true
return c,o
}
指针
支持指针,但支持十分有限。不支持C语言中的指针运算。
使用示例如下:
a := 1
var b *int
b = &a
fmt.Println(*b)
结构体
定义与C语言类似。对属性的使用与C语言类似。但是Golang中结构体可以定义结构体方法,借助该方法体现了面向对象的思想。
使用示例如下:
type student struct {
id int
name string
}
func main() {
a := student{id: 1, name: "ZhangSan"}
b := student{id: 2}
b.name = "LiSi"
fmt.Println(a.equal(&b))
}
func (s student) equal(s2 *student) bool {
return s.id == s2.id && s.name == s2.name
}
错误处理
与C语言和Java等都不同,使用返回值和if语句进行错误处理。使用panic进行错误打印
func main() {
v, err := divide(1, 0)
if err != nil {
panic(err)
return
}
fmt.Println(v)
}
func divide(a int, b int) (v int, err error) {
if b == 0 {
return 0,errors.New("divide by zero")
}
return a/b,nil
}
Golang实践练习
猜数游戏
要点一:随机数
使用math/rand包中的Intn方法获取随机数。但是需要使用Seed设置种子,否则每次获取的随机数都是相同的。
代码如下:
maxNum := 100
rand.Seed(time.Now().UnixNano())
secretNumber := rand.Intn(maxNum)
fmt.Println("The secret number is ", secretNumber)
要点二:输入
使用fmt包中的Scanln函数进行读取。
代码如下:
fmt.Println("Please input your guess")
guess := 0
_, err := fmt.Scanln(&guess)
if err != nil {
return
}
if err != nil {
fmt.Println("An error occured while reading input. Please try again", err)
return
}
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!")
}
最后将用户输入和游戏规则放入一个循环中游戏就完成了。
在线词典
要点一:发送请求并获取收到的请求
首先抓取彩云小译 - 在线翻译 (caiyunapp.com)的包,并获取请求url,然后在Convert curl to Go (curlconverter.com)中生成请求代码。
由于生成代码较多,此处省略。
要点二:定义请求的结构体并进行序列化
代码如下:
type DictRequest struct {
TransType string `json:"trans_type"`
Source string `json:"source"`
UserID string `json:"user_id"`
}
request := DictRequest{TransType: "en2zh", Source: "good"}
buf, err := json.Marshal(request)
要点三:结果反序列化
定义与返回结果对应的结构体,由于结构体过于复杂,所以使用JSON转Golang Struct进行生成。
由于生成代码较多,此处省略。
反序列化部分如下:
var dictResponse DictResponse
err = json.Unmarshal(bodyText, &dictResponse)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%#v\n", dictResponse)
要点四:获取命令行参数
使用os.Args进行获取。
具体代码如下:
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, `usage: simpleDict WORD
example: simpleDict hello
`)
os.Exit(1)
}
word := os.Args[1]
SOCLKS5
要点一:server测试
写一个简易的server测试是否正常。
server部分: 创建一个server侦听发送到该接口的信息并进行处理
代码如下:
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)
}
处理部分: 写入控制台中:
代码如下:
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)
}
要点二:SOCKS5初步实现协议-协商阶段
按照代理协议的第一部分协商的内容,使用代码实现解析与返回。并将处理部分的内容更改为协议第一部分。
代码如下:
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
}
要点三:SOCKS5初步实现协议-请求阶段
按照第二阶段,即请求阶段的报文规则进行编码,并将请求阶段放入协商阶段之后作为完成协商后的下一步。
代码如下:
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
}
要点四:SOCKS5初步实现协议-连接阶段
与真正的目的服务器建立连接,并将数据从客户服务器拷贝到目标服务器,即数据转发。
代码如下:
//建立连接
dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port))
if err != nil {
return fmt.Errorf("dial dst failed:%w", err)
}
defer dest.Close()
//数据转发
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
_, _ = io.Copy(dest, reader)
cancel()
}()
go func() {
_, _ = io.Copy(conn, dest)
cancel()
}()
<-ctx.Done()
全部完成后SOCKS5代理服务器就实现完成了。
个人总结
在本次课程中,接触到了Go语言的一些基础语法,同时通过实战学会了Go语言的一些基本运用。虽然在实践第三部分中使用到了Go语言独有的线程机制,但是大体思路也和前几个实践的思路相同,都是实现简单功能,只是有一个不太熟悉的协议规范,导致看起来和实现起来比较复杂,但是大体上还是比较容易的。