GO学习笔记

本文最后更新于:2024年7月11日 下午

跟着《GO圣经》学习GO啦!

Go的优势

  • 简单易学

    • Go语言的作者都有C的基因,Go自然而然也有了C的基因,但是Go的语法比C还简单, 并且几乎支持大多数你在其他语言见过的特性:封装、继承、多态、反射等
  • 丰富的标准库

    • Go目前已经内置了大量的库,特别是网络库非常强大
    • 前面说了作者是C的作者,所以Go里面也可以直接包含c代码,利用现有的丰富的C库
  • 跨平台编译和部署

    • Go代码可直接编译成机器码,不依赖其他库,部署就是扔一个文件上去就完事了. 并且Go代码还可以做到跨平台编译(例如: window系统编译linux的应用)
  • 内置强大的工具

    • Go语言里面内置了很多工具链,最好的应该是gofmt工具,自动化格式化代码,能够让团队review变得如此的简单,代码格式一模一样,想不一样都很困难
  • 性能优势: Go 极其地快。其性能与 C 或 C++相似。在我们的使用中,Go 一般比 Python 要快 30 倍左右

    • 语言层面支持并发,这个就是Go最大的特色,天生的支持并发,可以充分的利用多核,很容易的使用并发
    • 内置runtime,支持垃圾回收

Go环境搭建

Goland下载安装

Goland各版本下载

Goland破解教程

Go语言安装

官网地址

go version查看是否安装完成

开始GO!——Part1

笔者跟着《Go语言圣经》一起学习的~~😘

对应第一章

编写第一个GO程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
"fmt"
"io/ioutil"
"net/http"
"os"
)

func main() {
// 通过循环遍历命令行参数中的每个URL
for _, url := range os.Args[1:] {
// 使用HTTP GET请求获取URL的响应
resp, err := http.Get(url)
if err != nil {
// 如果发生错误,将错误信息输出到标准错误流并退出程序
fmt.Fprintf(os.Stderr, "fetch:%v\n", err)
os.Exit(1)
}

// 读取响应体的内容
b, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
// 如果在读取过程中发生错误,将错误信息输出到标准错误流并退出程序
fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err)
os.Exit(1)
}

// 将获取到的内容输出到标准输出
fmt.Printf("%s", b)
}
}

在命令行里执行go run .\Hello.go http://gopl.io,得到返回包,说明你的Go语言环境已经完全搭建成功啦!

命令行参数

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

os.Args变量是一个字符串的切片,区间索引时,同样是左闭右开

os.Args的第一个元素,os.Args[0], 是命令本身的名字;其它的元素则是程序启动时传给它的 参数。s[m:n]形式的切片表达式,产生从第m个元素到第n-1个元素的切片。如果省略切片表达式的m或n,会默认传入0或 len(s),因此切片可以简写成os.Args[1:]。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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里面注释使用//

  • GO初始定义的变量若是没有赋值,则隐式地被赋为零值,数值类型是0,字符串类型是空字符串””

  • 对于string类型的变量,+号可以直接连接字符串

  • 循环变量i没有定义类型,是因为。符号:= 是短变量声明的一部分, 这是定义一个或多个变量并根据它们的初始值为这些变量赋予适当类型的语句

  • GO中i++是语句,不是表达式,所以不能用于赋值,**j=i++非法,而且++--都只 能放在变量名后面,因此--i 也非法**

  • GO中只有for一种循环语句,但有多种形式

    • 第一种:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      for initialization; condition; post{

      }
      /*
      for 循环三个部分不需要括号包围,左大括号必须与post同一行
      initalization如果存在,必须是一条简单语句。即,短变量声明、自增语句、赋值语句或函数调用。
      condition 是一个布尔表达式(boolean expression),其值在每次循环迭代开始时计算。如果为 true 则执行循环体语句。
      post语句在循环体执行结束后执行,之后再次对conditon求值。condition 值为 false 时,循环结束
      */
    • 省略:

      1
      2
      3
      4
      5
      // a traditional "while" loop
      for condition {

      }
      //上面就是省略了初始化和post,变成了熟悉的while循环
    • 使用range

      1
      2
      3
      4
      5
      6
      7
      for _, arg := range os.Args[1:] {
      s += sep + arg
      sep = " "
      }
      /*for 循环的另一种形式, 在某种数据类型的区间(range)上遍历,如字符串或切片。
      大多数程序员都这么写for循环
      */

      每次循环迭代,range产生一对值:索引以及索引对应的元素值
      但是我们这个例子不需要索引,而range的语法要求:要处理元素,必须处理索引
      而且GO不允许使用无用的局部变量,会编译错误
      解决方法是使用空标识符_空标识符可用于任何语法需要变量名但程序逻辑不需要的时候

  • 声明一个变量有好几种方式,下面这些都等价:

    1
    2
    3
    4
    s := ""
    var s string
    var s = ""
    var s string = ""
  • 一个简洁的写法,使用strings包的Join函数

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

