本教程全面涵盖了Go语言基础的各个方面。一共80个例子,每个例子对应一个语言特性点,既适合新人快速上手,也适合工作中遇到问题速查知识点。
教程代码示例来自go by example,文字部分来自本人自己的理解。
本文是教程系列的第四部分,共计20个例子、约1.5万字。
系列文章快速跳转:
跟着实例学Go语言(一)
跟着实例学Go语言(二)
跟着实例学Go语言(三)
跟着实例学Go语言(四)
目录
- 61. Base64 Encoding
- 62. Reading Files
- 63. Writing Files
- 64. Line Filters
- 65. File Paths
- 66. Directories
- 67. Temporary Files and Directories
- 68. Embed Directive
- 69. Testing and Benchmarking
- 70. Command-Line Arguments
- 71. Command-Line Flags
- 72. Command-Line Subcommands
- 73. Environment Variables
- 74. HTTP Client
- 75. HTTP Server
- 76. Context
- 77. Spawning Processes
- 78. Exec'ing Processes
- 79. Signals
- 80. Exit
61. Base64 Encoding
Go提供了Base64编解码的功能。Base64是将二进制数据转换成可读字符串的编码方式。例如我们用记事本打开jpg文件,会看到一串乱码,这个就是Base64转换后的字符串。Base64提供了64种不同的字符,6位编码表示一个字符。
package main
import (
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!?$*&()'-=@~
62. Reading Files
下面例子展示了读文件的操作。
package main
import (
"bufio"
"fmt"
"io"
"os"
)
func check(e error) {
if e != nil {
panic(e)
}
}
func main() {
// 直接从文件读到byte slice
dat, 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包提供了带缓冲的读写并提供一些更简便的操作,这里创建了带缓冲的reader
r4 := bufio.NewReader(f)
// 获取后面5个字节,但不实际移动指针,类似于队列中的peek
b4, 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
63. Writing Files
下面例子展示了写文件的操作。
package main
import (
"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创建自带缓冲的writer
w := 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
64. Line Filters
下面例子展示了从标准输入中读取并解析。类似于Java中的Scanner和Linux下的grep、sed命令。
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
// 从标准输入创建scanner
scanner := 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
65. File Paths
下面例子展示了对文件路径的各种操作。
package main
import (
"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
66. Directories
下面例子展示了对目录的相关操作。
package main
import (
"fmt"
"os"
"path/filepath"
)
func check(e error) {
if e != nil {
panic(e)
}
}
func main() {
// 使用指定的名字和权限创建目录
err := os.Mkdir("subdir", 0755)
check(err)
// 递归删除目录,等效于rm -rf
defer os.RemoveAll("subdir")
createEmptyFile := func(name string) {
d := []byte("")
// 通过写空数据创建文件,文件不存在就创建
check(os.WriteFile(name, d, 0644))
}
createEmptyFile("subdir/file1")
// 强制创建目录,若路径上的父级不存在则创建,等效于mkdir -p
err = 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)
// 对当前目录执行ReadDir
c, 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/parent
child true
file2 false
file3 false
Listing subdir/parent/child
file4 false
Visiting subdir
subdir true
subdir/file1 false
subdir/parent true
subdir/parent/child true
subdir/parent/child/file4 false
subdir/parent/file2 false
subdir/parent/file3 false
67. Temporary Files and Directories
下面例子展示了对临时文件和目录的操作。
package main
import (
"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
68. Embed Directive
embed编译指令可以在程序运行时从指定的文件获取数据到程序变量。这种编程风格有点类似于Srping中的依赖注入。
package main
import (
"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.FS
func 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
69. Testing and Benchmarking
Go对于单元测试和基准测试提供了原生的支持。仅需以指定的格式命名函数,通过正则匹配会被自动识别为单元测试或基准测试用例,再用go test命令启动测试。
package main
import (
"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 int
want 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
70. Command-Line Arguments
通过Go提供的命令行api,可以方便地把Go编译出来的程序用作命令行工具。下面例子展示了Go程序读取命令行参数并处理的过程。
package main
import (
"fmt"
"os"
)
func main() {
// os.Args包含所有参数,当前文件编译生成的可执行文件为第一个参数
argsWithProg := os.Args
argsWithoutProg := 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
71. Command-Line Flags
Go支持命令行标志,为命令行提供了不同的选项。例如grep -c中的-c就是一种命令行标志。
package main
import (
"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 string
flag.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:
...
72. Command-Line Subcommands
Go支持命令行子命令。例如go build和run就是go下面的两个子命令。
package main
import (
"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: true
name: joe
tail: [a1 a2]
$ ./command-line-subcommands bar -level 8 a1
subcommand 'bar'
level: 8
tail: [a1]
$ ./command-line-subcommands bar -enable a1
flag provided but not defined: -enable
Usage of bar:
-level int
level
73. Environment Variables
下面例子展示了环境变量的设置和读取。
package main
import (
"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
...
74. HTTP Client
Go提供了方便的Http请求和处理的工具。下面是HTTP客户端的例子。
package main
import (
"bufio"
"fmt"
"net/http"
)
func main() {
// 仅一句Get就能访问http url
resp, 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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Go by Example</title>
75. HTTP Server
下面是HTTP服务端的例子。仅需HandleFunc和ListenAndServe两句代码就能实现一个简单的服务端,非常方便。
package main
import (
"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
76. Context
下面例子展示了通过HTTP请求上下文处理请求取消的操作。
package main
import (
"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.StatusInternalServerError
http.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
77. Spawning Processes
下面例子展示了在Go程序中调用其他命令,如date,并获取返回值。
package main
import (
"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 PDT
command 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
78. Exec’ing Processes
下面展示了从Go程序启动一个新的进程。与上一个例子不同,这里启动新进程后,原Go程序会主动退出。
package main
import (
"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
79. Signals
下面例子展示了信号量的使用。通过监听中止信号量,在外部强行中断程序后,还能执行一段结束代码。
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
)
func main() {
sigs := make(chan os.Signal, 1)
// 监听程序中止的信号量,并存入通道sigs
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
done := make(chan bool, 1)
go func() {
// 从sigs通道读取信号量并打印
sig := <-sigs
fmt.Println()
fmt.Println(sig)
done <- true
}()
fmt.Println("awaiting signal")
<-done
fmt.Println("exiting")
}
$ go run signals.go
awaiting signal
^C
interrupt
exiting
80. Exit
下面例子展示了以指定退出码主动退出进程。主动退出时不会指定defer语句。
package main
import (
"fmt"
"os"
)
func main() {
defer fmt.Println("!")
os.Exit(3)
}
$ go run exit.go
exit status 3
$ go build exit.go
$ ./exit
$ echo $?
3