Go编程基础-1. 入门案例

6

1. 前言

  1. Go语言及其配套工具的设计目标是具有表达力,高效的编译和执行效率,有效地编写高效和健壮的程序。
  2. Go语言从C语言继承了表达式语法、控制流语句、基本数据类型、按值调用的形参传递以及指针。然而,更为重要的是,它继承了C语言所强调的核心原则:将程序编译为高效的机器码,并与所在操作系统提供的抽象机制自然地协同工作。Go语言家谱
  3. 在高级编程语言中,Go相对较晚出现,因而具备一定的后发优势。它的基础部分实现得相当不错,包括垃圾回收、包系统、一等公民函数、词法作用域、系统调用接口,以及默认使用UTF-8编码的不可变字符串。然而,相较而言,Go的语言特性相对较少,且在增加新特性方面表现不太活跃。例如,它不支持隐式数值类型强制转换,缺乏构造或析构函数,不具备运算符重载,不支持形参默认值,没有继承、泛型、异常、宏以及函数注解等功能,也不提供线程局部存储。

2. Go入门

2.1 Hello World

package main

import "fmt"

func main(){
    fmt.Println("Hello, world")
}

Go是编译性的语言,其官方工具链负责将程序的源文件转变为机器码。这些工具可以通过go+子命令进行使用:

  1. 最简单的子命令是run, 它将一个或多个以.go为后缀的源文件进行编译、链接,然后运行生成的可执行文件(并不会保存):go run helloworld.go
  2. 如果这个成功连续不是一次性脚本,那么编译输出一个可复用的程序会比较好。这可以通过build子命令来实现:go build helloworld.go
  3. Go对于代码的格式化要求非常严格。fmt子命令会调用gofmt工具来格式化指定包里的所有文件或者当前文件夹中的文件(默认情况下)。程序员应该养成对自己的代码使用gofmt工具的习惯,遵循一个标准的格式: go fmt

2.2 Go的包管理

Go代码使用包进行组织和管理,包类似于其他语言中的库和模块。

  1. 一个包由一个或多个 .go 源文件组成,这些文件被放置在一个文件夹中,该文件夹的名称描述了包的功能。
  2. 每个源文件的开头都使用 package 声明,例如,在这个例子中是 package main,明确指定了该文件属于哪个包。
  3. 后面是导入的其他包的列表,然后是存储在文件中的程序声明。

名为main的包相当特殊,其主要目的是定义独立的可执行程序而非库。无论在何种程序中,main函数始终是程序开始执行的入口。

2.3 命令行参数

os包提供了一些函数和变量,以与平台无关的方式与操作系统交互。

  1. 命令行参数以os.Args这个变量供外部程序访问,是一个字符串slice。
  2. os.Args的第一个元素是命令本身;另外的元素是程序执行时传入的命令行参数(os.Args[1:])。
package main

import (
	"fmt"
	"os"
)

func main() {
	var s, sep string
	for i := 1; i < len(os.Args); i++ {
		// 字符串相加的方式会不断生成新的字符串,给垃圾处理带来压力,更合理的方式是使用strings.Join(os.Args[1:]," ")
        s += sep + os.Args[i]
		sep = " "
	}
	fmt.Println(s)
}

示例代码如上,在执行go run .\osArgs.go hello world 时,会返回输入的命令行参数hello world

2.4 控制台交互&Map去重复

package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	counts := make(map[string]int)

	// 1. read input & dup
	input := bufio.NewScanner(os.Stdin)
	for input.Scan() {
		counts[input.Text()]++
	}

	// 2. print the res
	for line, n := range counts {
		fmt.Printf("%d\t%s\n", n, line)
	}
}
  1. 与控制台的交互方式和Java类似,将系统标准输入流os.Stdin封装到bufio.NewScanner()中,然后通过.Scan()方法判断是否抵达输入的结尾,并不断获取下一行输入.Test()
  2. 注意.Scan()遇到ctrl+c的终止命令时才会认为输入终止了,这与多数使用场景并不符合。因此要获取多少输入信息、什么时候需要终止,更多情况下是通过输入信息+代码逻辑,使用条件语句进行判断的。
  3. Go语言中,os.Stdin是*os.File指针,和os.Open()得到的文件指针一致,因此可以使用同一套代码逻辑处理控制台输入文件输入,只需要在外层控制逻辑上进行处理和兼容即可。