练习

练习 1.1

修改 echo 程序,使其能够打印 os.Args[0] ,即被执行命令本身的名字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
"os"
)

func main() {
var s, sep string
for i := 0; i < len(os.Args); i++ {
s += sep + os.Args[i]
sep = " "
}
fmt.Println(s)
}
练习 1.2

修改 echo 程序,使其打印每个参数的索引和值,每个一行。

1
2
3
4
5
func main() {
for n, arg := range os.Args[1:] {
fmt.Printf("%d %s\n", n, arg)
}
}
练习 1.3

做实验测量潜在低效的版本和使用了 strings.Join 的版本的运行时间差异。(1.6 节讲解了部分 time 包,11.4节展示了如何写标准测试程序,以得到系统性的性能评测。)

1
2
3
4
5
6
7
8
9
10
11
func main() {
start := time.Now()
//测试一
//for _, arg := range os.Args[1:] {
// fmt.Printf("%s ", arg)
//}
//测试二
fmt.Println(strings.Join(os.Args[1:], " "))
secs := time.Since(start).Seconds()
fmt.Printf("花费:%.8fs", secs)
}

使用Join函数确实可以大幅减少时间复杂度

查找重复的行

对文件做拷贝、打印、搜索、排序、统计或类似事情的程序都有一个差不多的程序结构:一 个处理输入的循环,在每个元素上执行计算处理,在处理的同时或最后产生输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
// 创建一个映射(map),用于存储每行文本及其出现次数
counts := make(map[string]int)
// 创建一个用于从标准输入读取的扫描器
input := bufio.NewScanner(os.Stdin)
// 循环读取标准输入的每一行文本
for input.Scan() {
// 将当前行文本作为键,增加其对应的出现次数
counts[input.Text()]++
}
// 遍历映射(map),打印出现次数大于1的行及其出现次数
for line, n := range counts {
if n > 1 {
fmt.Printf("%d\t%s\n", n, line)
}
}
}

对上面的代码可以有如下理解:

  • map是存储了键值对的集合,键可以是任意类型,只要其值可以用==运算符比较;值可以是任意类型。上面得的代码中,键是字符串,值是整数。内置函数make创建空函数

  • 打印结果使用range函数,在counts上迭代,每次得到键和值。需要注意的是,**map的遍历顺序不确定,该顺序随机,每次运行都会变化**。

  • 关于bufio包,这个包使得处理输入和输出方便高效。Scanner类型是该包最有用的特性之 一,它读取输入并将其拆成行或单词;通常是处理行形式的输入最简单的方法。 程序使用短变量声明创建bufio.Scanner类型的变量input。读取内容由input.Text()获取,在读到一行时返回true

  • fmt.Printf函数对一些表达式产生格式化输出。该函 数的首个参数是个格式字符串,指定后续参数被如何格式化。和C语言类似。后缀ffomartlnline

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    %d 十进制整数
    %x, %o, %b 十六进制,八进制,二进制整数。
    %f, %g, %e 浮点数: 3.141593 3.141592653589793 3.141593e+00
    %t 布尔:truefalse
    %c 字符(rune) (Unicode码点)
    %s 字符串
    %q 带双引号的字符串"abc"或带单引号的字符'c'
    %v 变量的自然形式(natural format)
    %T 变量的类型
    %% 字面上的百分号标志(无操作数)
  • fmt.Printf相当于C的printf函数,可以格式化参数;而fmt.Println函数相当于C++的cout函数,直接输出字符串

    1
    2
    3
    4
    5
    6
    name := "Alice"
    age := 30
    fmt.Printf("Name: %s, Age: %d\n", name, age)
    name := "Bob"
    age := 25
    fmt.Println("Name:", name, "Age:", age)

