Go语言圣经 —— 第一章 入门


参考Go语言圣经(中文版),个人电子笔记,仅供学习交流之用。

1.1 Hello, World

package main // Indicates which package the file belongs to

import "fmt" // A list of imported packages

func main() {
	fmt.Println("Hello, world.")
}
  • Go是一门编译型语言,Go语言的工具链将源代码及其依赖转换成计算机的机器指令。Go语言提供的工具都通过一个单独的命令go调用,go命令有一系列子命令。最简单的一个子命令就是run。这个命令编译一个或多个以.go结尾的源文件,链接库文件,并运行最终生成的可执行程序。$ go run hello.go
  • Go语言原生支持Unicode,它可以处理全世界任何语言的文本。
  • 如果不只是一次性实验,希望能够编译这个程序,保存编译结果以备将来使用,可以使用build子命令:$ go build hello.go
  • Go语言的代码通过包(package)组织,包类似于其他语言里的库(libraries)或者模块(modules)。一个包由位于单个目录下的一个或多个.go源代码文件组成,目录定义包的作用。每个源文件都以一条package声明语句开始,这个例子里就是package main表示该文件属于哪个包,紧跟着一系列导入(import) 的包,之后是存储在这个文件里的程序语句。
  • Go的标准库提供了100多个包,以支持常见功能,如输入、输出、排序以及文本处理。比如fmt包,就含有格式化输出、接收输入的函数。Println是其中一个基础函数,可以打印以空格间隔的一个或多个值,并在最后添加一个换行符,从而输出一整行。必须告诉编译器源文件需要哪些包,这就是import声明以及随后的package声明扮演的角色。hello.go只用到了一个包,大多数程序需要导入多个包。 必须恰当导入需要的包,缺少了必要的包或者导入了不需要的包,程序都无法编译通过。这项严格要求避免了程序开发过程中引入未使用的包
  • main包比较特殊。它定义了一个独立可执行的程序,而不是一个库。在main里的main函数也很特殊,它是整个程序执行时的入口main函数所做的事情就是程序做的。当然,main函数一般调用其他包里的函数完成很多工作,例如fmt.Println

1.2 命令行参数

  • os包以跨平台的方式,提供了一些与操作系统交互的函数和变量。程序的命令行参数可从os包的Args变量获取;os包外部使用os.Args访问该变量。
  • os.Args变量是一个字符串(string)的切片(slice),切片是Go语言的基础概念
  • os.Args的第一个元素(os.Args[0])是命令本身的名字;其他的元素则是程序启动时传给它的参数。s[m:n]形式的切片表达式,产生从第m个元素到第n-1个元素的切片,下个例子用到的元素包含在os.Args[1:len(os.Args)]切片中。如果省略切片表达式的mn,会默认传入0len(s),因此前面的切片可以简洁写成os.Args[1:]
  • 下面是Unixecho命令的一份实现,echo把它的命令行参数打印成一行。程序导入两个包,用括号把它们括起来写成列表形式,而没有分开写成独立的import声明。两种形式都合法,列表形式习惯上用得多。包导入顺序并不重要,gofmt工具格式化时按照字母顺序对包名排序。
// Myecho prints its command-line argumments.
package main

import (
	"fmt"
	"os"
)

func main() {
	var s, sep string
	for i := 1; i < len(os.Args); i++ {
		s += sep + os.Args[i] // The first seq = "", not " "
		sep = " "
	}
	fmt.Println(s)
}
  • var声明定义了两个string类型得变量sseq。变量会在声明时直接初始化。如果变量没有显式初始化,则被隐式地赋予其类型地零值(zero value),数值类型是0,字符串类型是空字符串""。对数值类型,Go语言提供了常规的数值和逻辑运算符,对于string类型,+计算符连接字符串。
  • Go语言只有for循环这一种循环语句for循环有多种形式,其中一种如下所示:
for initialization; condition; post {
	// zero or more statements
}
  • initialization语句是可选的,在循环开始前执行。如果存在,必须是一条简单语句(simple statement),即短变量声明、自增语句、赋值语句或函数调用。condition是一个布尔表达式(boolean expression),其值会在每次循环迭代前进行计算,如果为true则执行循环体语句。post语句在循环体执行结束后执行,之后再对condition求值。condition值为false时,循环结束。
  • for循环的三个部分都可以省略,如果省略initializationpost,分号也可以省略:
