Go语言入门 | 青训营笔记

104 阅读3分钟

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

Go简介

Go 被称为 21世纪的C语言 ,继承了 C语言相似的很多特性,比如调用参数传值,指针等。

什么是 Go语言

go 语言有以下优点:

  • 高性能高并发
  • 语法简单易上手
  • 丰富的标准库
  • 完善的工具链
  • 静态链接
  • 快速编译
  • 跨平台
  • 垃圾回收

一些案例上手 Go

猜谜游戏

随机生成一个1-100的数字,然后让用户输入一个数字

如果用户输入的数字不对,就提醒用户重新输入一个更大或更小的值

 package main
 ​
 import (
     "fmt"
     "math/rand"
     "time"
 )
 ​
 func main() {
     fmt.Println("Please input your guess:")
     // 设置随机数种子
     rand.Seed(time.Now().Unix())
     maxNum := 100
     target := rand.Intn(maxNum)
 ​
     var num int
     for {
         fmt.Scanf("%d\n", &num)
         if num == target {
             fmt.Println("you are right")
             break
         }else if num > target {
             fmt.Println("your guess is bigger, please try again:")
         } else {
             fmt.Println("your guess is smaller, please try again:")
         }
     }
 }

命令行词典

在命令行输入一个单词,利用 http 访问现有的网站,获得该单词的翻译结果

 package main
 ​
 import (
     "bytes"
     "encoding/json"
     "fmt"
     "io/ioutil"
     "log"
     "net/http"
     "os"
 )
 ​
 type DictRequest struct {
     TransType string `json:"trans_type"`
     Source    string `json:"source"`
     UserID    string `json:"user_id"`
 }
 ​
 // 在网站 https://oktools.net/json2go 生成
 type DictResponse struct {
     Rc   int `json:"rc"`
     Wiki struct {
         KnownInLaguages int `json:"known_in_laguages"`
         Description     struct {
             Source string      `json:"source"`
             Target interface{} `json:"target"`
         } `json:"description"`
         ID   string `json:"id"`
         Item struct {
             Source string `json:"source"`
             Target string `json:"target"`
         } `json:"item"`
         ImageURL  string `json:"image_url"`
         IsSubject string `json:"is_subject"`
         Sitelink  string `json:"sitelink"`
     } `json:"wiki"`
     Dictionary struct {
         Prons struct {
             EnUs string `json:"en-us"`
             En   string `json:"en"`
         } `json:"prons"`
         Explanations []string      `json:"explanations"`
         Synonym      []string      `json:"synonym"`
         Antonym      []string      `json:"antonym"`
         WqxExample   [][]string    `json:"wqx_example"`
         Entry        string        `json:"entry"`
         Type         string        `json:"type"`
         Related      []interface{} `json:"related"`
         Source       string        `json:"source"`
     } `json:"dictionary"`
 }
 ​
 func main() {
     if len(os.Args) != 2 {
         fmt.Fprintf(os.Stderr, `usage simpleDict Word example: simpleDict hello`)
         os.Exit(1)
     }
     word := os.Args[1]
     query(word)
 }
 ​
 func query(word string) {
     client := &http.Client{}
 ​
     request := DictRequest{TransType: "en2zh", Source: word}
     buf, err := json.Marshal(request)
 ​
     if err != nil {
         log.Fatal(err)
     }
     var data = bytes.NewReader(buf)
     // 创建请求
     req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)
     if err != nil {
         log.Fatal(err)
     }
 ​
     // 设置请求头  在网站 https://curlconverter.com 生成
     req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:100.0) Gecko/20100101 Firefox/100.0")
     req.Header.Set("Accept", "application/json, text/plain, */*")
     req.Header.Set("Accept-Language", "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2")
     req.Header.Set("Accept-Encoding", "deflate, br")
     req.Header.Set("Content-Type", "application/json;charset=utf-8")
     req.Header.Set("X-Authorization", "token:qgemv4jr1y38jyq6vhvi")
     req.Header.Set("app-name", "xy")
     req.Header.Set("version", "1.8.0")
     req.Header.Set("os-type", "web")
     req.Header.Set("Origin", "https://fanyi.caiyunapp.com")
     req.Header.Set("Connection", "keep-alive")
     req.Header.Set("Referer", "https://fanyi.caiyunapp.com/")
     req.Header.Set("Sec-Fetch-Dest", "empty")
     req.Header.Set("Sec-Fetch-Mode", "cors")
     req.Header.Set("Sec-Fetch-Site", "cross-site")
     // 发起请求
     resp, err := client.Do(req)
     if err != nil {
         log.Fatal(err)
     }
     defer resp.Body.Close()
 ​
     // 读取响应
     bodyText, err := ioutil.ReadAll(resp.Body)
     if err != nil {
         log.Fatal(err)
     }
 ​
     if resp.StatusCode != 200 {
         log.Fatal("bad statusCode:", resp.StatusCode, "body")
     }
 ​
     var dictResponse DictResponse
 ​
     err = json.Unmarshal(bodyText, &dictResponse)
     if err != nil {
         log.Fatal(err)
     }
     fmt.Println(word, "UK:", dictResponse.Dictionary.Prons.En, "Us:", dictResponse.Dictionary.Prons.EnUs)
 ​
     for _, item := range dictResponse.Dictionary.Explanations {
         fmt.Println(item)
     }
 }
 ​

