Go编程基础-1. 入门案例
入门案例
1. 前言
- Go语言及其配套工具的设计目标是具有表达力,高效的编译和执行效率,有效地编写高效和健壮的程序。
- Go语言从C语言继承了表达式语法、控制流语句、基本数据类型、按值调用的形参传递以及指针。然而,更为重要的是,它继承了C语言所强调的核心原则:将程序编译为高效的机器码,并与所在操作系统提供的抽象机制自然地协同工作。
- 在高级编程语言中,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:])。
```Go
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)
}
}
- 与控制台的交互方式和Java类似,将系统标准输入流
os.Stdin
封装到bufio.NewScanner()
中,然后通过.Scan()
方法判断是否抵达输入的结尾,并不断获取下一行输入.Test()
; - 注意
.Scan()
遇到ctrl+c的终止命令时才会认为输入终止了,这与多数使用场景并不符合。因此要获取多少输入信息、什么时候需要终止,更多情况下是通过输入信息+代码逻辑,使用条件语句进行判断的。 - 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)如果不存在,则根据路径长度从长到短进行匹配,然后记录最长匹配的路径模式,并调用对应的处理函数。