// a traditional "while" loop
for condition {
 // ...
}
  • 如果连condition也省略,像下面这样:
// a traditional infinite loop
for {
 // ...
}
  • for循环的另外一种形式,在某种数据类型的区间(range)上遍历,如字符串或切片。echo的第二版本展示了这种形式:
package main

import (
	"fmt"
	"os"
)

func main() {
	s, seq := "", ""
	for _, arg := range os.Args[1:] {
		s += seq + arg
		seq = " "
	}
	fmt.Println(s)
}

包含 func main() 的程序入口文件,包名必须是 package mainpackage echo2 是普通库包,不能包含 main() 函数

  • 每次循环迭代,range产生一对值,索引以及在该索引处的元素值。这个例子不需要索引,但range的语法要求,要处理元素,必须处理索引。一种思路是把索引赋值给一个临时变量,如temp,然后忽略它的值,但Go语言不允许使用无用的局部变量(local variables),因为这会导致编译错误。
  • Go语言中这种情况的解决方法是用空标识符(blank identifier),即_(也就是下划线)。空标识符可用于任何语法需要变量名但程序逻辑不需要的时候,例如,在循环里,丢弃不需要的循环索引,保留元素值。
  • 声明一种变量有很多种形式,下面这些是等价的:
s := ""
var s string
var s = ""
var s string = ""
  • 上述实现的echo都存在一个问题,即s += seq + os.Args[i]会产生新的字符串,并将它赋值给ss原来的内容已经不再使用,将在适当时机对它进行垃圾回收。如果连接涉及的数据量很大,这种方式代价高昂。一种简单且高效的解决方案是使用strings包的Join函数:
package main

import (
	"fmt"
	"os"
	"strings"
)

func main() {
	fmt.Println(strings.Join(os.Args[1:], " "))
}
  • 最后,如果不关心输出格式,只想看输出值,或者只是为了调试,可以使用Println函数格式化输出
fmt.Println(os.Args[1:])
  • 这条语句的输出结果和strings.Join(os.Args[1:], " ")得到的结果很像,只是被放到了一对方括号里。切片都会被打印成这种格式
package main

import (
	"fmt"
	"os"
)

func main() {
	fmt.Println(os.Args[1:]) // $ ./echo4 123 456 --> [123 456]
}

1.3 查找重复行

  • 对文件做拷贝、打印、搜索、排序、统计或类似事情的程序都有一个差不多的程序结构:一个处理输入的循环,在每个元素上执行计算处理,在处理的同时或最后产生输出。下展示一个名为dup的程序的三个版本,灵感来自于Unix的uniq命令,其寻找相邻的重复行。
  • dup的第一个版本打印标准输入中多次出现的行,以重复次数开头。该程序将引入if语句,map数据类型以及bufio包。
// Dup1 prints the text of each line that appears more than
// once in the standard input, preceded by its count.

package main

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

func main() {
	counts := make(map[string]int)
	input := bufio.NewScanner(os.Stdin)
	for input.Scan() { // ctrl + D end the input
		counts[input.Text()]++
	}
	// NOTE: ignoring potential errors from input.Err()
	for line, n := range counts {
		if n > 1 {
			fmt.Printf("%d\t%s\n", n, line)
		}
	}
}
  • map存储的是键/值对(key/value)的集合,对集合元素,提供常数时间的存、取或测试操作。键可以是任意类型,只要其值能用==运算符比较,最常见的例子是字符串;值可以是任意类型。这个例子中的键是字符串,值是整数。内置函数make创建空mapmap可以当作是一种数学上的映射。
  • 每次dup读取一行输入,该行被当作map,其对应的值会递增。counts[input.Text()]++语句等价为:
line := input.Text()
counts[line] = counts[line] + 1
  • map中不含某个键时不用担心,首次读到新行时,等号右边的表达式counts[line]的值将被计算为其类型的零值,对于int而言就是0
  • 为了打印结果,我们使用了基于range的循环,并在counts这个map上迭代,跟之前类似,每次迭代得到两个结果,键和其在map中对应的值。map的迭代的迭代顺序并不确定,从实践来看,该顺序随机,每次运行都会变化。这种设计是有意为之的,因为能防止程序以来特定遍历顺序,而这是无法保证的。
  • bufio包,它使处理输入和输出方便又高效。Scanner类型是该包最有用的特性之一,它读取输入并将其拆成单词;通常是处理行形式的输入最简单的方法。程序使用短向量声明创建bufio.Scanner类型的变量inputinput := bufio.NewScanner(os.Stdin),该变量从程序的标准输入中读取内容。每次调用input.Scanner,即读入下一行,并移除行末的换行符,读取的内容可以调用input.Text()得到,Scan函数在读到一行时返回true,在无输入时返回false
  • 类似于C语言或其他语言里的printf函数,fmt.Printf函数对一些表达式产生格式化输出。该函数的首个参数时格式字符串,指定后续参数被如何格式化。各个参数的格式取决于“转换字符”(conversion character),形式为百分号后跟一个字母。举个例子就是,%d表示以十进制形式打印一个整型操作数,而%s则表示把字符串型操作数的值展开。
  • Printf有很多的这种转换,称之为动词(verb):
格式化字符 意义
%d 十进制整数
%x%o%b 十六进制,八进制,二进制整数
%f%g%e 浮点数:3.141593 3.141592653589793 3.141593e+00
%t 布尔:true或false
%c 字符(rune) (Unicode码点)
%s 字符串
%q 带双引号的字符串"abc"或带单引号的字符'c'
%v 变量的自然醒时(natural format)
%T 变量的类型
%% 字面上的百分号标志(无操作数)
  • dup1的格式化字符串中还有制表符\t和换行符\n,字符串字面上可能含有这些代表不可见字符的转义字符(escap sequences)。默认情况下,Printf不会换行。按照惯例,以字母f结尾的格式化函数,如log.Printffmt.Errorf,都采用fmt.Printf的格式化准则。而以ln结尾的格式化函数,则遵循Println的方式,以跟%v差不多的方式格式化参数,并在最后添加一个换行符。
  • 很多程序要么从标准输入中读取数据,要么从一系列具名文件中读取数据。dup程序的下个版本读取标准输入或是使用os.Open打开各个具名文件,并操作它们。
// Dup2 prints the count and text of lines that appear more than once
// in the input. It reads from stdin or from a list of named files.

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.Fprintf(os.Stderr, "dup2: %v\n", 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()]++
	}
	// NOTE: ignoring potential errors from input.Err()
}
  • os.Open函数返回两个值。第一个值是被打开的文件(*os.File),其后被Scanner读取。os.Open返回的第二个值是内置error类型的值。如果err等于内置值nil(相当于其他语言中的NULL),那么文件被成功打开,读取文件,指导文件结束,然后调用Close关闭该文件,并释放占用的所有资源。相反的话,如果err的值不是nil,说明打开文件时出错了,这种情况下,错误值描述了遇到的问题。
  • 上述程序的错误处理非常简单,只是使用Fprintf与表示任意类型默认格式值得动词%v,向标准错误流打印一条信息,然后dup继续处理下一个文件。
  • 函数和包级别的变量(package-level entities)可以任意顺序声明,并不影响其被调用。
  • map是一个由make函数创建的数据结构的引用。map作为参数传递给某函数时,该函数接收这个引用的一份拷贝(copy),被调用函数对map底层数据结构的任何修改,调用者函数都可以通过持有的map引用看到。在上述例子中,countLines函数对counts的修改,会被main看到。(类似于C++里的引用传递,实际上指针是另一个指针了,但内部存的值指向的是同一块内存)
  • dup的前两个版本以“流”模式读取输入,并根据需要拆分成多个行。理论上,这些程序可以处理任意数量的输入数据。还有一个方法,就是一口气把全部输入数据读到内存中,一次分割为多行,然后进行处理。下面这个版本,dup3就是这么操作的,这个例子引入了ReadFile函数(os),其读取指定文件的全部内容,strings.Split函数将字符串分割成字串的切片。
package main

import (
	"fmt"
	"os"
	"strings"
)

func main() {
	counts := make(map[string]int)
	for _, filename := range os.Args[1:] {
		data, err := os.ReadFile(filename)
		// fmt.Printf("The type of data is %T\n", data) // The type of data is []uint8
		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)
		}
	}
}
  • ReadFile函数返回一个字节切片(byte slice),必须把它转换为string,才能用string.Split分割。
  • 实现上,bufio.Scanneros.ReadFileos.WriteFile都使用*os.FileReadWrite方法。

