文件操作:os、io、bufio

Go语言中,文件操作主要分三层

  • os : 直接和操作系统交互,比如打开文件、删除文件、创建文件、读写文件等。
  • io :抽象I/O行为,比如:io.Reader、io.Writer、io.Copy
  • bufio : 带缓冲的读写,适合按行读取、大文件处理、减少系统调用。

os.ReadFile

一次性读取整个文件,适合读取小文件,例如配置文件、模板文件、JSON文件。

1
2
3
4
5
6
7
8
func TestOsReadFile(t *testing.T) {
data, err := os.ReadFile("d:/a.txt")
if err != nil {
t.Error(err)
}

fmt.Println(string(data))
}

注意:不要用它读取特别大的文件,因为会一次性把整个文件加载到内存。

os.WriteFile

一次性写入文件

1
2
3
4
5
6
7
8
9
10
11
12
func TestOsWriteFile(t *testing.T) {

data := []byte("Hello World\n")

err := os.WriteFile("d:/a1.txt", data, 0644)
if err != nil {
t.Error(err)
return
}

fmt.Println("write success")
}

0644是权限标志,代表:owner:读写(6) 、group:只读(4)、others: 只读(4),等同于linux中的rw-r--r--。另外:可执行权限是1(x)。

os.Open

打开文件:os.Open默认是只读打开。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func TestOsOpen(t *testing.T) {
file, err := os.Open("d:/a.txt")
if err != nil {
t.Error(err)
return
}
// 关闭文件
//defer file.Close()

defer func(file *os.File) {
err := file.Close()
if err != nil {
t.Error(err)
}
}(file)

t.Logf("file name: %s", file.Name())
}

打开文件后,一定要关闭,日常Go代码里,打开资源后立刻defer Close()是很常见的习惯。

不过,这里和Java一样,也要处理关闭时的异常。推荐使用如下代码:

1
2
3
4
5
6
defer func(file *os.File) {
err := file.Close()
if err != nil {
t.Error(err)
}
}(file)

os.Create

创建文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func TestOsCreate(t *testing.T) {
file, err := os.Create("d:/a2.txt")
if err != nil {
t.Error(err)
return
}
defer func(file *os.File) {
if err := file.Close(); err != nil {
t.Logf("close file failed : %v", err)
}
}(file)

_, err = file.WriteString("hello world \n")
if err != nil {
t.Error(err)
return
}
}

如果文件已经存在,会清空源文件内容。

os.OpenFile

追加写入文件:日常开发里的日志、追加数据等场景可以用到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func TestOsOpenFile(t *testing.T) {
file, err := os.OpenFile("a.txt", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
t.Error(err)
return
}

defer func(file *os.File) {
err := file.Close()
if err != nil {
t.Error(err)
}
}(file)

_, err = file.WriteString("\n666666666666666666666666666\n")
if err != nil {
t.Error(err)
return
}
}

其中第二个参数是flag,常见的有:

  • os.O_CREATE : 文件不存在则创建
  • os.O_WRONLY :只写
  • os.O_RDWR :读写
  • os.O_APPEND : 追加写
  • os.O_TRUNC: 打开时清空

os.ErrNotExist

判断文件是否存在

1
2
3
4
5
6
7
8
9
10
11
12
13
func TestOsErrNotExist(t *testing.T) {
_, err := os.Stat("ab.txt")
//if err != nil {
// t.Error(err)
// return
//}
// 推荐使用errors.Is判断错误类型
if errors.Is(err, os.ErrNotExist) {
t.Error("file does not exist")
return
}
t.Log("stat file", err)
}

io.Reader和io.Writer

这是Go语言关于I/O的核心抽象

io.Reader的定义大概如下:

1
2
3
type Reader interface {
Read(p []byte) (n int,err error)
}

io.Writer的定义大概如下:

1
2
3
type Writer interface {
Write(p []byte) (n int,err error)
}