还有另一个方法,一口气把全部输入数据读到内存中,分割为多行,然后处理它们。这个例子引入 了ReadFile函数(来自于io/ioutil包),读取指定文件的全部内容,strings.Split函数把字符串分割成子串的切片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func main() {
// 创建一个映射(map),用于存储每行文本及其出现次数
counts := make(map[string]int)
// 循环遍历命令行参数中的每个文件名
for _, filename := range os.Args[1:] {
// 读取文件的内容
data, err := ioutil.ReadFile(filename)
if err != nil {
// 如果读取文件发生错误,将错误信息输出到标准错误流并继续处理下一个文件
fmt.Fprintf(os.Stderr, "xxx:%v\n", err)
continue
}
// 将文件内容按行拆分,并统计每行的出现次数
for _, line := range strings.Split(string(data), "\n") {
counts[line]++
}
}
// 遍历映射(map),打印出现次数大于1的行及其出现次数
for line, n := range counts {
if n > 1 {
fmt.Printf("%d\t%s\n", n, line)
}
}
}

ReadFile函数返回一个字节切片,必须把它转换为string,才能 用strings.Split分割。实现上,bufio.Scannerioutil.ReadFileioutil.WriteFile都使 用*os.FileReadWrite方法,但是,大多数程序员很少需要直接调用那些低级函数。

获取URL

Go语言在net这 个强大package的帮助下提供了一系列的package来做这件事情,使用这些包可以更简单地用 网络收发信息,还可以建立更底层的网络连接,编写服务器程序。

下面是一个使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func main() {
// 循环遍历命令行参数中的每个URL
for _, url := range os.Args[1:] {
// 使用HTTP GET请求获取URL的响应
resp, err := http.Get(url)
if err != nil {
// 如果发生错误,将错误信息输出到标准错误流并退出程序
fmt.Fprintf(os.Stderr, "fetch:%v\n", err)
os.Exit(1)
}
// 读取响应体的内容
b, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
// 如果在读取过程中发生错误,将错误信息输出到标准错误流并退出程序
fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err)
os.Exit(1)
}
// 将获取到的内容输出到标准输出
fmt.Printf("%s", b)
}
}

在文件资源目录下使用终端,命令:go run hello.go http://remixxyh.github.io

练习

练习1.7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
for _, url := range os.Args[1:] {
resp, err := http.Get(url)
if err != nil {
fmt.Fprintf(os.Stderr, "fetch:%v\n", err)
os.Exit(1)
}
_, err = io.Copy(os.Stdout, resp.Body)
resp.Body.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err)
os.Exit(1)
}
fmt.Printf("%s", b)
}
}

这里使用io.Copy函数,是题目所给的将内容直接拷贝到标准输出中,将 io.Copy 的返回值存储在一个匿名变量 _ 中,因为我们主要关心错误处理,而不是复制的字节数。

