Go语言上手 — 基础语言 | 青训营笔记
这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记
记录我不熟悉与新学习的知识。比较零碎,不过对于切片的笔记比较详细,因为最近在看切片相关的文章。
目录
基础语言
实战
仅简单说明实现方法以及使用到的工具
Go语言有哪些优点
- 高性能、高并发
- 语法简单、学习曲线平缓
- 丰富的标准库
- 完善的工具链
- 静态链接
- 快速编译
- 跨平台
- 垃圾回收
Go的switch
与C/Cpp对比
在C/Cpp里面,
switch case如果不不显示加break的话会然后会继续往下跑完所有的case,在go语言里面的话是不需要加break的。
go语言里面的
switch功能强大。可以使用任意的变量类型,甚至可以用来取代任意的if-else语 句。可以在switch后面不加任何的变量,然后在case里面写条件分支,相比使用用多个if-else代码逻辑会更为清晰。
Go的slice
这部分我写的略微详细,因为最近都在看这个。
array 与 slice——需要区分声明方式
golang中的数组是一种由固定长度和固定对象类型所组成的数据类型。必须指定长度类型,或者在声明并初始化时使用
[...]
s = append(s, value)为什么需要s =
一方面append()函数签名为:
func append(slice []Type, elems ...Type) []Type; 另一方面,如果append导致超出切片cap,可以利用扩容机制返回新的切片
使用make()函数创建slice
make()除了类型,只提供一个参数len,创造出的slice的len与cap相等。slice使用make()函数创建时必须至少提供一个len。
深入了解slice
Go对slice、map、channel进行了特殊处理,平时可能会遇见一些让人费解的问题,这里写一些最近学到的知识。
Go只有值传递,可以尝试对传入函数之前与之后的变量的地址进行打印输出查看(注意:对于
slice没有用,打印的是底层数组的地址)
slice实现包含了底层数组的指针,值传递会复制指针地址,使指向同一个底层数组
slice实际可以强制输出到[:cap],故打印slice有时会出乎意料(eg.fmt.Println(slice)与fmt.Println(slice[:cap(slice)])
slice可以不用通过&而直接使用%p打印地址,实际fmt对打印slice的地址进行了优化,会使用反射机制打印底层数组的地址。
切片导致内存泄漏原因(之一):原切片由大量的元素构成,但是我们在原切片的基础上切片,虽然只使用了很小一段,但底层数组在内存中仍然占据了大量空间,(未使用部分)得不到释放
解决切片导致内存泄漏的方法
- 使用
append():源切片加入到新的切片并超出新切片的cap。- 使用
copy():复制到不同的切片,但是要注意目的切片的cap是否足够,以切断与底层共用的数组的联系。
PS:
这里简单记录一下,slice实际上有很多可以聊的,不过我没有仔细整理,可以去看看微信公众号脑子进煎鱼了一些关于slice的文章。
map
可以用
make()来创建一个空map,即不提供len也可以(这里的len可以理解为容量)。当一个map变量被创建后,可以指定map的容量,但是不可以在map上使用cap()方法。
可以直接通过
key创建新的键值对;可以用delete()删除键值对。
Go的
map是完全无序的,遍历的时候不会按照字母顺序,也不会按照插入顺序输出,而是随机顺序。
range
对于一个slice或者一个map的话,可以用range来快速遍历,这样代码能够更加简洁。range遍历的时候,对于数组会返回两个值,第一个是索引,第二个是对应位置的值。如果我们不需要索引的话,可以用下划线来忽略。
由于第一个返回值是索引,故只写一个返回值时,返回的只是索引
channel也可以使用range遍历。
func
支持多返回值,一般第二个返回值做错误处理
| 函数用法 | 描述 |
|---|---|
| 函数作为另外一个函数的实参 | 函数定义后可作为另外一个函数的实参数传入 |
| 闭包 | 闭包是匿名函数,可在动态编程中使用 |
在Go语言中,函数也是一种类型,可以和其他类型一样保存在变量中
函数变量与匿名函数
func main() {
var foo func(int, int) int // 声明一个函数类型
foo = func(a int, b int) int {
return a + b
}
fmt.Println(foo(1, 2)) // 3
foo2 := func() int {
a, b := 1, 1
return a + b
}
fmt.Println(foo2()) // 2
a, b := 1, 1
foo3 := func() int { // 闭包
a++
b++
return a + b
}
fmt.Println(foo3()) // 4
fmt.Println(foo3()) // 6
fmt.Println(foo3()) // 8
fmt.Println(foo3()) // 10
}
结构体方法
在实现结构体的方法的时候有两种写法,一种是带指针,一种是不带指针。
func (s *struct) foo() returnType {}与func (s struct) foo() returnType {}
区别:带指针可以对这个结构体去做修改。不带指针实际上操作的是一个拷贝,无法对结构体进行修改。
string strings包与strconv包
Go 语言中的字符串和其他高级语言(Java、C#)一样,默认是不可变的(immutable)。
字符串不可变有很多好处:
- 天生线程安全,大家使用的都是只读对象,无须加锁;
- 方便内存共享,而不必使用写时复制(Copy On Write)等技术;
- 字符串 hash 值只需要制作一份。
修改字符串时,可以将字符串转换为 []byte 进行修改。
[]byte 和 string 可以通过强制类型转换互转。
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 := "你好"
c := "郾一撾" // 测试生僻字
d := "🐟"
fmt.Println(len(b)) // 6
fmt.Println(len(c), len(d)) // 9 4;汉字看起来都是3字节
}
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
}
fmt.Printf
%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
}
json包
json 与 结构体
json.Marshal() 序列化,可以将结构体转化为[]byte
json.MarshalIndent(struct, "", "\t") 打印时更加清晰,不影响反序列化
json.Unmarshal(string, any) 反序列化,可以将[]byte转化为结构体, any接收体必须传递指针(&struct,无论any是否为指针类型)
func main() {
a := userInfo{Name: "wang 王", Age: 18, Hobby: []string{"Golang", "TypeScript"}, id: 1}
buf, err := json.Marshal(a)
if err != nil {
panic(err)
}
fmt.Println(buf) // [123 34 78 97...]
fmt.Println(string(buf)) // {"Name":"wang","age":18,"Hobby":["Golang","TypeScript"]}
buf, err = json.MarshalIndent(a, "", "\t")
if err != nil {
panic(err)
}
fmt.Println(string(buf))
var b userInfo
err = json.Unmarshal(buf, &b)
if err != nil {
panic(err)
}
fmt.Printf("%#v\n", b) // main.userInfo{Name:"wang", Age:18, Hobby:[]string{"Golang", "TypeScript"}}
}
time包
Go 对关于时间格式的操作有要求,时间一定是2006年1月2日15点4分5秒——简记为612345
func main() {
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()) // 2022 March 27 1 25
fmt.Println(t.Format("2006-01-02 15:04:05")) // 2022-03-27 01:25:36
diff := t2.Sub(t)
fmt.Println(diff) // 1h5m0s
fmt.Println(diff.Minutes(), diff.Seconds()) // 65 3900
t3, err := time.Parse("2006-01-02 15:04:05", "2022-03-27 01:25:36")
if err != nil {
panic(err)
}
fmt.Println(t3 == t) // true
fmt.Println(now.Unix()) // 1648738080
}
猜字游戏
仅保留关键
func main() {
maxNum := 100
rand.Seed(time.Now().UnixNano()) // 随机数种子
secretNumber := rand.Intn(maxNum)
...
reader := bufio.NewReader(os.Stdin) // 创建一个新的读取器
for {
input, err := reader.ReadString('\n')
...
input = strings.TrimSuffix(input, "\n") // 去除换行符 注意:windows下是\r\n
guess, err := strconv.Atoi(input)
//_, err := fmt.Scanf("%d", &guess) // 或者使用scanf
...
}
}
}
简单在线字典
仅保留关键
- 利用网站提供的接口实现
- 寻找POST请求
- 工具推荐 Convert Curl Conmmands to Go | JSON转Golang Struct
type DictRequest struct {
}
type DictResponse struct {
}
func query(word string) {
client := &http.Client{} // 创建请求
request := DictRequest{TransType: "en2zh", Source: word}
buf, err := json.Marshal(request) // 请求参数序列化
... // 设置请求头
var data = bytes.NewReader(buf)
req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)
...
resp, err := client.Do(req) // 发起请求
...
defer resp.Body.Close()
...
bodyText, err := ioutil.ReadAll(resp.Body) // 读取响应;用ioutil.ReadAll来读取这个流,能得到整个body
...
var dictResponse DictResponse
err = json.Unmarshal(bodyText, &dictResponse) // 获得的结果反序列化
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 { // 命令行参数个数不为2
fmt.Fprintf(os.Stderr, `usage: simpleDict WORD
example: simpleDict hello
`)
os.Exit(1)
}
word := os.Args[1]
query(word)
}
socks5代理
简单介绍
socks协议虽然是代理协议,但它并不能用来翻墙,它的协议都是明文传输。这个协议历史比较久远,诞生于互联网早期。它的用途是,比如某些企业的内网为了确保安全性,有很严格的防火墙策略,但是带来的副作用就是访问某些资源会很麻烦。
socks5相当于在防火墙开了个口子,让授权的用户可以通过单个端口去访问内部的所有资源。实际上很多翻墙软件,最终暴露的也是一个socks5协议的端口。
爬虫在爬取过程中很容易会遇到P访j问频率超过限制。这个时候很多人就会去网上找一些代理IP池,这些代理IP池里面的很多代理的协议就是socks。
原理图
socks5协议的工作原理
正常浏览器访问一个网站,如果不经过代理服务器的话,就是先和对方的网站建立TP连接,然后三次握手,握手完之后发起HTP请求,然后服务返回HTTP响应。如果设置代理服务器之后,流程会变得复杂一些。
首先是浏览器和socks5代理建立TCP连接,代理再和真正的服务器建立TCP连接。这里可以分成四个阶段,握手阶段、认证阶段、请求阶段、relay阶段。
第一个握手阶段,浏览器会向socks5代理发送清求,包的内容包括一个协议的版本号,还有支持的认证的种类,socks5服务器会选中一个认证方式,返回给浏览器。如果返回的是00的话就代表不需要认证,返回其他类型的话会开始认证流程,这里我们就不对认证流程进行概述了。
第三个阶段是请求阶段,认证通过之后训览器会socks5服务器发起请求。主要信息包括版本号,请求的类型,一段主要是connection请求,就代表代理服务器要和某个域名,或者某个IР地址某个端口建立TCP连接。代理服务器收到响应之后,会真正和后端服务器建立连接,然后返回一个响应。
第四个阶段是relay阶段。此时浏览器会发送正常发送请求,然后代理服务器接收到请求之后,会直接把请求转换到真正的服务器上。然后如果真正的服务器接收以后返回响应的话。那么也会把请求转发到浏览器这边。实际上代理服务器并不关心流量的细节,可以是HTTP流量,也可以是其它TCP流量。
点击查看代码🐋
package main
import (
"bufio"
"context"
"encoding/binary"
"errors"
"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") // TCP echo server
if err != nil {
panic(err)
}
for {
client, err := server.Accept() // Wait for connection
if err != nil {
log.Printf("Accept failed %v", err)
continue
}
go process(client) // Handle connection in new goroutine
}
}
func process(conn net.Conn) { // Handle connection
defer conn.Close()
reader := bufio.NewReader(conn) // Create reader, 缓冲流可以减少网络IO操作 减少底层系统调用次数
err := auth(reader, conn) // Authenticate 认证
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
}
}
// auth 认证
// 三个字段:版本号,认证方法数量,认证方法编码
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) // 读取methodSize个字节 支持的认证方法
if err != nil {
return fmt.Errorf("read method failed:%w", err)
}
// +----+--------+
// |VER | METHOD |
// +----+--------+
// | 1 | 1 |
// +----+--------+
_, err = conn.Write([]byte{socks5Ver, 0x00}) // 00表示不需要认证
if err != nil {
return fmt.Errorf("write failed:%w", err)
}
return nil
}
// connect 连接
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) // 创建一个4字节的缓冲区
_, err = io.ReadFull(reader, buf) // 上面的字段一共4字节 直接全部读取
if err != nil {
return fmt.Errorf("read header failed:%w", err)
}
ver, cmd, atyp := buf[0], buf[1], buf[3]
if ver != socks5Ver { // 判断是否为版本5
return fmt.Errorf("not supported ver:%v", ver)
}
if cmd != cmdBind { // 判断是否为 1
return fmt.Errorf("not supported cmd:%v", ver)
}
addr := ""
switch atyp {
case atypIPV4:
_, err = io.ReadFull(reader, buf) // IPV4 4个字节
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]) // 打印ipv4地址
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])
dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port)) // 建立tcp连接
if err != nil {
return fmt.Errorf("dial dst failed:%w", err)
}
defer dest.Close()
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)
}
ctx, cancel := context.WithCancel(context.Background()) // context机制 阻塞
defer cancel()
// 启动2个goroutine实现双向转发
go func() {
_, _ = io.Copy(dest, reader) // 单向数据转发 io.Copy(dist, src)
cancel()
}()
go func() {
_, _ = io.Copy(conn, dest)
cancel()
}()
<-ctx.Done()
return nil
}
其他推荐
总的来说,还是很建议去看看这些基础语法相关的深度文章,这里简单放了几篇