io包的主要作用就是把各种I/O实现抽象成通用接口,例如文件网络链接HTTP响应体字符串reader都可以被统一处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
这个函数可以接受任何io.Reader
printAll(file)
printAll(resp.Body)
printAll(strings.NewReader("abc"))
*/
func printAll(r io.Reader) error {
data, err := io.ReadAll(r)
if err != nil {
return err
}
fmt.Println(string(data))
return nil
}

func TestIOReader(t *testing.T) {
r := strings.NewReader("Hello Reader")
err := printAll(r)
if err != nil {
t.Fatal(err)
}
}

这就是Go的接口设计风格:小接口、非常灵活。

io.Copy

复制数据流,比如把一个我呢见复制到另一个文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
func TestIOCopy(t *testing.T) {
src, err := os.Open("a.txt")
if err != nil {
t.Error(err)
return
}

defer func(file *os.File) {
err := src.Close()
if err != nil {
t.Error(err)
}
}(src)

dest, err := os.Create("b.txt")
if err != nil {
t.Error(err)
return
}

defer func(file *os.File) {
err := file.Close()
if err != nil {
t.Error(err)
}
}(dest)

n, err := io.Copy(dest, src)
if err != nil {
t.Error(err)
}

t.Log("copied bytes ", n)

}

使用场景:文件复制HTTP 下载保存到文件上传文件写入磁盘把请求体转发给另一个服务等。

bufio.Scanner

按行读取文件,这是处理日志、CSV、文本文件时非常常用的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func TestBufioScanner(t *testing.T) {
file, err := os.Open("info.log")
if err != nil {
t.Error(err)
return
}

defer func(file *os.File) {
err := file.Close()
if err != nil {
t.Error(err)
}
}(file)

scanner := bufio.NewScanner(file)

// Scanner默认单个token大小有限,如果读取超长行或者大日志等,需要调大buffer
// scanner.Buffer(make([]byte ,1024),1024*1024)

for scanner.Scan() {
line := scanner.Text()
t.Log(line)
}

if err := scanner.Err(); err != nil {
t.Error(err)
}
}

bufio会包装一个io.Readerio.Writer,提供缓冲能力和更方便的文本I/O操作。

Scanner默认单个token大小有限。如果你读取超长行,可能需要调大buffer。

bufio.Writer

缓冲写入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func TestBufioWriter(t *testing.T) {
file, err := os.Create("bufio_writer.txt")
if err != nil {
t.Error(err)
return
}

defer func(file *os.File) {
err := file.Close()
if err != nil {
t.Error(err)
}
}(file)

writer := bufio.NewWriter(file)
for i := 0; i < 3; i++ {
_, err := writer.WriteString(fmt.Sprintf("line %d\n", i))
if err != nil {
t.Error(err)
return
}
}
// 注意,这里需要flush,否则不会落盘
if err := writer.Flush(); err != nil {
t.Error(err)
}
}

使用bufio.Writer后,数据可能还在缓冲区里,不调用Flush()可能不会真正写入文件。

一句话总结:

os负责具体的文件和操作系统交互,io提供通用I/O抽象,核心时io.Readerio.Writerbufio在Reader/Writer外层增加缓冲,适合按行读取和批量写入。小文件可以使用os.ReadFile,大文件或流式处理应该用os.Open配合bufio.Scannerbufio.Reader或者io.Copy

JSON 处理:encoding/json

Go标准库的encoding/json用于JSON编码和解码,也是Go对象与JSON字符串之间的转换。常见操作有:

1
2
3
4
json.Marshal               : Go struct/map -> JSON bytes
json.Unmarshal : JSON bytes -> Go struct/map
json.NewEncoder : 把JSON写到io.Writer
json.NewDecoder : 从io.Reader 读取 JSON

先定义一个user

1
2
3
4
5
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
}

json.Marshal

struct/map 转JSON

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func TestJsonMarsha1(t *testing.T) {
u := User{
ID: 1,
Name: "tom",
Age: 18,
}

data, err := json.Marshal(u)
if err != nil {
t.Error(err)
return
}
t.Log(string(data))

m := map[string]any{
"id": 1,
"age": 18,
"phone": 1323232,
"name": "Jerry",
}

dataMap, err := json.Marshal(m)
if err != nil {
t.Error(err)
return
}
t.Log(string(dataMap))
}

