这是我参与「第五届青训营 」伴学笔记创作活动的第 1 天
课程重点
- 安装Golang及其开发环境(IDE)
- 什么是Go?& Go语言基础语法
- 通过三个小项目深入了解Go语言
1. 安装Golang及其开发环境
Go是一门需要编译的编程语言,所以编译器和开发环境(如果你不想用记事本写代码)是必须的
- 1.1 安装编译器
用浏览器打开 studygolang.com/dl ( 会魔法的同学也可以打开go.dev )
选择自己电脑的系统下载对应的安装包安装即可
这里以Windows为例,下载go1.19.5.windows-amd64.msi安装即可
- 1.2 安装集成开发环境
Go语言可用的集成开发环境有很多(例如VSCode或者Goland)
可以选择自己喜欢的IDE进行下载,这里为下载VSCode的同学提醒一下:记得安装Go拓展
到这里, 我们就可以打开编辑器愉快的开始进行Go语言的开发啦!
注意:
当我们在下载Go语言的一些包的时候,有些同学可能会报错安装失败
我们只需要打开终端输入以下两行命令即可
go env -w GO111MODULE=on go env -w GOPROXY=https://goproxy.cn,direct
2.什么是Go?& Go语言基础语法
Go 是一个开源的编程语言,它能让构造简单、可靠且高效的软件变得容易
(优点就不列啦,太多了)
- 2.1 Go语言基础语法
在了解基础语法之前,首先我们要了解Go语言的语言结构是什么样的
在Go的代码中通常包含了以下部分:
- 包声明
- 引入包
- 函数
这里以一个简单的HelloWorld为例
package main <-包声明(main包是程序的入口包,程序代码执行会从这里开始)
import (
"fmt" <-引入包(将需要用到的包丢到括号里就可以啦,一行一个哦!)
)
func main() { <-函数( 函数通常以func定义,main函数是程序运行时执行的第一个函数)
fmt.Println("Hello, World!")
}
于是,当我们在终端中执行 go run (你的代码路径)时
Hello,World!就成功的显示在我们的屏幕上了
- 变量
在Go语言中声明一个变量可以有很多种方法:
var a = "init" //哇!有个a变量(雾)
var b, c int = 1, 2 //b = 1 , c = 2
d := true
Go是一门强类型语言,变量是有数据类型的(虽然也可以自动识别)
一般情况下,Go语言的数据类型声明在变量名之后
- 常量
常量跟变量很像,只是多了个const前缀
const a string = "Hello"
- 运算
字符串是Go的内置变量类型,可以直接进行运算哦!
a = "Hello "
b = "World!"
c := a + b //c = "Hello World!"
- 循环
在Go中,循环只有一种形式 —— for循环
可以直接写条件,也可以像C++的for循环那样,但是Go语言中for后是没有括号的
for { //无限循环(死循环)
fmt.Println("Hello Go!")
}
for i < n { //带条件的循环
fmt.Println("Hello Go!")
}
for i = 0; i < n; i++ { //C++式循环
fmt.Println("Hello Go!")
}
不知道有没有人注意到,Go语言的每一条语句后都是没有分号(;)的
因为编译器会自动的识别换行符作为语句结束的标志,因此我们在写循环时
不可以将for后的大括号写在下一行(if也是同理)
- 判断
判断语句同Go的循环语句一样,条件是不需要用括号括起来的
if i > n {
fmt.Println("Hello!")
} else {
fmt.Println("Go!")
}
- switch语句
Go的switch语句与C语言的写法基本一致,但是不同点是,每一个case内的语句结束后不需要break语句,Go语言会自动终止,而不是像C语言那样将之后所有的语句执行完
switch number {
case 1: //number为1时执行
fmt.Println("case 1")
case 2: //number为2时执行
fmt.Println("case 2")
default: //number非以上情况时执行
fmt.Println("Hello Go!")
}
- 数组
Go的数组一般不常用,因为长度不可变,下一个介绍的切片才是最常用的功能
var a [5]int //声明长度为5的整数型数组a
a[2] = 100 //将数组a的第三位树赋值为100
b := [5]int {1,2,3,4,5}
var c [5][5]int //二维数组
- 切片
本质上也可以说是数组。但是长度可变,非常好用!!
a := make([]string, 3) //创建长度为3的字符串型切片
a[0] = "aaa" //将切片a的第一位赋值为aaa
a = append(a,"bbb","ccc") //在切片a后增加两位,分别为bbb和ccc
注意:
append会返回一个新的切片,所以应当再次把他赋值给a才能完成对a的拓展
- Map
Map类似于词典或者哈希表,存储key - value的对应关系
a := make(map[string]int) //创建Map存储字符串(key)和整数(value)的对应关系
a["one"] = 1 //定义对应关系
fmt.Println(a[1]) //输出one
fmt.Println(a["one"]) //输出1
- range
range用于循环中对数组,切片或者Map进行遍历,用法如下
nums := {1,2,3,4}
for index, num := range nums { //index为索引,num为值
fmt.Println(num) //输出 1234
}
range函数在运行中会返回两个值,第一个是当前位置的索引(在数组的哪个位置)
第二个是当前位置的值
而在遍历Map时,第一个则是Key 第二个是Value
不需要的值可以填入下划线来忽略
- 指针
听到指针,部分学C/C++的同学就要站起来了,但是此指针非彼指针
Go语言的指针能执行的操作非常有限,举两个例子来说明一下吧
func add2(a int) {
n += 2
}
在这段代码中,我们想要实现一个给传入的数+2的操作,但是由于这是以形参的形式传入的,所以上面的这段代码可以说加了个寂寞
func add2(a *int) {
n += 2
}
add2(&n)
但是加了*就大不一样了,这是函数可以根据内存地址直接对n进行操作
同样的,传入参数也要加&表示传入指针(内存地址)
- 结构体
可以格式化的储存数据
type user struct { //创建User结构体
UserName string //格式:数据名 数据类型
PassWord string
}
var user1 user //用user结构体创建uesr1
user2 := user{"李四","pwd"} //带参构造方法
user1.UserName = "张三" //结构体数据操作
- 函数 & 错误处理
说了这么多,来说说Go语言程序最基础的单位——函数
Go的函数以func声明,返回值在函数名之后声明,传入值也是一样先名称后类型
不声明返回值则默认为无返回值类型
func add(a int, b int) int {
return a + b
}
Go语言有一个很有趣的特性——多返回值
这一特性在错误处理中有很多应用,例如:
func findUser(users []user, name string) (target *user, err error) {
for _,u := range users { //遍历数组寻找用户
if u.name == name {
return &u, nil //找到了返回用户和空的错误(nil在Go中是空的意思)
}
}
return nil, errors.New("Not found") //没有找到返回空用户和错误
}
我们只需要后续加以判断即可在出错时终止程序,防止更大的错误
- 字符串操作
这里倒是没啥好说的,看代码
a := "hello"
fmt.Println(strings.Contains(a,"l1")) // true
fmt.Println(strings.Count(a,"1")) // 2
fmt.Println(strings.HasPrefix(a, "he")) // true
fmt.Println(strings.HasSuffix(a,"1lo")) // true
fmt.Println(strings.Index(a,"11")) // 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
- 字符串格式化
p := point{1,2}
fmt.Printf("p=%v\n",p) // p={1 2}
fmt.Printf("p=%+v\n",p) // p={y:1 y:2}
fmt.Printf("p=%#v\n",p) // p=main.point{x:1, y:2}
- JSON处理
对于名称首字母都大写的结构体可以执行JSON处理
type user struct {
UserName string `json:"name"` //使用这样的写法可以将json中的UserName改为name
PassWord string
}
js, err := json.Marshal(user) //JSON格式化
umjs, err := json.Unmarshal(js) //去格式化
- 时间
引入time包可以进行时间操作
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()) //获取指定的数字
fmt.Println(t.Format("2006-01-02 15:04:05")) //格式化为:2022-03-27 01:25:36 字符串
diff :=t2.Sub(t) //两个时间点相差的时间段
fmt.Println(diff) //1h50m0s
fmt.Println(diff.Minutes(),diff.Seconds()) // 65 3900
t3, err := time.Parse("2006-01-02 15:04:05","2022-03-27 01:25:36") //把字符串解析回时间
fmt.Println(t3==t) //true
fmt.Println(now.Unix()) //获取时间戳
- 数字处理
strconv包可以帮助我们对数字进行一些处理
strconv.ParseFloat("1.234",64) //1.234
strconv.ParseInt("123",0,64) //123
这两个函数可以用于将字符串转换为数字类型,传入值第一个为字符串,第二个是进制数(如10进制,填0表示自动推测)第三个是转换的精度(64位)
或者我们也可以快速转换
strconv.Atoi("123") //字符串转数字
strconv.Itoa(123) //数字转字符串
注意:
以上关于数字处理的所有函数均有两个返回值,第二个为错误类型!
- 进程信息
os.Args //数组类型,第一位为运行路径 往后为启动时附带的参数
os.Setenv("AA","BB") //设置环境变量
os.Getenv("AA") //获取环境变量
3.通过三个小项目深入了解Go语言
- 猜数游戏
package main
import (
"bufio"
"fmt"
"math/rand"
"os"
"strconv"
"strings"
"time"
)
func main() {
rand.Seed(time.Now().Unix()) //设定随机数种子
target := rand.Intn(100) //获取随机数(最大为100)
reader := bufio.NewReader(os.Stdin) //创建Reader将输入转换为流
for {
fmt.Println("Please enter your answer: ")
input, err := reader.ReadString('\n') //读取一行输入
if err != nil {
fmt.Println("An error occrued while reading", err)
continue
}
input = strings.Trim(input, "\r\n") //去除换行符
ans, err := strconv.Atoi(input)
if err != nil {
fmt.Println("Invalid input, Please enter an integer")
continue
}
if ans > target { //判断大小
fmt.Println("It's too big, try again")
}else
if ans < target{
fmt.Println("It's too small, try again")
}else
if ans == target{
fmt.Println("Correct, congratulations!! ")
break
}
}
}
- 网络词典
解决思路:
- 通过浏览器调试观察正常翻译请求
- 通过网站Convert curl commands to Go将请求转化为代码
- 通过网站JSON转Golang Struct生成返回值结构体
- 编写请求内容结构体将自定义的请求内容发送至翻译网站
- 将返回值去格式化写入结构体中
- 获取结构体中的翻译和音标部分
- 输出到屏幕,完成!
代码太长就不放了,可以自行去Github克隆(紫色的是链接,可以点的!)
- Socket5代理服务端
在开始之前,我们需要了解Socket5代理是怎么工作的:
这里的每一步都需要我们使用代码来实现
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
}
}
首先是认证协商阶段
Socket5在连接时客户端会发送数据到服务端,包括:
- Socket5版本
- 支持的认证方法数
- 认证方法
我们使用的是无需认证,选择 00 方法即可
func auth(reader *bufio.Reader, conn net.Conn) (err error) {
ver, err := reader.ReadByte() //因为内容是有序的,前两部分各占用1字节,所以按字节读取
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)
}
_, err = conn.Write([]byte{socks5Ver, 0x00}) //返回认证成功数据
if err != nil {
return fmt.Errorf("write failed:%w", err)
}
return nil
}
接下来需要读取客户端的连接请求,并向目标服务器发送请求
连接请求包含:
- Socket5版本
- 请求类型 (通常是CONNECT请求)
- 保留字段
- 目标地址类型 (域名,iPv4,iPv6)
- 目标地址
- 目标端口
连接完成后向客户端发送回执并转发来自目标服务器的数据
func connect(reader *bufio.Reader, conn net.Conn) (err error) {
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) //ipv4
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") //ipv6(暂不支持)
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) //向终端打印输出信息
_, 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()) //利用context在连接中断前能保持转发
defer cancel()
go func() {
_, _ = io.Copy(dest, reader) //向目标服务器转发内容
cancel()
}()
go func() {
_, _ = io.Copy(conn, dest) //向客户端转发内容
cancel()
}()
<-ctx.Done()
return nil
}
今日的学习内容到这里就结束了,我们明天再见!