Golang入门

本章介绍 Go 语言的基础组件,提供一些入门程序示例

HelloWorld

梦的开始

如何用 go 输出 helloworld:

 package main
 ​
 import "fmt"
 ​
 func main(){
     fmt.Println("hello world!")
 }

go 是一门编译型语言,go 语言的工具链将源代码及其依赖转换成计算机的机器指令。

go 语言提供的工具都通过一个单独的命令 go 调用, go 命令有一系列子命令。比如 go run ,这个命令编译一个或多个以 .go 为结尾的源文件,链接库文件,并运行最终生成的可执行文件。

 $ go run helloworld.go

如果不只是一次性实验,只编译而不运行,则可以用 go build 命令

 $ go build helloworld.go

这个命令会生成一个名为 helloworld 的可执行的二进制文件,之后可以随时运行它

 $ ./helloworld
 hello world!

package

go的标准库提供了 100 多个包,以支持常见功能,比如输入输出,排序以及文本处理等。

比如 fmt 包就含有格式化输出、接收输入的函数。

main 包也比较特殊,它定义了一个独立可执行的程序,而不是一个库,在 main 包里的 main函数 也很特殊,是整个程序执行的入口。

获取命令行参数

大多数程序都是处理输入,产生输出,其中一个重要的输入源就是 命令行参数

os 包以跨平台的方式,提供了一些与操作系统交互的函数和变量,程序的命令行参数可从 os包 的 Args 变量获取

os.Args 是一个 字符串的切片 ,其中第一个参数 os.Args[0] 是命令本身的名字,其他的元素是程序启动时传给它的参数。

使用举例

获取命令行参数并打印

 package main
 ​
 import(
     "fmt"
     "os"
 )
 ​
 func main(){
     var s, sep string
     for i := 1; i < len(os.Args); i++{
         s += sep + os.Args[i]
         sep = " "
     }
     fmt.Println(s)
 }

循环操作

对于字符串 + 表示拼接操作

对于循环:go只有 for 一种循环,因为可以替代 java 中的 while 和 for

对标 java 的 for 循环

 for initcondition; overcondition; post{
 ​
 }

对标 java 的 while 循环

 for condition {
     
 }

for 循环还可以使用 range 遍历数据

 package main
 ​
 import(
     "fmt"
     "os"
 )
 ​
 func main(){
     var s, sep string
     for _,arg := range os.Args[1:]{
         s += sep + arg
         sep = " "
     }
     fmt.Println(s)
 }

每次循环过程中,range 产生一对值,索引以及索引在该处的元素值,因为 go 语言不允许出现未使用的变量,故用 _ 代替这种不需要的变量

变量声明方式

声明变量也有很多种方式:

 s := ""
 var s string
 var s = ""
 var s string = ""

第一种声明方式只能用于函数内部的变量,而不能用于包变量

第二种依赖于字符串的默认初始化机制

第三种用的不多,一般使用于同时声明多个变量

第四种显示表明变量的初始值,如果要指定的话就用第四种

Go 中字符串也是不变的,故每次 使用 + 连接会产生新字符串并复制给s,而s之前的内容则会被垃圾回收

故如果数据量很大的话,代价极高,推荐使用 strings 包的 Join 函数

代码如下:

 func main() {
     fmt.Println(strings.Join(os.Args[1:], " "))
 }

练习1.2 修改 echo 程序,打印每个参数的索引和值

 package main
 ​
 import (
     "fmt"
     "os"
 )
 ​
 func main() {
     for idx, val := range os.Args {
         fmt.Println(idx, val)
     }
 }

查找重复的行

在这个案例中会学到 map ,bufio包,判断语句 if 的基础内容

案例

代码如下:

 package main
 ​
 import (
     "bufio"
     "fmt"
     "os"
 )
 ​
 func main() {
     counts := make(map[string]int)
     input := bufio.NewScanner(os.Stdin)
 ​
     for input.Scan() {
         counts[input.Text()]++
     }
 ​
     for text, time := range counts {
         if time > 1{
             fmt.Println(text, time)
         }
     }
 }

