go语言基础与小实战 | 青训营笔记

61 阅读9分钟

day1 go基础入门

这是我参与「第五届青训营 」笔记创作活动的第一天。

统一在这里贴一些看的时候遇到的问题:

  1. 字符串操作部分,Replace的具体参数,最后一个参数-1是什么意思

func Replace(s, old, new string, n int) string

返回将s中前n个不重叠old子串都替换为new的新字符串,如果n<0会替换所有old子串。

-1表示替换所有的"e"为E

  1. 猜数游戏中,生成随机数需要用到一个随机数种子,获取方法为:

rand.Seed(time.Now().UnixNano())

UnixNanao()的作用: Go语言中的Time.UnixNano()函数用于产生“t”(作为Unix时间),这是从1970年1月1日开始,以UTC为单位的秒数。 打印time.Now().UnixNano() 发现其值为距离1970年的秒数。

  1. 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