输出:

1
2
{"id":1,"name":"tom","age":18}
{"age":18,"id":1,"name":"Jerry","phone":1323232}

json.Unmarshal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func TestJsonUnmarshal(t *testing.T) {
input := []byte(`{"id":1,"name":"tom","age":18}`)
var u User
var m map[string]any

err := json.Unmarshal(input, &u)
if err != nil {
t.Error(err)
return
}
t.Logf("%+v\n", u)
// 通常建议 err复用即可,因为本身就是一个临时变量。
err = json.Unmarshal(input, &m)
if err != nil {
t.Error(err)
return
}
t.Logf("%+v\n", m)
}

输出:

1
2
{ID:1 Name:tom Age:18}
map[age:18 id:1 name:tom]

从上述例子可以看到,json.Unmarshal(input,&u)这里必须传递指针,因为要修改u的值。

JSON tag

1
2
3
4
5
6
type UserInfo struct {
ID int `json:"id"`
UserName string `json:"user_name"`
Password int `json:"-"`
Email string `json:"email,omitempty"`
}

含义:

1
2
3
4
json:"id"               JSON字段名是id
json:"uer_name" JSON字段名是user_name
json:"-" 忽略该字段
json:"email,omitempty" 空值时忽略

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func TestJsonTag(t *testing.T) {
u := UserInfo{
ID: 1,
UserName: "tom",
Password: "123456",
}

s, err := json.Marshal(u)
if err != nil {
t.Error(err)
return
}
t.Log(string(s))
}

输出:

1
{"id":1,"user_name":"tom"}

注意:Go的JSON序列化只能处理导出字段,也就是大写开头的字段。

下面的这个就无法序列化成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type UserInfo1 struct {
id int `json:"id"`
name string `json:"user_name"`
}

func TestJsonTag1(t *testing.T) {
u := UserInfo1{
id: 1,
name: "tom",
}
s, err := json.Marshal(u)
if err != nil {
t.Error(err)
return
}
// 输出 {}
t.Log(string(s))
}

Decoder/Encoder

适合HTTP和流式处理,例如在HTTP handler里,经常会这样写:

1
2
json.NewDecoder(r.Body).Decode(&req)
json.NewEncoder(w).Encode(resp)

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
type CreateUserRequest struct {
Name string `json:"name"`
Age int `json:"age"`
}

type CreateUserResponse struct {
ID int `json:"id"`
Name string `json:"name"`
}

func createUserHandler(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
resp := CreateUserResponse{
ID: 1,
Name: req.Name,
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(resp); err != nil {
return
}

}

Encoder/Decoder的好处是可以直接对接io.Readerio.Writer,不用先手动转换为[]byte

DisallowUnknownFields

拒绝未知字段:默认情况下,Go会忽略JSON里的未知字段。

1
{“name”:"Tom","age":20,"unknown":"xxx"}

如果struct里没有unknown字段,默认不会报错。

如果需要严格校验:

1
2
3
decoder := json.NewDecoder(r.Body)
//
decoder.DisallowUnknownFields()

API开发里很有用,可以避免前端传错字段。

除了上述提到的小写无法序列化(非导出)Unmarshal需要传递指针外,还有一个需要注意的就是数字会被解析为flaot64

1
2
3
4
var m map[string]any
json.Unmarshal([]byte(`{"id":123}`),&m)
// float64
fmt.Printf("%T\n",m["id"])

如果需要精确处理数字,可以使用struct,或者使用Decoder.UseNumber()

HTTP 服务:net/http

net/http是Go标准库里非常重要的包,它同时提供HTTP client和 HTTP server实现。

一个简单的HTTP服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func helloHandler(w http.ResponseWriter, r *http.Request) {
_, err := fmt.Fprintln(w, "hello go")
if err != nil {
return
}
}

func TestNet(t *testing.T) {
http.HandleFunc("/hello", helloHandler)

err := http.ListenAndServe(":8080", nil)
if err != nil {
panic(err)
}
}

http.ResponseWriter和 *http.Request

handle的核心签名是:

1
func(w http.ResponseWriter,r * http.Request)

可以理解为:

  • w:用来写响应
  • r:表示当前请求

例如:

1
2
3
4
5
6
7
8
9
10
func userHandler(w http.ResponseWriter, r *http.Request) {
method := r.Method
path := r.URL.Path
query := r.URL.Query().Get("name")

fmt.Println(method, path, query)
}

// 在上一个例子中,添加如下:
http.HandleFunc("/user", userHandler)

然后再Goland中的rest-api.http的请求中执行如下:

1
GET http://localhost:8080/user?name=zhangsan

按Method区分请求

userHandler方法修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func userHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode([]User{
{ID: 1, Name: "tom"},
})
case http.MethodPost:
var req User
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invaild json", http.StatusBadRequest)
return
}