if

if 语句的条件两边不需要加括号,但是主体部分一定要加大括号,大括号内是执行的内容

else 是可选项,同 java

map

map负责存储键值对,提供常数时间的存、取或测试操作,使用内置函数 make 创建 空map ,类似于 HashMap

map的访问可以使用 中括号, map[ key ] = value

bufio

Scanner 类型是该包最有用的特性之一,它读取输入并将其拆成 行或单词,通常是处理行形式的输入最简单的方法

创建 input 变量的方式:

 input := bufio.NewScanner(os.Stdin)

该变量从程序的标准输入中读取内容, 每次调用 input.Scan(),即读取下一行,并移除换行符。读取的内容可以调用 input.Text() 得到,在读到一行时返回 true,不再有输入时返回 false

读取文件中的内容

代码如下:

 package main
 ​
 import (
     "bufio"
     "fmt"
     "os"
 )
 ​
 func main() {
     counts := make(map[string]int)
 ​
     files := os.Args[1:]
 ​
     if len(files) == 0 {
         countLines(os.Stdin, counts)
     } else {
         for _, arg := range files {
             f, err := os.Open(arg)
             if err != nil {
                 fmt.Println(os.Stderr, "dup2: %v", err)
                 continue
             }
             countLines(f, counts)
             f.Close()
         }
     }
 ​
     for line, n := range counts {
         if n > 1 {
             fmt.Printf("%d\t%s\n", n, line)
         }
     }
 }
 ​
 func countLines(f *os.File, counts map[string]int) {
     input := bufio.NewScanner(f)
     for input.Scan() {
         counts[input.Text()]++
     }
 }

os.Open() 函数返回两个值,第一个是被打开的文件( *os.File ),其后被 Scanner 读取,第二个值是内置 error 类型的值,如果 err 等于内置值 nil(相当于null),那么说明文件被成功打开。接下来读取文件直到文件的末尾。

分割字符串

使用 strings.Split("", err)

 package main
 ​
 import (
     "fmt"
     "io/ioutil"
     "os"
     "strings"
 )
 ​
 func main() {
     counts := make(map[string]int)
 ​
     for _, filename := range os.Args[1:] {
         data, err := ioutil.ReadFile(filename)
         if err != nil {
             fmt.Fprintf(os.Stderr, "dup3: %v\n", err)
             continue
         }
         for _, line := range strings.Split(string(data), "\n") {
             counts[line]++
         }
     }
     for line, n := range counts {
         if n > 1 {
             fmt.Printf("%d\t%s\n", n, line)
         }
     }
 }

获取URL

Go 语言在 net 这个package的帮助下提供了一系列的 package 来访问互联网。

案例

获取对应的 url 并将其源文本打印出来

 package main
 ​
 import (
     "fmt"
     "io/ioutil"
     "net/http"
     "os"
 )
 ​
 func main() {
     for _, url := range os.Args[1:] {
         // net/http 下的 Get 方法
         resp, err := http.Get(url)
         if err != nil {
             fmt.Println(os.Stderr, "fetch ", err)
             os.Exit(1)
         }
         b, err := ioutil.ReadAll(resp.Body)
         resp.Body.Close()
         if err != nil {
             fmt.Println(os.Stderr, "fetch reading ", url, err)
             os.Exit(1)
         }
         fmt.Printf("%s", b)
     }
 }

http.Get 函数是创建 HTTP 请求得函数,如果过程没有错误,那么会在 resp 这个结构中得到访问的请求结果。resp 的 Body 字段包括一个可读的服务器响应流。 ioutil.ReadAll 函数从 response 中读取到全部内容,将其结果保留在变量 b 中。

使用 io.Copy(dst, src) 实现复制到文件中:

 package main
 ​
 import (
     "bufio"
     "fmt"
     "io"
     "net/http"
     "os"
     "strings"
 )
 ​
 func main() {
     for _, url := range os.Args[1:] {
         hasPrefix := strings.HasPrefix(url, "http://")
 ​
         // 没有前缀 加上前缀
         if !hasPrefix {
             url = "http://" + url
         }
 ​
         // 发送 http 请求
         resp, err := http.Get(url)
         if err != nil {
             fmt.Println(os.Stderr, "fetch ", err)
             os.Exit(1)
         }
 ​
         // 输出流的路径
         out, err := os.Create("/home/hz/test.txt")
         w := bufio.NewWriter(out)
         // 将 resp.Body的内容复制到 w
         written, err := io.Copy(w, resp.Body)
 ​
         // 输出大小
         fmt.Println(written)
 ​
         if err != nil {
             fmt.Println(os.Stderr, "fetch reading ", url, err)
             os.Exit(1)
         }
         resp.Body.Close()
 ​
         fmt.Println(resp.Status)
     }
 }

