Go语言上手 — 基础语言 | 青训营笔记

167 阅读13分钟

Go语言上手 — 基础语言 | 青训营笔记

这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记

记录不熟悉与新学习的知识。比较零碎,不过对于切片的笔记比较详细,因为最近在看切片相关的文章。

目录

基础语言

实战

仅简单说明实现方法以及使用到的工具


Go语言有哪些优点

  1. 高性能、高并发
  2. 语法简单、学习曲线平缓
  3. 丰富的标准库
  4. 完善的工具链
  5. 静态链接
  6. 快速编译
  7. 跨平台
  8. 垃圾回收

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,创造出的slicelencap相等。slice使用make()函数创建时必须至少提供一个len

深入了解slice

Go对slicemapchannel进行了特殊处理,平时可能会遇见一些让人费解的问题,这里写一些最近学到的知识。

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是否为指针类型)

高性能json库

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

简单在线字典

仅保留关键

  1. 利用网站提供的接口实现
  2. 寻找POST请求
  3. 工具推荐 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

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
    }

其他推荐

总的来说,还是很建议去看看这些基础语法相关的深度文章,这里简单放了几篇

  1. channel用法
  2. Go语言copy():切片复制
  3. Go Slice Tricks Cheat Sheet
  4. 你不知道的 Go 之 string
  5. 你不知道的Go 之 slice

返回顶部