req.ID = 100
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
err := json.NewEncoder(w).Encode(req)
if err != nil {
return
}
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}

自定义http.Server

http.ListenAndSarver(":8080",nil)通常也只是用在demo阶段,实际工作中还是会显示的创建http.Server,配置超时时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func TestHttpServer(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
_, err := w.Write([]byte("ok"))
if err != nil {
return
}
})

server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}

log.Println("server start at :8080")

if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}

HTTP Client

HTTP客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func TestHttpClient(t *testing.T) {
client := &http.Client{
Timeout: 5 * time.Second,
}

resp, err := client.Get("https://localhost:8080/health")
if err != nil {
fmt.Println("request failed: ", err)
return
}
// 关闭的模板代码
defer func(resp *http.Response) {
if err := (*resp).Body.Close(); err != nil {
fmt.Println("close response body failed: ", err)
}
}(resp)

body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("read body failed ", err)
}
fmt.Println("status: ", resp.StatusCode)
fmt.Println(string(body))
}

和读取文件一样(本质上也是IO流),HTTP响应体必须关闭,否则会造成连接泄露。

带Context的请求

日常开发,更推荐使用context控制超时和取消

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func TestContext(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://localhost:8080/health", nil)
if err != nil {
fmt.Println("new request failed ", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Println("request failed : ", err)
}
defer func(resp *http.Response) {
err := resp.Body.Close()
if err != nil {
fmt.Println("close failed ")
return
}
}(resp)
data, _ := io.ReadAll(resp.Body)
fmt.Println(string(data))
}

用httptest测试HTTP handler

net/http/httptest是测试HTTP handler的标准工具。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func helloHandler1(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, err := w.Write([]byte("hello"))
if err != nil {
fmt.Println("resp error")
return
}
}

func TestHelloHandler1(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/hello", nil)
rec := httptest.NewRecorder()

helloHandler1(rec, req)

if rec.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d ", rec.Code)
}

if rec.Body.String() != "hello" {
t.Fatalf("unexpected body : %s ", rec.Body.String())
}
}

关于网络

net/http同时提供HTTP服务端和客户端。服务端核心是http.Handler,常见的方法签名是func(w http.ResponseWriter, r *http.Request)。 ResponseWriter 用来写响应,Request表示请求。通常编码时推荐使用自定义http.Server并且设置timeout。客户端请求后必须关闭resp.Body,并且推荐使用context控制超时和取消。

字符串处理:strings、strconv

strings用来处理字符串查找、切割、拼接、转换、裁剪等操作;strconv用于字符串和基础类型之间的转换。例如 string转换为intintstring等。

strings.Contains

判断是否包含

1
2
3
4
5
func TestStringsContains(t *testing.T) {
s := "hello go"
t.Log(strings.Contains(s, "ll"))
t.Log(strings.Contains(s, "golang"))
}

strings.HasPrefix/strings.HasSuffix

前缀和后缀

1
2
3
4
5
func TestHasPrefix(t *testing.T) {
url := "https://www.google.com"
t.Log(strings.HasPrefix(url, "https"))
t.Log(strings.HasSuffix(url, ".com"))
}

