文件操作: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 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 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 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) for scanner.Scan() { line := scanner.Text() t.Log(line) } if err := scanner.Err(); err != nil { t.Error(err) } }
bufio会包装一个io.Reader或io.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 } } if err := writer.Flush(); err != nil { t.Error(err) } }
使用bufio.Writer后,数据可能还在缓冲区里,不调用Flush()可能不会真正写入文件。
一句话总结:
os负责具体的文件和操作系统交互,io提供通用I/O抽象,核心时io.Reader和io.Writer,bufio在Reader/Writer外层增加缓冲,适合按行读取和批量写入。小文件可以使用os.ReadFile,大文件或流式处理应该用os.Open配合bufio.Scanner、bufio.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 = 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.Reader和io.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 ]anyjson.Unmarshal([]byte (`{"id":123}` ),&m) 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)
可以理解为:
例如:
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转换为int、int转string等。
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) } joined := strings.Join(parts, "|" ) t.Log(joined) }
strings.TrimSpace 去掉首位空白
1 2 3 4 5 func TestTripSpace (t *testing.T) { s := " 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)) } 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) } t.Logf("string: %s ,convert %T" , s, n) num := 888 str := strconv.Itoa(num) t.Logf("str type : %T, value : %s" , str, str) }
如果需要控制进制和位数,用ParseInt(ParseFloat、ParseBool、ParseComplex、ParseUint)
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: 十进制,64:int64
单元测试: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) } }) } }