1.4 GIF动画

  • 下面的程序会演示Go语言标准库里的image这个package的用法,用这个包生成一系列的bit-mapped图,要看这个程序的结果,需要将标准输出重定向到一个GIF图像文件(使用./lissajous > output.gif
// Lissajous generates GIF animations of random Lissajous figures.
package main

import (
	"image"
	"image/color"
	"image/gif"
	"io"
	"math"
	"math/rand"
	"os"
)

var palette = []color.Color{color.White, color.Black}

const (
	whiteIndex = 0 // first color in palette
	blackIndex = 1 // next color in palette
)

func main() {
	lissajous(os.Stdout)
}

func lissajous(out io.Writer) {
	const (
		cycles 	= 5 	// number of complete x oscillator revolutions
		res 	= 0.001	// angular resolution
		size 	= 100 	// image canvas covers [-size..+size]
		nframes = 64 	// number of animation frames
		delay	= 8		// delay between frames in 10ms units
	)
	freq := rand.Float64() * 3.0 // relative frequency of y oscillator
	anim := gif.GIF{LoopCount: nframes}
	phase := 0.0 // phase difference
	for i:= 0; i < nframes; i++ {
		rect := image.Rect(0, 0, 2 * size + 1, 2 * size + 1)
		img := image.NewPaletted(rect, palette)
		for t := 0.0; t < cycles * 2 * math.Pi; t += res {
			x := math.Sin(t)
			y := math.Sin(t * freq + phase)
			img.SetColorIndex(size + int(x * size + 0.5), size + int(y * size + 0.5), blackIndex)
		}
		phase += 0.1
		anim.Delay = append(anim.Delay, delay)
		anim.Image = append(anim.Image, img)
	}
	gif.EncodeAll(out, &anim) // NOTE: ignoring encoding errors
}
  • 当我们import了一个包路径包含有多个单词的package时,比如image/color(image和color两个单词),通常我们只需要用最后那个单词表示这个包就可以。所以当我们写color.White时,这个变量指向的是image/color包里的变量,同理hif.GIF是属于image/gif包里的变量。
  • 这个程序里的常量声明给出了一系列的常量值,常量是指程序编译后运行时始终都不会变化的值,比如圈数、帧数、延迟值。常量声明和变量声明一般都会出现在包级别,所以这些常量在整个包中都是可以共享的,或者可以把常量声明定义在函数体内部,那么这些常量只能在函数体内用,目前常量声明的值必须是一个数字值、字符串或者一个固定的boolean值。
  • []color.Color{...}gif.GIF{...}这两个表达式就是我们说的复合声明。这是实例化Go语言里的符合类型的一种写法,这里的前者生成的是一个slice切片,后者生成的是一个struct结构体

1.5 获取URL

  • Go语言在net这个强大的package的帮助下提供了一系列的package来完成互联网上信息的访问,使用这些包可以更简单地用网络收发信息,还可以建立更底层地网络连接,编写服务器程序。在这些情景下,Go语言原生的并发特性显得尤其好用。
  • 为了最简单地展示基于HTTP获取信息的方式,下面给出了一个示例程序fetch,这个程序将获取对应的url,并将其源文本打印出来;这个例子的灵感开源与curl工具。
// Fetch prints the content found at a URL.
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
)
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)
		}
		b, err := io.ReadAll(resp.Body)
		resp.Body.Close()
		if err != nil {
			fmt.Fprintf(os.Stderr, "fetch: %v\n", err)
			os.Exit(1)
		}
		fmt.Printf("%s\n", b)
	}
}
  • 这个程序从两个package中导入了函数,net/http或io包,http.Get函数是创建HTTP请求的函数,如果获取过程没有出错,那么会在resp这个结构体中得到访问的请求结果。respBody字段包括一个可读的服务器响应流。io.ReadAll函数从response中读取到全部内容;并将其结果保存在变量b中。resp.Body.Close()关闭respBody流,防止资源泄露,Printf函数会将结果写到标准输出流中。
  • 如果HTTP请求失败的话,会得到下面这样的结果:
└─[$]> ./fetch http://hahahalalala.com
fetch: Get "http://hahahalalala.com": dial tcp: lookup hahahalalala.com on 127.0.0.53:53: no such host
  • 无论哪种失败原因,程序都通过os.Exit(1)来终止进程,并且返回一个status错误码,其值为1

1.6 并发获取多个URL

  • Go语言最有意思并且最新奇的特性就是对并发编程的支持。并发编程是一个大话题,这里就浅尝辄止地体验一下Go语言里的goroutinechannel
  • 下面的fetchall和前面1.5的fetch程序完成的工作基本一致,fetchall的特别之处在于它会同时获取所有的URL,所以这个程序的总执行时间不会超过执行时间最长的那一个任务,前面的fetch程序执行事件则是所有任务执行时间之和。fetchall程序只会打印获取的内容大小和经过的时间,并不会像之前那样打印获取的内容。
// Fetchall fetches URLs in parallel and reports their times and sizes.
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) // start a goroutine
	}
	for range os.Args[1:] {
		fmt.Println(<-ch) // receive from channel 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) // send to channel ch
		return
	}
	nbytes, err := io.Copy(io.Discard, resp.Body)
	resp.Body.Close() // don't leak resources
	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方法。这个程序里的io.Copy会把相应的Body内容拷贝到io.Discard输出流中(可以把这个变量看作一个垃圾桶,可以向里面写入一些不需要的数据),因为我们需要这个方法返回的字节数,但是又不想看其内容。每当请求返回内容时,fetch函数都会往ch这个channel里写入一个字符串,由main函数的第二个foe循环来处理并打印channe里的这个字符串。
  • 当一个goroutine尝试在一个channel上做send或者receive操作时,这个goroutine会阻塞在调用处,直到另一个goroutine往这个channel里写入或者接收值,这样两个goroutine才会继续执行channel操作之后的逻辑。在这个例子里,每一个fetch函数在执行时都会往channel里发送一个值(ch <- expression),主函数负责接收这些值(<- ch)。这个程序中沃恩用main函数来接收所有fetch函数传回的字符串,可以避免在goroutine异步执行时还没有完成时main函数提前退出。

1.7 Web服务

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

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

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

// handler echoes the Path component of the request URL r.
func handler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}
  • main函数将所有发送到/路径下的请求和handler函数关联起来,/开头的请求其实就是所有发送到当前节点上的请求,服务监听8000端口。发送到这个服务的“请求”是一个http.Request类型的对象,这个对象中包含了请求中的一系列相关字段,其中就包括我们需要的URL。当请求到达服务器时,这个请求就会被传给handle函数来处理,这个函数会将/hello这个路径从URL中解析出来,然后把其发送到响应中,这里我们用的时标准输出流的fmt.Fprintf。
    Pasted image 20260802095413

  • 在这个服务器的基础上叠加特性是很容易的。一种比较实用的修改是为访问的URL添加某种形状。比如下面这个版本输出了同样的内容,但是会对请求的次数进行计算;对URL的请求结果会包含格中URL被访问的总次数,直接对/count这个URL的访问要除外。

// Server2 is a minimal "echo" and counter server.
package main

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

var mu sync.Mutex
var count int

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

// handler echoed 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 echoed 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()
}

4bf97e1c-4bad-4a9d-a9b9-97652367dcf8

  • 这个服务器有两个请求处理函数,根据请求的URL不同会调用不同的函数:对/count这个URL请求会调用count这个函数,其他的URL都会调用默认的处理函数。如果请求的pattern是以/结尾,那么所有以该URL为前缀的URL都会被这条规则匹配。在这些代码的背后,服务器每一次接受请求处理时都会另起一个goroutine,这样服务器就可以同一时间处理多个请求。然而在并发情况下,假如真的有两个请求同一时刻去更新count,这个值可能不被正确地增加;这个程序可能会引发一个严重的Bug:竞态条件。为了避免这个问题,我们必须保证每次修改变量的最多只有一个goroutine,这也是代码中的mu.Lock与mu.Unlock调用将修改count的行为包在中间的目的。
  • 下面是一个更为丰富的例子,handler函数会把请求的http偷和请求的from数据都打印出来,这样可以使检查和调试这个服务更加方便:
// handler echoes the HTTP request.
func handler(w, http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "%s %s %s\n", r.Method, r,URL, r.Proto)
	for k, v := range r.Header {
		fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
	}
	fmt.Fprintf(w, "Host = %q\n", r.Host)
	fmt.Fprintf(w, "RemoteAddr = %q\n", r.RemoteAddr)
	if err := r.ParseForm(); err != nil {
		log.Print(err)
	}
	for k, v := range r.Form {
		fmt.Fprintf(w, "Form[%q] = %q\n", k, v)
	}
}

48fb2459-7a86-4af9-9082-70be359bbafd

  • 可以看到这里的ParseForm被嵌套在if语句中。Go语言允许这样的一个简单的语句结果作为循环的变量声明出现在if语句的最前面,这一点对错误处理很有用。
// one
if err := r.ParseForm(); err != nil {
	...
}
// two
err := r.ParseForm()
if err != nil {
	...
}
  • 这些程序中,我们看到很多不同类型被输出到标准输出流中。比如前面的fetch程序,把HTTP的响应数据拷贝到os.Stdout,lissajous程序里我们输出的是一个文件。fetchall程序则完全忽略了HTTP的响应Body,只计算了响应Body的大小,这个程序中把响应Body拷贝到了io.Discard中。在本节的web服务器程序中则是用到fmt.Fprintf直接写道了http.ResponseWriter中。尽管三种具体的实现流程不太一样,它们都实现了一个共同的接口,即当它们被调用需要一个标准流输出时都可以满足。这个接口叫做io.Writer。
  • 接口能做什么?
// one
handler := func(w http.ResponseWriter, r *http.Request) {
	lissajous(w)
}
http.HandleFunc("/", handler)
// two
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
	lissajous(w)
})
  • HandleFunc函数的第二个参数是一个函数的字面值,也就是一个在使用时定义的匿名函数。
package main

import (
	"net/http"
	"log"
	"drawServer/lissajous"
)

func main() {
	handler := func(w http.ResponseWriter, r *http.Request) {
		lissajous.Lissajous(w)
	}
	http.HandleFunc("/draw", handler)
	log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

e41d14ef-b811-432f-a9ef-cba2d5cd72f4
Pasted image 20260802114306

Practice

1.1

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

import (
	"fmt"
	"os"
	"strings"
)

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

1.2

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

import (
	"fmt"
	"os"
)

func main() {
	for index, element := range os.Args[1:] {
		fmt.Println(index, element)
	}
}

1.3

  • 做试验测量潜在低效的版本和使用了strings.Join的版本的运行时间差异。
package main

import (
	"fmt"
	"strings"
	"os"
	"time"
)

func main() {
	start := time.Now()
	dumpPrint()
	fmt.Printf("dumpPrint time cost: %.6fs\n", time.Since(start).Seconds())

	start = time.Now()
	joinPrint()
	fmt.Printf("joinPrint time cost: %.6fs\n", time.Since(start).Seconds())
}

func dumpPrint() {
	var s, seq string
	for _, arg := range os.Args[1:] {
		s += seq + arg
		seq = " "
	}
	fmt.Println(s)
}

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

41d6eb05-2130-409b-9bbf-06d7a748cf30
1.4

  • 修改dup2,出现重复行时,打印文件名称。
// 1.4 Modify dup2 to print the names of all files in which each duplicated line occurs.
package main

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

type nameAndCount struct {
	fileNames []string
	count int
}

func main() {
	counts := make(map[string]*nameAndCount)
	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.Fprintf(os.Stderr, "dup2pro: %v\n", err)
				continue
			}
			countLines(f, counts)
			f.Close()
		}
	}
	for line, v := range counts {
		if v.count > 1 {
			fmt.Printf("%d\t%s\t%s\n", v.count, strings.Join(v.fileNames, ", "), line)
		}
	}
}

func countLines(f *os.File, counts map[string]*nameAndCount) {
	input := bufio.NewScanner(f)
	name := f.Name()
	for input.Scan() {
		line := input.Text()
		v, ok := counts[line]
		if ok {
			v.count++
			files := v.fileNames
			if len(files) == 0 || files[len(files) - 1] != name {
				v.fileNames = append(files, name)
			}
		} else {
			counts[line] = &nameAndCount{
				fileNames: []string{name},
				count: 1,
			}
		}
	}
}