练习1.8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
for _, url := range os.Args[1:] {
if !strings.HasPrefix(url, "http://") {
url = "http://" + url
}
resp, err := http.Get(url)
if err != nil {
fmt.Fprintf(os.Stderr, "fetch:%v\n", err)
os.Exit(1)
}
_, err = io.Copy(os.Stdout, resp.Body)
resp.Body.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err)
os.Exit(1)
}
}
}
练习1.9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
for _, url := range os.Args[1:] {
if !strings.HasPrefix(url, "http://") {
url = "https://" + url
}
resp, err := http.Get(url)
fmt.Printf(resp.Status)
if err != nil {
fmt.Fprintf(os.Stderr, "fetch:%v\n", err)
os.Exit(1)
}
_, err = io.Copy(os.Stdout, resp.Body)
resp.Body.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err)
os.Exit(1)
}
}
}

并发获取多个URL

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func main() {
start := time.Now()
ch := make(chan string)
for _, url := range os.Args[1:] {
go fetch(url, ch)
}
for range os.Args[1:] {
fmt.Println(<-ch)
}
fmt.Printf("%.2fs elapsed\n", time.Since(start).Seconds())
}

func fetch(url string, ch chan<- string) {
start := time.Now()
resp, err := http.Get(url)
if err != nil {
ch <- fmt.Sprint(err)
return
}
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)
}

terminal里执行:go run hello.go https://www.baidu.com http://gopl.io https://remixxyh.github.io

将会得到:

我们可以发现整个程序执行的时间由获取请求最长的那个决定

这是一个并发请求的程序,分析上面的代码:

  • ch := make(chan string)是一个用于在Goroutine之间传递数据的管道,每当fetch函数执行完毕后,它会将一个带有消息的字符串发送到通道中
  • Goroutine是一种函数的并发执行方式,main函数本身也运行在一个goroutine中,而go function则表示创建一个新的goroutine,并在这个新的goroutine中执行这个函数
  • 这个程序里会异步执行Get方法,将Body拷贝到ioutil.Discard(可以把这个变量看成一个垃圾桶)输出流中,io.Copy函数会返回两个值(复制的字节数和可能的错误),从而实现不处理Body,得到响应体的字节数
  • 在使用通道发送或者接收数据时,如果此时通道已满或者主函数main没有做好接收的准备(注意:main函数也是一个Goroutine),那么会在发送数据的Goroutine那里阻塞,这个例子中就是fetch函数;如果通道为空或者没有数据可以使用,那么会在接收数据的Goroutine那里阻塞。所以这个例子在main使用一个for循环接收数据,是为了防止主函数执行完了但是fetch还没有执行完

Web服务

Go语言的内置库使得写一个类似fetch的web服务器变得异常地简单。在本节中,我们会展示 一个微型服务器,这个服务器的功能是返回当前用户正在访问的URL。比如用户访问的是 http://localhost:8000/hello ,那么响应是URL.Path = “hello”。

1
2
3
4
5
6
7
8
func main() {
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}

在浏览器里访问:

多个控制器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var mu sync.Mutex
var count int

func main() {
http.HandleFunc("/", handler)
http.HandleFunc("/count", counter)
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

// handler echoes the Path component of the requested URL.
func handler(w http.ResponseWriter, r *http.Request) {
mu.Lock()
count++
mu.Unlock()
fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}

// counter echoes the number of calls so far.
func counter(w http.ResponseWriter, r *http.Request) {
mu.Lock()
fmt.Fprintf(w, "Count %d\n", count)
mu.Unlock()
}

在这些代码的背后,服务器每一 次接收请求处理时都会另起一个goroutine,这样服务器就可以同一时间处理多个请求。在并发情况下,两个请求同一时刻去更新count,那么这个值可能并不会被正确地 增加;这个程序可能会引发一个严重的bug:竞态条件。

为了避免这个问题,我们必须保证每次修改变量的最多只能有一个goroutine,这也就是代码里的mu.Lock()mu.Unlock()调用将修改count的所有行为包在中间的目的。

第一章的学习就到这里,入门了GO👍👍👍


GO学习笔记
https://3xsh0re.github.io/2023/08/15/GO学习笔记/
作者
3xsh0re
发布于
2023年8月15日
许可协议