Golang基础知识-第二部分
- Golang
- 2天前
- 23热度
- 0评论
Go的异常处理
也就是说,使用的是error这个特殊类型
基本使用
一个简单的例子如下:
func processFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return fmt.Errorf("打开文件失败: %w", err) // %w 用于错误包装
}
defer file.Close()
// 处理文件...
return nil
}
// 调用方处理错误
func main() {
err := processFile("test.txt")
if err != nil {
fmt.Printf("错误: %v\n", err)
// 可以获取原始错误
if originalErr := errors.Unwrap(err); originalErr != nil {
fmt.Printf("原始错误: %v\n", originalErr)
}
}
}
可以看到,我们在定义函数以及调用函数的时候,都是使用函数的返回值进行处理
通过判断返回值的error是否为nil,来判断在执行过程中,是否出现了异常
错误类型的判断
func handleError(err error) {
// 1. 使用 errors.Is 检查特定错误
if errors.Is(err, os.ErrNotExist) {
fmt.Println("文件不存在")
return
}
// 2. 使用 errors.As 类型断言
var divisionErr *DivisionError
if errors.As(err, &divisionErr) {
fmt.Printf("自定义错误: %v\n", divisionErr)
return
}
// 3. 类型断言(不推荐,不如 errors.As)
if e, ok := err.(*os.PathError); ok {
fmt.Printf("路径错误: %v\n", e)
return
}
fmt.Printf("未知错误: %v\n", err)
}
panic和recover
panic-不可恢复的异常
func riskyFunction() {
defer fmt.Println("defer 语句会在 panic 后执行")
// 触发 panic
panic("发生严重错误!")
fmt.Println("这行不会执行")
}
// 调用 panic 的函数
func callRiskyFunction() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("恢复 panic: %v\n", r)
}
}()
riskyFunction()
fmt.Println("如果 recover 成功,这行会执行")
}
recover的使用
// 安全的函数执行包装器
func SafeExecute(f func()) (err error) {
defer func() {
if r := recover(); r != nil {
// 将 panic 转换为 error
if e, ok := r.(error); ok {
err = fmt.Errorf("panic recovered: %w", e)
} else {
err = fmt.Errorf("panic recovered: %v", r)
}
// 记录堆栈信息
debug.PrintStack()
}
}()
f()
return nil
}
func main() {
err := SafeExecute(func() {
// 可能 panic 的代码
panic("测试 panic")
})
if err != nil {
fmt.Printf("执行出错: %v\n", err)
}
}
错误处理的最佳实践
多返回值
// 返回错误应该放在最后一个
func ReadConfig(path string) (Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return Config{}, fmt.Errorf("读取配置文件失败: %w", err)
}
var config Config
if err := json.Unmarshal(data, &config); err != nil {
return Config{}, fmt.Errorf("解析配置文件失败: %w", err)
}
return config, nil
}
错误包装和上下文
// 错误包装链
func Process() error {
if err := step1(); err != nil {
return fmt.Errorf("处理步骤1失败: %w", err)
}
if err := step2(); err != nil {
return fmt.Errorf("处理步骤2失败: %w", err)
}
return nil
}
// 使用 errors.Join 合并多个错误
func Validate(input string) error {
var errs []error
if len(input) == 0 {
errs = append(errs, errors.New("输入不能为空"))
}
if len(input) > 100 {
errs = append(errs, errors.New("输入过长"))
}
if !strings.Contains(input, "@") {
errs = append(errs, errors.New("必须包含@符号"))
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
资源清理
func ProcessWithResources() (err error) {
// 使用命名返回值,defer 中可以访问
file, err := os.Open("data.txt")
if err != nil {
return err
}
defer func() {
closeErr := file.Close()
if closeErr != nil && err == nil {
err = fmt.Errorf("关闭文件失败: %w", closeErr)
}
}()
// 处理文件内容...
return nil
}
日志处理
文件处理
与操作系统交互
go的指针处理
type Pool struct {
*sync.Mutex // 内嵌指针类型
conn []net.Conn
}
p := &Pool{}
p.Lock() // 直接调用 sync.Mutex 的方法
JSON数据的处理
- 序列化:Marshal(编码,Go → JSON),将go对象编码成JSON数据,一般是处理完成用于http客户端调用或其他输出
- 反序列化:Unmarshal(解码,JSON → Go),将JSON数据解码为go对象,用于后续处理
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
u := User{ID: 1, Name: "Alice"}
data, err := json.Marshal(u) // []byte(`{"id":1,"name":"Alice"}`)
2、反序列化(Unmarshal)
var u2 User
err := json.Unmarshal(data, &u2) // u2 == User{ID:1, Name:"Alice"}
一句话总结
- 序列化 = Go 对象 → JSON 字节流
- 反序列化 = JSON 字节流 → Go 对象
Go中的流程控制
for循环
if判断
switch
go的多线程
go的并发处理
go的类,继承等
常用第三方模块
内置模块
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"strings"
正则处理
输出excel
发送HTTP请求-net/http模块专项
Get请求
代码示例:
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
)
func main() {
// 发起GET请求
resp, err := http.Get("https://example.com")
if err != nil {
log.Fatal("Error fetching:", err)
}
defer resp.Body.Close() // 确保关闭响应体
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatal("Error reading response:", err)
}
fmt.Println("Status:", resp.Status)
fmt.Println("Body:", string(body))
}
Post请求
代码示例:
//发送这个json数据,使用net/http模块,发送http请求
// 准备POST请求的数据
payload := bytes.NewBuffer(json_http_data)
// 发起POST请求,并设置Content-Type头
resp, err := http.Post("http://127.0.0.1:8080/api/receive_data_test", "application/json", payload)
if err != nil {
log.Fatal("Error posting data:", err)
}
defer resp.Body.Close() //确保关闭响应体
// body, err := ioutil.ReadAll(resp.Body)
resp_body, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatal("Error reading response:", err)
}
这里的json_http_data为使用json.Marshal(send_list) 格式化的json数据
//json序列化操作
json_http_data, err := json.Marshal(send_list)
if err != nil {
// 处理错误
log.Println("marshal failed:", err)
return
}
defer resp.Body.Close() 这个的作用详细说明
它是 Go 语言中 确保网络资源及时释放 的一句“固定写法”,出现在每次使用 net/http 客户端之后。
它背后涉及 TCP 连接复用、文件描述符泄漏、HTTP 协议规范 等多个知识点。
为什么必须调 resp.Body.Close()?
1、HTTP 客户端底层是 TCP 连接
http.Response 的 Body 类型是 io.ReadCloser,底层持有 已建立/已复用的 TCP 连接(net.Conn)
简单说就是释放网络连接,因为底层 TCP 连接不会被 Go 的垃圾回收器自动释放,必须显式关闭以使其返回到连接池供后续请求复用。
2、复用HTTP长连接
http.Client 默认启用连接池(Keep-Alive),不关闭 Body 会阻碍该连接的复用,导致“连接泄漏”
执行之后,可以把连接归还给连接池
归还条件:
- 只有 把响应体完整读完 并 显式调用 Close(),Transport 才会把连接放回 idleConnPool,供后续请求复用;
否则
- 连接被标记为 in-use
- 文件描述符(FD)一直占用
- 高并发场景下很快触发 too many open files 错误。
3、协议层面的“优雅关闭”
- 对于 HTTP/1.1 Keep-Alive,Close() 会先把剩余数据 discard 再 putIdleConn;
- 对于 HTTP/2,Close() 会发送 RST_STREAM 并归还流/连接。
不关闭会产生的问题
代码示例
func main() {
for i := 0; i < 10_000; i++ {
resp, err := http.Get("https://httpbin.org/bytes/1024")
if err != nil { log.Fatal(err) }
// 故意不读 Body 也不 Close
// io.Copy(io.Discard, resp.Body) // 正确做法
// resp.Body.Close()
}
}
运行结果:
ulimit -n 256
运行几百次后 → socket: too many open files
通过 lsof -p $PID 可见大量 CLOSE_WAIT / ESTABLISHED 状态的 TCP 连接。
注意事项
注意如下:
- 位置有要求:defer resp.Body.Close() 必须 写在检查响应错误之后,但在读取数据(
io.ReadAll)之前。这是标准且安全的写法。 - 写在检查响应错误之后,在响应正确的前提下再进行操作
- 写在读取数据之前,不会导致读取不完,defer 只是声明了函数退出时要执行的操作,而不是立即执行。
常见错误点及规避方式
1、忽略响应体的关闭
问题:在客户端,如果未正确关闭响应体(resp.Body.Close()),会导致网络连接和内存资源泄露。
解决:始终使用 defer resp.Body.Close() 来确保响应体被关闭。
2、缺乏错误处理
问题:忽视对HTTP操作(如 http.Get、http.Post)返回错误的检查。
解决:始终检查并妥善处理错误,这是Go编程的基本原则。
3、服务器端资源泄露与超时设置
问题:服务器未配置超时,可能导致大量连接或Goroutine堆积,耗尽资源。
解决:使用 http.Server 的 ReadTimeout 和 WriteTimeout 等参数来管理连接生命周期。
4、路由设计混乱
问题:将所有路由逻辑都写在 http.HandleFunc 中,导致代码难以维护和扩展。
解决:对于复杂应用,考虑使用第三方路由库(如 gorilla/mux) 或自行设计清晰的路由结构。
5、并发请求控制不当
问题:在客户端,不加控制地并发发起大量HTTP请求,可能导致系统资源耗尽。
解决:使用 sync.WaitGroup 或带缓冲的通道(channel) 来控制并发请求的数量。
