Golang基础知识-第二部分

Go的异常处理

在Go中,它采用独特的错误处理机制,与传统的python这些语言,使用 try-catch 不同,主要通过返回值来处理错误

也就是说,使用的是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
}

日志处理

文件处理

与操作系统交互

例如执行Linux命令

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对象,用于后续处理
1、序列化(Marshal)
把 Go 值 变成 JSON 字符串(便于存储、网络传输)
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)

把 JSON 字符串 还原成 Go 值(便于程序内部使用)
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) 来控制并发请求的数量。