package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	counts := make(map[string]int)
	files := os.Args[1:]

	if len(files) == 0 {
		// 如果files为空,从命令行中读取输入并进行处理
		countLines(os.Stdin, counts)
	} else {
		// 如果files非空,读取文件中内容并进行去重
		for _, file := range files {
			f, err := os.OpenFile(file, os.O_RDONLY, 0)
			if err != nil {
				fmt.Fprintf(os.Stderr, "dup2: %v\n", err)
				continue
			}
			countLines(f, counts)
		}
	}

	// 打印结果
	for k, v := range counts {
		fmt.Printf("%s %d\n", k, v)
	}

}

// 统计文件中的行数(去重)
func countLines(f *os.File, counts map[string]int) {
	input := bufio.NewScanner(f)
	for input.Scan() {
		counts[input.Text()]++
	}
}

2.5 获取一个url响应

对于许多应用而言,访问互联网上的信息和访问本地文件系统同样重要。Go提供了一系列包,在net包下组织管理,使用它们可以方便地通过互联网发送和接收信息,利用底层的网络连接创建服务器。在这种情况下,Go的并发特性(详见第8章)尤为有用。下为使用net包获取url响应并输出的一个简单示例:

package main

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

func main() {
	for _, url := range os.Args[1:] {
		r, err := http.Get(url)
		if err != nil {
			fmt.Fprintf(os.Stderr, "fetch: %v\n", err)
			os.Exit(1)
		}
		b, err := io.ReadAll(r.Body)
		r.Body.Close()
		if err != nil {
			fmt.Fprintf(os.Stderr, "fetch: reading %s:%v\n", url, err)
			os.Exit(1)
		}
		fmt.Printf("%s", b)
	}
}

Go 最令人感兴趣和新颖的特点之一是其支持并发编程。这里我们只是简单了解一下 G0 的主要并发机制,包括 goroutine 和通道(channel)。程序 fetchall 和getch的类似,但它是并发获取多个 URL 内容。因此,这个进程花费的时间不会超过最耗时的获取任务所用的时间,而不是所有获取任务总共的时间。这个版本的 fetchall 会丢弃响应的内容,但会报告每一个响应的大小和花费的时间:

package main

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

func main() {
	start := time.Now()
	ch := make(chan string)
	for _, url := range os.Args[1:] {
		go fetch(url, ch) // 启动一个goroutine
	}
	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)
	}

	// 解析响应
	nbytes, err := io.Copy(io.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 是一个并发执行的函数, 通道是一种允许某一例程向另一个例程传递指定类型值的通信机制。

2.6 一个简单的Web服务器

使用Go的库可以非常容易实现一个Web服务器,用来响应客户端请求。例如,下述程序实现了一个简单的功能,记录所有请求的次数,同时/count返回到目前为止请求的个数:

package main

import (
	"fmt"
	"log"
	"net/http"
	"sync"
)

var mu sync.Mutex
var count int

func main() {
	serveMux := http.NewServeMux()
	serveMux.HandleFunc("/", handler)
	serveMux.HandleFunc("/count", counter)
	log.Fatal(http.ListenAndServe("localhost:8080", serveMux))
}

func handler(w http.ResponseWriter, r *http.Request) {
	mu.Lock()
	count++
	fmt.Println("handler: 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()
}

服务器的路由通过ServeMux来实现,其中HandleFunc函数将一个路径映射到一个处理函数(生成一个Handler对象),ServeMux保存一个路径到Handler的映射Map+一个按照路径长度从长到短排列的Hanler列表。在进行路由时,(1)首先根据Map进行查找,看看是否存在和实际请求路径完全匹配的路径模式,如果存在直接调用对应的处理函数;(2)如果不存在,则根据路径长度从长到短进行匹配,然后记录最长匹配的路径模式,并调用对应的处理函数。