strings.Split/strings.Join

1
2
3
4
5
6
7
8
9
10
11
func TestSplit(t *testing.T) {
s := "go,java,python,typescript"
parts := strings.Split(s, ",")
for _, part := range parts {
t.Log(part)
}
// []string -> string
joined := strings.Join(parts, "|")
// go|java|python|typescript
t.Log(joined)
}

strings.TrimSpace

去掉首位空白

1
2
3
4
5
func TestTripSpace(t *testing.T) {
s := " Hello Go "
//Hello Go
t.Log(strings.TrimSpace(s))
}

strings.ReplaceAll

替换所有字符串

1
2
3
4
func TestReplaceALl(t *testing.T) {
s := "Hello Go"
t.Log(strings.ReplaceAll(s, "Go", "Golang"))
}

strings.Builder

拼接字符串,类似Java里的StringBuilder

1
2
3
4
5
6
7
8
func TestBuilder(t *testing.T) {
var sb strings.Builder
for i := 0; i < 3; i++ {
sb.WriteString("GO")
sb.WriteString(" ")
}
t.Log(sb.String())
}

这玩意也是为了解决大量字符串拼接的问题。解决string += "xxx"的问题。

len(string)统计字节数

这个在第二天的只是中学习过,注意:它统计的是字节数,而不是字符数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func TestLen(t *testing.T) {
s := "你好,Go"
for i, r := range s {
t.Log(i, string(r))
}
/**
strings_test.go:55: 0 你
strings_test.go:55: 3 好
strings_test.go:55: 6 ,
strings_test.go:55: 9 G
strings_test.go:55: 10 o
*/

// 5 如何需要按字符数
t.Log(len([]rune(s)))

}

strconv.Atoi/Itoa

这个也是第二天的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
func TestStrconv(t *testing.T) {
s := "123"
n, err := strconv.Atoi(s)
if err != nil {
t.Error("convert failed", err)
}
// string: 123 ,convert int
t.Logf("string: %s ,convert %T", s, n)

num := 888
str := strconv.Itoa(num)
t.Logf("str type : %T, value : %s", str, str)
}

strconv.ParseInt/FormatInt

如果需要控制进制和位数,用ParseInt(ParseFloatParseBoolParseComplexParseUint)

1
2
3
4
5
6
7
8
9
10
11
12
func TestParseInt(t *testing.T) {
s := "123"
n, err := strconv.ParseInt(s, 10, 64)
if err != nil {
t.Error("convert failed", err)
}

t.Logf("string: %s ,convert %T", s, n)

s1 := strconv.FormatInt(n, 10)
t.Logf("str type : %T, value : %s", s1, s1)
}

其中:123是输出的字符串,10十进制64int64

单元测试:testing 包

Go的测试不需要JUnit这种外部框架,标准库自带testing包,配合go test命令就可以完成测试。例如我大量的例子都是,其规则为:1、文件名,要以_test.go结尾,函数必须是func TestXxx(*testing.T)

例如:math_test.go,创建一个加法代码:

1
2
3
func Add(a, b int) int {
return a + b
}

测试代码:

1
2
3
4
5
6
7
8
func TestMath(t *testing.T) {
got := Add(1, 2)
want := 3

if got != want {
t.Fatalf("Add(1,2) = %d,want %d", got, want)
}
}

t.Error和t.Fatal的区别

t.Error/t.Errorf:报错但继续

t.Fatal/t.Fatalf:保存并停止当前测试

表驱动测试

日常开发中非常重要的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func TestAdd(t *testing.T) {
tests := []struct {
name string
a int
b int
want int
}{
{name: "positive number", a: 1, b: 2, want: 3},
{name: "zero", a: 0, b: 5, want: 5},
{name: "negative number", a: -1, b: 1, want: 0},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Add(tt.a, tt.b)
if got != tt.want {
t.Fatalf("Add(%d,%d) = %d,want %d", tt.a, tt.b, got, tt.want)
}
})
}
}