1.5

  • 修改前面的Lissajous程序里的调色板,由黑色改为绿色。可以用color.RGBA{0xRR, 0xGG, 0xBB, 0xff}来的搭配#RRGGBB这个色值,三个十六进制的字符串分别代表红、绿、蓝像素。
// Lissajous generates GIF animations of random Lissajous figures.
package main

import (
	"image"
	"image/color"
	"image/gif"
	"io"
	"math"
	"math/rand"
	"os"
)

// black --> green
var palette = []color.Color{color.White, color.RGBA{0x00, 0xff, 0x00, 0xff}}

// blackIndex --> greenIndex
const (
	whiteIndex = 0 // first color in palette
	greenIndex = 1 // next color in palette
)

func main() {
	lissajous(os.Stdout)
}

func lissajous(out io.Writer) {
	const (
		cycles 	= 5 	// number of complete x oscillator revolutions
		res 	= 0.001	// angular resolution
		size 	= 100 	// image canvas covers [-size..+size]
		nframes = 64 	// number of animation frames
		delay	= 8		// delay between frames in 10ms units
	)
	freq := rand.Float64() * 3.0 // relative frequency of y oscillator
	anim := gif.GIF{LoopCount: nframes}
	phase := 0.0 // phase difference
	for i:= 0; i < nframes; i++ {
		rect := image.Rect(0, 0, 2 * size + 1, 2 * size + 1)
		img := image.NewPaletted(rect, palette)
		for t := 0.0; t < cycles * 2 * math.Pi; t += res {
			x := math.Sin(t)
			y := math.Sin(t * freq + phase)
			img.SetColorIndex(size + int(x * size + 0.5), size + int(y * size + 0.5), greenIndex)
		}
		phase += 0.1
		anim.Delay = append(anim.Delay, delay)
		anim.Image = append(anim.Image, img)
	}
	gif.EncodeAll(out, &anim) // NOTE: ignoring encoding errors
}

0f8d87df-f24c-4539-be39-eda2bffd022b

1.6

  • 修改Lissajous程序,修改器调色板来生成更丰富的颜色,然后修改SetColorIndex的第三个参数,查看显示效果。
  • \(\cdots\cdots\)
    1.7
  • 函数调用io.Copy(dst, src)会从src中读取内容,并将读到的结果写道dst中,使用这个函数替代掉例子中的io.ReadAll来拷贝相应结构体到os.Stdout,避免申请一个缓冲区(例子中的b)来存储。记得存储io.Copy返回结果中的错误。
package main

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

func main() {
	for _, url := range os.Args[1:] {
		resp, err := http.Get(url)
		if err != nil {
			fmt.Fprintf(os.Stderr, "fetchNoB: %v\n", err)
			os.Exit(1)
		}
		_, copyErr := io.Copy(os.Stdout, resp.Body)
		resp.Body.Close()
		if copyErr != nil {
			fmt.Fprintf(os.Stderr, "%v\n", copyErr)
			os.Exit(1)
		}
	}
}

1.8

  • 修改fetch这个范例,如果输入的url参数没有http://前缀的话,为这个url加上该前缀。可能会用到strings.HasPrefix这个函数。
package main

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

const (
	mask = "http://"
)

func main() {
	for _, url := range os.Args[1:] {
		if !strings.HasPrefix(url, mask) {
			url = mask + url
		}
		resp, err := http.Get(url)
		if err != nil {
			fmt.Fprintf(os.Stderr, "fetch: %v\n", err)
			os.Exit(1)
		}
		_, copyErr := io.Copy(os.Stdout, resp.Body)
		resp.Body.Close()
		if copyErr != nil {
			fmt.Fprintf(os.Stderr, "fetch: copyErr: %v\n", copyErr)
			os.Exit(1)
		}
	}
}

1.9

  • 修改fetch打印出HTTP协议地状态码,可以从resp.Status变量得到该状态码。
  • io.Copy 需要一个 io.Reader 作为数据源,但 resp.Status 是 string 类型,不是 io.Reader
package main

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

const (
	mask = "http://"
)

func main() {
	for _, url := range os.Args[1:] {
		if !strings.HasPrefix(url, mask) {
			url = mask + url
		}
		resp, err := http.Get(url)
		if err != nil {
			fmt.Fprintf(os.Stderr, "fetch: %v\n", err)
			os.Exit(1)
		}
		status := resp.Status // string
		fmt.Println(status)
	}
}