并发获取多个 URL

Go 语言最有意思且最新奇的特性就算对并发编程的支持。

本案例主要体验一下 goroutine 和 channel

fetchall

 package main
 ​
 import (
     "fmt"
     "io"
     "io/ioutil"
     "net/http"
     "os"
     "time"
 )
 ​
 func main() {
     start := time.Now()
     // 创建了一个传递 string 类型参数的 channel 
     ch := make(chan string)
 ​
     for _, url := range os.Args[1:] {
         go fetch(url, ch)
     }
 ​
     for range os.Args[1:] {
         // 打印 n 次 ch的内容
         fmt.Println(<-ch)
     }
     fmt.Printf("%.2fs elaspsed\n", time.Since(start).Seconds())
 ​
 }
 ​
 func fetch(url string, ch chan<- string) {
     start := time.Now()
     // 发送 http 请求
     resp, err := http.Get(url)
     if err != nil {
         ch <- fmt.Sprint(err)
         return
     }
     // 将响应结果 拷贝到 Discard输出流中 即垃圾箱
     nbytes, err := io.Copy(ioutil.Discard, resp.Body)
     resp.Body.Close()
     if err != nil {
         ch <- fmt.Sprintf("while reading %s: %v", url, err)
         return
     }
     secs := time.Since(start).Seconds()
     ch <- fmt.Sprintf("%.2fs  %7d  %s", secs, nbytes, url)
 }

一些讲解:

  • goroutine 是一种函数的并发执行方式,而 channel 是用来在 goroutine 之间进行参数传递。
  • main函数 本身也运行在一个 goroutine 中,而 go function 则表示创建一个新的 goroutine ,并在这个新的 goroutine 中执行这个函数
  • main 函数中 用make函数创建了一个传递 string 类型参数的 channel ,对每一个命令行参数,我们都用 go 这个关键字来创建一个 goroutine,并让函数在这个 goroutine 异步执行 http.Get() 方法。将响应的内容拷贝到 ioutil.Discard 输出流中,该变量相当于一个垃圾箱,存放不需要的数据。每当请求内容返回时,我们获取字节数,将字节数写入 channel ,由main函数中的循环来处理。
  • channel发送 操作将导致发送者 goroutine 阻塞,直到另一个 goroutine 在相同的 channel 上执行 接收 操作 ,当发送的值通过 channel 成功传输之后,两个 goroutine 可以继续执行后面的语句。反之,如果 接收 操作先发生,那么接收者 goroutine 也将阻塞,直到有另一个 goroutine 在相同的 channel 上执行 发送 操作。在该例子中,每一个 fetch函数的调用过程中,都会往 channel中写入一个值 (ch<-expression) ,主函数负责接收这些值 (<-ch) ,这个过程中我们用main函数来接收所有fetch函数回床的字符串,可以避免主线程提前结束。

Web服务

Go 语言的内置库 使得写一个类似 fetch 的 web服务器变得很简单

本节的内容是一个微型服务器,功能是返回当前用户正在访问的 URL

案例

 package main
 ​
 import (
     "fmt"
     "log"
     "net/http"
 )
 ​
 func main() {
     // 有点像拦截器,对于每个请求 都调用 handler
     http.HandleFunc("/", handler)
     // 监听 8000 端口号并打印错误日志
     log.Fatal(http.ListenAndServe("localhost:8000", nil))
 }
 ​
 func handler(w http.ResponseWriter, r *http.Request) {
     // 将 url 写入 响应对象
     fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
 }

如果想后台运行该程序,只需要在命令最后加一个 '&'

 $ go run net.go &

添加功能

计算用户的请求次数

 package main
 ​
 import (
     "fmt"
     "log"
     "net/http"
     "sync"
 )
 ​
 var mu sync.Mutex
 var count int
 ​
 func main() {
     http.HandleFunc("/", handler)
     // 对于 /count 开头的请求,进行计数打印
     http.HandleFunc("/count/", counter)
     log.Fatal(http.ListenAndServe("localhost:8000", nil))
 }
 ​
 func handler(w http.ResponseWriter, r *http.Request) {
     // 加上 Lock 防止并发问题
     mu.Lock()
     count++
     mu.Unlock()
     fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
 }
 ​
 func counter(w http.ResponseWriter, r *http.Request) {
     mu.Lock()
     fmt.Fprintf(w, "Count:%d\n", count)
     mu.Unlock()
 }
 ​

关于 http 后面会有详细说名,它的功能大概有:获取请求头信息,获取请求参数等