本教程全面涵盖了Go语言基础的各个方面。一共80个例子,每个例子对应一个语言特性点,非常适合新人快速上手。
教程代码示例来自go by example,文字部分来自本人自己的理解。
本文是教程系列的第四部分,共计20个例子、约1.5万字。
Go提供了Base64编解码的功能。Base64是将二进制数据转换成可读字符串的编码方式。例如我们用记事本打开jpg文件,会看到一串乱码,这个就是Base64转换后的字符串。Base64提供了64种不同的字符,6位编码表示一个字符。
package mainimport (b64 "encoding/base64""fmt"
)func main() {data := "abc123!?$*&()'-=@~"// 将data底层的byte slice编码成字符串sEnc := b64.StdEncoding.EncodeToString([]byte(data))fmt.Println(sEnc)// 用Base64解码sDec, _ := b64.StdEncoding.DecodeString(sEnc)fmt.Println(string(sDec))fmt.Println()// 用URL兼容的方式编码uEnc := b64.URLEncoding.EncodeToString([]byte(data))fmt.Println(uEnc)// 用URL兼容的方式解码uDec, _ := b64.URLEncoding.DecodeString(uEnc)fmt.Println(string(uDec))
}
$ go run base64-encoding.go
YWJjMTIzIT8kKiYoKSctPUB+
abc123!?$*&()'-=@~YWJjMTIzIT8kKiYoKSctPUB-
abc123!?$*&()'-=@~
下面例子展示了读文件的操作。
package mainimport ("bufio""fmt""io""os"
)func check(e error) {if e != nil {panic(e)}
}func main() {// 直接从文件读到byte slicedat, err := os.ReadFile("/tmp/dat")check(err)fmt.Print(string(dat))// 先打开文件,再对file做更复杂的操作f, err := os.Open("/tmp/dat")check(err)// 自己创建缓冲slice来接收读数据。返回的是读到的字节数b1 := make([]byte, 5)n1, err := f.Read(b1)check(err)fmt.Printf("%d bytes: %s\n", n1, string(b1[:n1]))// 偏移到下标为6的地方开始读取。两个参数分别代表:基于基准位置的偏移量、基准位置o2, err := f.Seek(6, 0)check(err)b2 := make([]byte, 2)n2, err := f.Read(b2)check(err)fmt.Printf("%d bytes @ %d: ", n2, o2)fmt.Printf("%v\n", string(b2[:n2]))o3, err := f.Seek(6, 0)check(err)b3 := make([]byte, 2)// ReadAtLeast保证一定读到指定字节数,否则返回错误n3, err := io.ReadAtLeast(f, b3, 2)check(err)fmt.Printf("%d bytes @ %d: %s\n", n3, o3, string(b3))_, err = f.Seek(0, 0)check(err)// bufio包提供了带缓冲的读写并提供一些更简便的操作,这里创建了带缓冲的readerr4 := bufio.NewReader(f)// 获取后面5个字节,但不实际移动指针,类似于队列中的peekb4, err := r4.Peek(5)check(err)fmt.Printf("5 bytes: %s\n", string(b4))f.Close()
}
$ echo "hello" > /tmp/dat
$ echo "go" >> /tmp/dat
$ go run reading-files.go
hello
go
5 bytes: hello
2 bytes @ 6: go
2 bytes @ 6: go
5 bytes: hello
下面例子展示了写文件的操作。
package mainimport ("bufio""fmt""os"
)func check(e error) {if e != nil {panic(e)}
}func main() {d1 := []byte("hello\ngo\n")// 将byte slice写入文件,若文件不存则创建,创建时使用0644代表的权限err := os.WriteFile("/tmp/dat1", d1, 0644)check(err)// 先创建文件,再对file做更复杂的操作f, err := os.Create("/tmp/dat2")check(err)// 将close操作推迟到最后defer f.Close()// 将自己创建的byte slice写入文件d2 := []byte{115, 111, 109, 101, 10}n2, err := f.Write(d2)check(err)fmt.Printf("wrote %d bytes\n", n2)// 直接写入一个字符串n3, err := f.WriteString("writes\n")check(err)fmt.Printf("wrote %d bytes\n", n3)// 写完后需要调用Sync()才能真正写入硬盘f.Sync()// 用bufio创建自带缓冲的writerw := bufio.NewWriter(f)n4, err := w.WriteString("buffered\n")check(err)fmt.Printf("wrote %d bytes\n", n4)// 写完后需要调用Flush(),将缓冲区数据落地w.Flush()}
$ go run writing-files.go
wrote 5 bytes
wrote 7 bytes
wrote 9 bytes$ cat /tmp/dat1
hello
go
$ cat /tmp/dat2
some
writes
buffered
下面例子展示了从标准输入中读取并解析。类似于Java中的Scanner和Linux下的grep、sed命令。
package mainimport ("bufio""fmt""os""strings"
)func main() {// 从标准输入创建scannerscanner := bufio.NewScanner(os.Stdin)for scanner.Scan() {// 默认用空格分隔,每次新读取一段ucl := strings.ToUpper(scanner.Text())fmt.Println(ucl)}if err := scanner.Err(); err != nil {fmt.Fprintln(os.Stderr, "error:", err)os.Exit(1)}
}
$ echo 'hello' > /tmp/lines
$ echo 'filter' >> /tmp/lines$ cat /tmp/lines | go run line-filters.go
HELLO
FILTER
下面例子展示了对文件路径的各种操作。
package mainimport ("fmt""path/filepath""strings"
)func main() {// 将不同级别目录名拼接成文件的完整路径p := filepath.Join("dir1", "dir2", "filename")fmt.Println("p:", p)fmt.Println(filepath.Join("dir1//", "filename"))fmt.Println(filepath.Join("dir1/../dir1", "filename"))// 从完整路径获取目录名和文件名fmt.Println("Dir(p):", filepath.Dir(p))fmt.Println("Base(p):", filepath.Base(p))// 是否是绝对路径fmt.Println(filepath.IsAbs("dir/file"))fmt.Println(filepath.IsAbs("/dir/file"))filename := "config.json"// 获取文件扩展名ext := filepath.Ext(filename)fmt.Println(ext)// 获取去除扩展名后的文件名fmt.Println(strings.TrimSuffix(filename, ext))// 获取两个路径之间的相对路径rel, err := filepath.Rel("a/b", "a/b/t/file")if err != nil {panic(err)}fmt.Println(rel)rel, err = filepath.Rel("a/b", "a/c/t/file")if err != nil {panic(err)}fmt.Println(rel)
}
$ go run file-paths.go
p: dir1/dir2/filename
dir1/filename
dir1/filename
Dir(p): dir1/dir2
Base(p): filename
false
true
.json
config
t/file
../c/t/file
下面例子展示了对目录的相关操作。
package mainimport ("fmt""os""path/filepath"
)func check(e error) {if e != nil {panic(e)}
}func main() {// 使用指定的名字和权限创建目录err := os.Mkdir("subdir", 0755)check(err)// 递归删除目录,等效于rm -rfdefer os.RemoveAll("subdir")createEmptyFile := func(name string) {d := []byte("")// 通过写空数据创建文件,文件不存在就创建check(os.WriteFile(name, d, 0644))}createEmptyFile("subdir/file1")// 强制创建目录,若路径上的父级不存在则创建,等效于mkdir -perr = os.MkdirAll("subdir/parent/child", 0755)check(err)createEmptyFile("subdir/parent/file2")createEmptyFile("subdir/parent/file3")createEmptyFile("subdir/parent/child/file4")// 列出指定目录下一级的所有目录和文件c, err := os.ReadDir("subdir/parent")check(err)fmt.Println("Listing subdir/parent")for _, entry := range c {fmt.Println(" ", entry.Name(), entry.IsDir())}// 改变当前目录,等效于Linux下的cd命令err = os.Chdir("subdir/parent/child")check(err)// 对当前目录执行ReadDirc, err = os.ReadDir(".")check(err)fmt.Println("Listing subdir/parent/child")for _, entry := range c {fmt.Println(" ", entry.Name(), entry.IsDir())}err = os.Chdir("../../..")check(err)fmt.Println("Visiting subdir")// 递归遍历指定目录下的所有子目录和文件,方式为深度优先遍历err = filepath.Walk("subdir", visit)
}func visit(p string, info os.FileInfo, err error) error {if err != nil {return err}fmt.Println(" ", p, info.IsDir())return nil
}
$ go run directories.go
Listing subdir/parentchild truefile2 falsefile3 false
Listing subdir/parent/childfile4 false
Visiting subdirsubdir truesubdir/file1 falsesubdir/parent truesubdir/parent/child truesubdir/parent/child/file4 falsesubdir/parent/file2 falsesubdir/parent/file3 false
下面例子展示了对临时文件和目录的操作。
package mainimport ("fmt""os""path/filepath"
)func check(e error) {if e != nil {panic(e)}
}func main() {f, err := os.CreateTemp("", "sample")check(err)fmt.Println("Temp file name:", f.Name())// 操作系统会在程序结束后一段时间自动清理临时文件,不过最好还是自己做清理defer os.Remove(f.Name())_, err = f.Write([]byte{1, 2, 3, 4})check(err)dname, err := os.MkdirTemp("", "sampledir")check(err)fmt.Println("Temp dir name:", dname)defer os.RemoveAll(dname)fname := filepath.Join(dname, "file1")err = os.WriteFile(fname, []byte{1, 2}, 0666)check(err)
}
$ go run temporary-files-and-directories.go
Temp file name: /tmp/sample610887201
Temp dir name: /tmp/sampledir898854668
embed编译指令可以在程序运行时从指定的文件获取数据到程序变量。这种编程风格有点类似于Srping中的依赖注入。
package mainimport ("embed"
)// 使用//go:embed指定从文件读取内容到变量fileString、fileByte
//go:embed folder/single_file.txt
var fileString string//go:embed folder/single_file.txt
var fileByte []byte// 使用//go:embed也可以打包多个文件到embed.FS类型变量,方便后续操作
//go:embed folder/single_file.txt
//go:embed folder/*.hash
var folder embed.FSfunc main() {print(fileString)print(string(fileByte))content1, _ := folder.ReadFile("folder/file1.hash")print(string(content1))content2, _ := folder.ReadFile("folder/file2.hash")print(string(content2))
}
$ mkdir -p folder
$ echo "hello go" > folder/single_file.txt
$ echo "123" > folder/file1.hash
$ echo "456" > folder/file2.hash$ go run embed-directive.go
hello go
hello go
123
456
Go对于单元测试和基准测试提供了原生的支持。仅需以指定的格式命名函数,通过正则匹配会被自动识别为单元测试或基准测试用例,再用go test命令启动测试。
package mainimport ("fmt""testing"
)func IntMin(a, b int) int {if a < b {return a}return b
}// 测试用例需要以Test开头
func TestIntMinBasic(t *testing.T) {ans := IntMin(2, -2)if ans != -2 {t.Errorf("IntMin(2, -2) = %d; want -2", ans)}
}func TestIntMinTableDriven(t *testing.T) {var tests = []struct {a, b intwant int}{{0, 1, 0},{1, 0, 0},{2, -2, -2},{0, -1, -1},{-1, 0, -1},}for _, tt := range tests {testname := fmt.Sprintf("%d,%d", tt.a, tt.b)// 用Run启动子测试用例,每个用例用数据表中获取输入t.Run(testname, func(t *testing.T) {ans := IntMin(tt.a, tt.b)if ans != tt.want {t.Errorf("got %d, want %d", ans, tt.want)}})}
}// 基准测试需要以Benchmark开头
func BenchmarkIntMin(b *testing.B) {// b.N是执行次数,由底层自动决定,以产生稳定精确的性能统计for i := 0; i < b.N; i++ {IntMin(1, 2)}
}
// 执行所有测试,显示详细信息,包括子测试
$ go test -v
== RUN TestIntMinBasic
--- PASS: TestIntMinBasic (0.00s)
=== RUN TestIntMinTableDriven
=== RUN TestIntMinTableDriven/0,1
=== RUN TestIntMinTableDriven/1,0
=== RUN TestIntMinTableDriven/2,-2
=== RUN TestIntMinTableDriven/0,-1
=== RUN TestIntMinTableDriven/-1,0
--- PASS: TestIntMinTableDriven (0.00s)--- PASS: TestIntMinTableDriven/0,1 (0.00s)--- PASS: TestIntMinTableDriven/1,0 (0.00s)--- PASS: TestIntMinTableDriven/2,-2 (0.00s)--- PASS: TestIntMinTableDriven/0,-1 (0.00s)--- PASS: TestIntMinTableDriven/-1,0 (0.00s)
PASS
ok examples/testing-and-benchmarking 0.023s// 执行基准测试
$ go test -bench=.
goos: darwin
goarch: arm64
pkg: examples/testing
BenchmarkIntMin-8 1000000000 0.3136 ns/op
PASS
ok examples/testing-and-benchmarking 0.351s
通过Go提供的命令行api,可以方便地把Go编译出来的程序用作命令行工具。下面例子展示了Go程序读取命令行参数并处理的过程。
package mainimport ("fmt""os"
)func main() {// os.Args包含所有参数,当前文件编译生成的可执行文件为第一个参数argsWithProg := os.ArgsargsWithoutProg := os.Args[1:]arg := os.Args[3]fmt.Println(argsWithProg)fmt.Println(argsWithoutProg)fmt.Println(arg)
}
$ go build command-line-arguments.go
$ ./command-line-arguments a b c d
[./command-line-arguments a b c d]
[a b c d]
c
Go支持命令行标志,为命令行提供了不同的选项。例如grep -c中的-c就是一种命令行标志。
package mainimport ("flag""fmt"
)func main() {// 新定义word作为命令行标志,三个参数为:标志名、默认值、注释wordPtr := flag.String("word", "foo", "a string")numbPtr := flag.Int("numb", 42, "an int")forkPtr := flag.Bool("fork", false, "a bool")var svar stringflag.StringVar(&svar, "svar", "bar", "a string var")flag.Parse()fmt.Println("word:", *wordPtr)fmt.Println("numb:", *numbPtr)fmt.Println("fork:", *forkPtr)fmt.Println("svar:", svar)fmt.Println("tail:", flag.Args())
}
go build command-line-flags.go$ ./command-line-flags -word=opt -numb=7 -fork -svar=flag
word: opt
numb: 7
fork: true
svar: flag
tail: []$ ./command-line-flags -word=opt
word: opt
numb: 42
fork: false
svar: bar
tail: []$ ./command-line-flags -word=opt a1 a2 a3
word: opt
...
tail: [a1 a2 a3]$ ./command-line-flags -word=opt a1 a2 a3 -numb=7
word: opt
numb: 42
fork: false
svar: bar
tail: [a1 a2 a3 -numb=7]$ ./command-line-flags -h
Usage of ./command-line-flags:-fork=false: a bool-numb=42: an int-svar="bar": a string var-word="foo": a string$ ./command-line-flags -wat
flag provided but not defined: -wat
Usage of ./command-line-flags:
...
Go支持命令行子命令。例如go build和run就是go下面的两个子命令。
package mainimport ("flag""fmt""os"
)func main() {fooCmd := flag.NewFlagSet("foo", flag.ExitOnError)fooEnable := fooCmd.Bool("enable", false, "enable")fooName := fooCmd.String("name", "", "name")barCmd := flag.NewFlagSet("bar", flag.ExitOnError)barLevel := barCmd.Int("level", 0, "level")if len(os.Args) < 2 {fmt.Println("expected 'foo' or 'bar' subcommands")os.Exit(1)}switch os.Args[1] {case "foo":fooCmd.Parse(os.Args[2:])fmt.Println("subcommand 'foo'")fmt.Println(" enable:", *fooEnable)fmt.Println(" name:", *fooName)fmt.Println(" tail:", fooCmd.Args())case "bar":barCmd.Parse(os.Args[2:])fmt.Println("subcommand 'bar'")fmt.Println(" level:", *barLevel)fmt.Println(" tail:", barCmd.Args())default:fmt.Println("expected 'foo' or 'bar' subcommands")os.Exit(1)}
}
$ go build command-line-subcommands.go $ ./command-line-subcommands foo -enable -name=joe a1 a2
subcommand 'foo'enable: truename: joetail: [a1 a2]$ ./command-line-subcommands bar -level 8 a1
subcommand 'bar'level: 8tail: [a1]$ ./command-line-subcommands bar -enable a1
flag provided but not defined: -enable
Usage of bar:-level intlevel
下面例子展示了环境变量的设置和读取。
package mainimport ("fmt""os""strings"
)func main() {// 环境变量为进程级别,进程退出后设置的变量消失os.Setenv("FOO", "1")fmt.Println("FOO:", os.Getenv("FOO"))fmt.Println("BAR:", os.Getenv("BAR"))fmt.Println()// 读取到的变量列表与具体机器有关for _, e := range os.Environ() {pair := strings.SplitN(e, "=", 2)fmt.Println(pair[0])}
}
$ go run environment-variables.go
FOO: 1
BAR: TERM_PROGRAM
PATH
SHELL
...
FOO$ BAR=2 go run environment-variables.go
FOO: 1
BAR: 2
...
Go提供了方便的Http请求和处理的工具。下面是HTTP客户端的例子。
package mainimport ("bufio""fmt""net/http"
)func main() {// 仅一句Get就能访问http urlresp, err := http.Get("https://gobyexample.com")if err != nil {panic(err)}defer resp.Body.Close()fmt.Println("Response status:", resp.Status)scanner := bufio.NewScanner(resp.Body)for i := 0; scanner.Scan() && i < 5; i++ {fmt.Println(scanner.Text())}if err := scanner.Err(); err != nil {panic(err)}
}
$ go run http-clients.go
Response status: 200 OK
Go by Example
下面是HTTP服务端的例子。仅需HandleFunc和ListenAndServe两句代码就能实现一个简单的服务端,非常方便。
package mainimport ("fmt""net/http"
)func hello(w http.ResponseWriter, req *http.Request) {fmt.Fprintf(w, "hello\n")
}func headers(w http.ResponseWriter, req *http.Request) {for name, headers := range req.Header {for _, h := range headers {fmt.Fprintf(w, "%v: %v\n", name, h)}}
}func main() {http.HandleFunc("/hello", hello)http.HandleFunc("/headers", headers)http.ListenAndServe(":8090", nil)
}
$ go run http-servers.go &$ curl localhost:8090/hello
hello
下面例子展示了通过HTTP请求上下文处理请求取消的操作。
package mainimport ("fmt""net/http""time"
)func hello(w http.ResponseWriter, req *http.Request) {ctx := req.Context()fmt.Println("server: hello handler started")defer fmt.Println("server: hello handler ended")select {case <-time.After(10 * time.Second):fmt.Fprintf(w, "hello\n")// 若在10秒内断开请求连接,则执行下面逻辑case <-ctx.Done():err := ctx.Err()fmt.Println("server:", err)internalError := http.StatusInternalServerErrorhttp.Error(w, err.Error(), internalError)}
}func main() {http.HandleFunc("/hello", hello)http.ListenAndServe(":8090", nil)
}
$ go run context-in-http-servers.go &$ curl localhost:8090/hello
server: hello handler started
// 这里模拟在接到回应前,断开请求连接
^C
server: context canceled
server: hello handler ended
下面例子展示了在Go程序中调用其他命令,如date,并获取返回值。
package mainimport ("fmt""io""os/exec"
)func main() {dateCmd := exec.Command("date")dateOut, err := dateCmd.Output()if err != nil {panic(err)}fmt.Println("> date")fmt.Println(string(dateOut))_, err = exec.Command("date", "-x").Output()if err != nil {switch e := err.(type) {case *exec.Error:fmt.Println("failed executing:", err)case *exec.ExitError:fmt.Println("command exit rc =", e.ExitCode())default:panic(err)}}grepCmd := exec.Command("grep", "hello")grepIn, _ := grepCmd.StdinPipe()grepOut, _ := grepCmd.StdoutPipe()grepCmd.Start()grepIn.Write([]byte("hello grep\ngoodbye grep"))grepIn.Close()grepBytes, _ := io.ReadAll(grepOut)grepCmd.Wait()fmt.Println("> grep hello")fmt.Println(string(grepBytes))lsCmd := exec.Command("bash", "-c", "ls -a -l -h")lsOut, err := lsCmd.Output()if err != nil {panic(err)}fmt.Println("> ls -a -l -h")fmt.Println(string(lsOut))
}
$ go run spawning-processes.go
> date
Thu 05 May 2022 10:10:12 PM PDTcommand exited with rc = 1
> grep hello
hello grep> ls -a -l -h
drwxr-xr-x 4 mark 136B Oct 3 16:29 .
drwxr-xr-x 91 mark 3.0K Oct 3 12:50 ..
-rw-r--r-- 1 mark 1.3K Oct 3 16:28 spawning-processes.go
下面展示了从Go程序启动一个新的进程。与上一个例子不同,这里启动新进程后,原Go程序会主动退出。
package mainimport ("os""os/exec""syscall"
)func main() {binary, lookErr := exec.LookPath("ls")if lookErr != nil {panic(lookErr)}args := []string{"ls", "-a", "-l", "-h"}env := os.Environ()execErr := syscall.Exec(binary, args, env)if execErr != nil {panic(execErr)}
}
$ go run execing-processes.go
total 16
drwxr-xr-x 4 mark 136B Oct 3 16:29 .
drwxr-xr-x 91 mark 3.0K Oct 3 12:50 ..
-rw-r--r-- 1 mark 1.3K Oct 3 16:28 execing-processes.go
下面例子展示了信号量的使用。通过监听中止信号量,在外部强行中断程序后,还能执行一段结束代码。
package mainimport ("fmt""os""os/signal""syscall"
)func main() {sigs := make(chan os.Signal, 1)// 监听程序中止的信号量,并存入通道sigssignal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)done := make(chan bool, 1)go func() {// 从sigs通道读取信号量并打印sig := <-sigsfmt.Println()fmt.Println(sig)done <- true}()fmt.Println("awaiting signal")<-donefmt.Println("exiting")
}
$ go run signals.go
awaiting signal
^C
interrupt
exiting
下面例子展示了以指定退出码主动退出进程。主动退出时不会指定defer语句。
package mainimport ("fmt""os"
)func main() {defer fmt.Println("!")os.Exit(3)
}
$ go run exit.go
exit status 3$ go build exit.go
$ ./exit
$ echo $?
3