day1 go基础入门
这是我参与「第五届青训营 」笔记创作活动的第一天。
统一在这里贴一些看的时候遇到的问题:
- 字符串操作部分,Replace的具体参数,最后一个参数-1是什么意思
func Replace(s, old, new string, n int) string
返回将s中前n个不重叠old子串都替换为new的新字符串,如果n<0会替换所有old子串。
-1表示替换所有的"e"为E
- 猜数游戏中,生成随机数需要用到一个随机数种子,获取方法为:
rand.Seed(time.Now().UnixNano())
UnixNanao()的作用: Go语言中的Time.UnixNano()函数用于产生“t”(作为Unix时间),这是从1970年1月1日开始,以UTC为单位的秒数。 打印time.Now().UnixNano() 发现其值为距离1970年的秒数。
- input = strings.Trim(input, "\r\n")
Trim的用法没有在基础中讲到 \r\n是什么 为什么需要删掉换行符(为什么会有换行符)?
func Trim(s string, cutset string) string
返回s 前后所有cutset包含的utf-8码都去掉后的字符串
每次输入一个字符串并按下回车后,会通过\n这个关键字符来区分字符串。
bufio 在项目中的用处
为一个已有的Reader或者Writer提供缓冲。
操作系统的io是资源瓶颈,应该尽可能少的调用io操作,所以把大批量的数据一起读取或写入是更好的选择。
基础语法
基础语法部分之前有过学习,在这次课程中也发现了一些曾经没有注意到的东西比如:
go中常量没有固定类型,会根据上下文自动确定类型。
go中的switch分支结构 与C++的区别 C++中的switch如果不加break的话会走完所有的case,而go中的switch在找到一个符合的case后就不去跑其他分支了。
指针常用的用途是对传入的参数进行修改。
普通函数与结构体方法:
普通函数
func checkPassword(u user, password string) bool {
return u.password == password
}
该函数接收user的实例,string类型的password为参数,返回布尔值。
这里如果传入指针,即传入 u *user,就能实现对结构体的修改,避免大结构体的开销
结构体的方法
go中可以为结构体定义方法,类似类成员函数
package main
import "fmt"
type user struct {
name string
password string
}
// 为user定义方法
func (u user) checkPassword(password string) bool {
return u.password == password
}
func (u *user) resetPassword(password string) {
u.password = password //参数为指针时可以实现修改结构体
}
区别在于把结构体参数u搬到函数名前
错误处理
使用参数的返回值来传递错误信息 (在定义函数时返回值里 添加 err error)
在返回时,如果没有错误 返回原本结构和nil值。
字符串操作
strings包中有很多常用字符串工具函数。如:
contains判断一个字符串中是否包含另一字符串
count 字符串计数 如a:=hello中l的个数 strings.Count(a,"l")
index查找某个字符串的位置。 strings.Index(a,"ll") 返回2 我觉得这里比较神奇
join 连接多个字符串 strings.Join([]string{"he", "llo"},"-") he-llo 连在了中间
repaeat 重复多个字符串 strings.Repeat(a,2) hellohello
replace 替换字符串 strings.Replace(a,"e","E",-1) 看样子是把a中的e替换成E
func Replace(s, old, new string, n int) string
返回将s中前n个不重叠old子串都替换为new的新字符串,如果n<0会替换所有old子串。
-1表示替换所有的"e"为E
字符串格式化
字符串格式化。在标准库的FMT包里面有很多的字符串格式相关的方法,比如 printf这个类似于C语言里面的printf函数。不同的是,在go语言里面的话,你可以很轻松地用%v来打印任意类型的变量,而不需要区分数字字符串。你也可以用%+v打印详细结果,%#v则更详细。(打印结构体)
json处理
对于一个已有的结构体,只要保证每个字段的第一个字母是大写,也就是公开字段,那么这个结构体就能用JSON.marshaler去序列化,变成一个JSON字符串。
默认序列化出来的字符串,它的风格是大写字母开头,而不是下划线。可以在字段后面用json tag等语法来修改输出json结果里面的字段名。
序列化之后的字符串也能够用JSON.unmarshaler去反序列化到一个空的变量里。
时间处理
time包提供了对时间的处理。
time.now()
创建一个带时区的时间 t=time.Date(2022,3,27,1,25,36,0,time.UTC)
可以通过t.Year()等得到相关信息。
格式化:t.Format("2006-01-02 15:04:05")
可以通过diff :=t2.Sub(t)来得到t2和t之间的时间段,可以通过diff.Minutes()等得到相关信息。
time.Parse("2006-01-02 15:04:05","2022-03-27 01:25:36"),来将字符串转换成时间。
可以用time.UNIX来获得一个时间戳
数字解析
strconv包实现了字符串和数字类型的转换
可以用strconv.ParseFloat(str,64) 来解析字符串。
ParseInt(str,10,64) 10表示进展,传0表示自动取推测 64表示返回的是是64位精度的整数。
也可以使用Atoi来把一个十进制字符串转成数字 (与ParseInt有什么区别?)
如果输入不合法,则这些函数都会返回error
进程信息
os.Args来获取进程在执行时的一些命令行参数。 如 go run .go a b c d
os.Getenv/os.Setenv 来获取或写入环境变量
可以使用exec.Command 来快速启动子进程并获取输入输出。
实战
猜数游戏
1.生成随机数
package main
import (
"fmt"
"math/rand"
)
func main() {
maxNum := 100
secretNumber := rand.Intn(maxNum)
fmt.Println("The secret number is ", secretNumber)
}
运行发现,每次生成的随机数都是相同的。
官方文档中说明生成随机数时要先设置一个随机数种子,否则每次都会生成一个相同的数字。
一般的做法是在程序启动时用启动的时间戳来初始化随机数种子。
修改后:
func main() {
maxNum := 100
rand.Seed(time.Now().UnixNano())
secretNumber := rand.Intn(maxNum)
fmt.Println("The secret number is ", secretNumber)
}
结果会在0-100之间。
问题:UnixNano的作用?
2.读取用户的输入
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")
// 每一个程序执行时都会打开一些文件,如Stdin,Stdout,Stderr。
// 这里要用到的stdin文件可以由os.Stdin得到,直接操作该文件不方便。
// 所以使用bufio来将其转换成流。
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) // 输入的为str,转换成数字
if err != nil {
fmt.Println("Invalid input. Please enter an integer value")
return
}
fmt.Println("You guess is", guess)
}
每一个程序执行时都会打开一些文件,如Stdin,Stdout,Stderr。
这里要用到的stdin文件可以由os.Stdin得到,直接操作该文件不方便。
所以使用bufio来将其转换成流。
reader := bufio.NewReader(os.Stdin)
input = strings.Trim(input, "\r\n")
Trim的用法没有在基础中讲到 \r\n是什么 为什么需要删掉换行符
bufio 在项目中的用处
SOCKS5代理
一种代理协议,明文传输。
起源:一些企业为了安全性配置了严格的防火墙,带来的副作用是即使是管理员访问某些资源时也会很麻烦。SOCKS5协议相当于在防火墙上开了个口子,被授权的用户可以通过单个端口访问所有资源。
SOCKS5工作原理:
首先实现一个简单的,监听一个端口,并输入啥返回啥
先定义一个名为process的func,从命令行接收参数,并做处理:
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
}
}
}
先写一个 defer conn.Close(),会在函数即将退出时把连接关闭。
reader := bufio.NewReader(conn) 用这个连接创建一个只读的带缓冲的流。
在下面的for死循环中,
b,err:=reader.ReadByte() 每次去读一个字节,
用conn.Write去写入一个字节。
虽然看着是一个字节一个读,但实际上会有缓存和合并,读写的效率也不差。
在func main中,为了检查服务是否正常,我们要监听一个端口,返回一个server。
在死循环中用server.Accept接收一个请求,不成功的话返回error,如果成功的话就执行process。
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)
}
}
其中panic(err)用来取代return err的函数返回,代码更简洁?
go run后,使用 nc 127.0.0.1 1080 建立TCP连接。
然后在这个功能的基础上实现协议的第一步,认证。
func auth(reader *bufio.Reader, conn net.Conn) (err error)
这个函数体接收一个bufio只读流,以及一个原始的tcp连接。
把上面process的死循环改为调用该函数,认证失败返回错误信息。
在认证阶段,浏览器给代理服务器发送一个报文,报文包括三个字段。
第一个字段为version,协议版本号。
第二个字段为鉴权方式的数目
第三个就是对于每个鉴权方式的编码
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
}
ver, err :=reader.ReadByte() 读取版本号(为单字节) 如果出错的话直接返回错误信息,关闭连接。
methodSize, err := reader.ReadByte() 与上面类似,读取鉴权方式的数目,同样为单字节。
method := make([]byte, methodSize)
_, err = io.ReadFull(reader, method)
创建一个切片,并用methodSize来为其初始化。
ReadFull()函数用于从指定的读取器“r”读取到指定的缓冲区“buf”,并且复制的字节恰好等于指定缓冲区的长度。
此时就读到了version,methodSize,method 这三个字段。log.Printlin将他们打印出来。
_, err = conn.Write([]byte{socks5Ver, 0x00})
if err != nil {
return fmt.Errorf("write failed:%w", err)
}
return nil
}
用dapi.interpreter.caiyunai.comapi.interpreter.caiyunai.comapi.interpreter.caiyunai.comapi.interpreter.caiyunai.comapi.interpreter.caiyunai.capi.intapi.interpreter.caiyunai.comerpretapi.interpreter.caiyunai.comer.caiyunai.comapi.interpreter.caiyunai.comd1