1.10

  • 找一个数据量比较大的网站,用本小节中的程序调研网站的缓存策略,对每个URL执行两边请求,查看两次时间是否有较大的差别,并且每次获取到的相应内容是否一致,修改本节中的程序,将相应结果输出,以便进行对比。
package main

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

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) // receive from channel 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) // send to channel ch
		return
	}
	defer resp.Body.Close()
	// Using TeeReader to simultaneously write to standard output and count bytes
	tee := io.TeeReader(resp.Body, os.Stdout)

	// Read the data and count the size (data will be simultaneously written to os.Stdout)
	nbytes, err := io.Copy(io.Discard, tee)
	if err != nil {
		ch <- fmt.Sprintf("while reading %s: %v", url, err)
		return
	}
	secs := time.Since(start).Seconds()
	ch <- fmt.Sprintf("%.2fs\t%7d\t%s", secs, nbytes, url)
}

Pasted image 20260801133545
Pasted image 20260801133627

  • 删除最后的时间差异以及空行后,比对first.txt与second.txt的md值查看两次的URL访问响应体的内容是否存在差异。
    Pasted image 20260801133946
  • 综上所述,我们第一次访问数据量很大的网站之后,会根据具体的缓存策略将部分内容缓存到我们的本地,一遍短期内下一次打开的时候可以更加高效,但是访问得到的内容最终都是一样的,并不会因为缓存的原因而导致前后两次得到的响应体的内容存在差异。
    1.11
  • 在fetchall中尝试使用长一些的参数列表,比如使用在alexa.com的上百万网站里排名靠前的。如果一个网站没有相应,程序将采取怎样的行为?(Section8.9描述了在这种情况下的应对机制)
  • \(\cdots\cdots\)
    1.12
  • 修改Lissajous服务,从URL中读取变量,比如可以访问http://localhost:8000/?cycles=20这个URL,这样访问可以将程序里的cycles默认的5修改为20.字符串转换为数字可以调用strconv.Atoi函数。
    practice/1-12/server.go
package main

import (
	"net/http"
	"log"
	"1-12/lissajous"
	"strconv"
)

func main() {
	handler := func(w http.ResponseWriter, r *http.Request) {
		var cycles int = 5
		if cycleStr := r.URL.Query().Get("cycles"); cycleStr != "" {
			if val, err := strconv.Atoi(cycleStr); err == nil && val > 0 {
				cycles = val
			}
		}
		lissajous.Lissajous(w, cycles)
	}
	http.HandleFunc("/", handler)
	log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

practice/1-12/lissajous/lissajous.go

package lissajous

import (
	"image"
	"image/gif"
	"image/color"
	"math"
	"math/rand"
	"io"
)

var palette = []color.Color{color.White, color.Black}

const (
	whiteIndex = 0 // first color in palette
	blackIndex = 1 // next color in palette
)

func Lissajous(out io.Writer, cycles int) {
	const (
		res 	= 0.001	// angular resolution
		size 	= 100 	// image canvas covers [-size..+size]
		nframes = 64 	// number of animation frames
		delay	= 8		// delay between frames in 10ms units
	)
	freq := rand.Float64() * 3.0 // relative frequency of y oscillator
	anim := gif.GIF{LoopCount: nframes}
	phase := 0.0 // phase difference
	for i:= 0; i < nframes; i++ {
		rect := image.Rect(0, 0, 2 * size + 1, 2 * size + 1)
		img := image.NewPaletted(rect, palette)
		for t := 0.0; t < float64(cycles) * 2 * math.Pi; t += res {
			x := math.Sin(t)
			y := math.Sin(t * freq + phase)
			img.SetColorIndex(size + int(x * size + 0.5), size + int(y * size + 0.5), blackIndex)
		}
		phase += 0.1
		anim.Delay = append(anim.Delay, delay)
		anim.Image = append(anim.Image, img)
	}
	gif.EncodeAll(out, &anim) // NOTE: ignoring encoding errors
}

0d619f68-d773-4a6b-b6b6-7a351c9f1f26
03c130e1-2847-479a-9d09-150d234a782a

posted @ 2026-08-02 12:52  chen_xing  阅读(8)  评论(0)    收藏  举报