优化一个已有的 Go 程序,提高其性能并减少资源占用 |青训营

79 阅读5分钟

引言和介绍

优化 Go 程序的性能和资源占用是非常必要的,通过优化可以:

  • 减少程序的执行时间,提高整体的吞吐量和响应性能,使用户获得更好的体验,

  • 可以减少程序对系统资源的占用,如内存、CPU、网络等。减少硬件成本。

  • 同时性能优化是提升产品和服务竞争力的一种重要手段。

使用合适的数据结构和算法,并发处理,使用高效的库和工具,基准测试和持续优化,减少系统调用是进行go程序优化的一般途径。

实例演习

package main

import (
	"fmt"
	"log"
	"net/http"
)
// 定义处理程序函数(Handler Function)
func handler(w http.ResponseWriter, r *http.Request) {
	// 向 HTTP 响应中写入 "Hello, World!" 字符串
	fmt.Fprintf(w, "Hello, World!")
}
func main() {
	// 注册处理程序函数,当收到根路径请求时会执行该函数
	http.HandleFunc("/", handler)

	// 启动 HTTP 服务器,并监听 8080 端口
	log.Println("启动 HTTP 服务器...")
	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		// 如果启动失败,则输出错误日志并退出程序
		log.Fatal("HTTP 服务器启动失败:", err)
	}
}

代码分析

我们使用 Go 标准库创建了一个简单的 HTTP 服务器。在主函数中监听本地的 8080 端口,当有请求访问根路径时,服务器会返回 "Hello, World!" 的响应。

如果启动过程中出现错误,则使用 log.Fatal 函数记录错误信息,并终止程序的执行。

优化策略

并发处理:

将处理函数包装在协程中,允许并发处理多个请求。

// 定义处理程序函数(Handler Function)
func handler(w http.ResponseWriter, r *http.Request) {
	// 设置响应头的 Content-Type 为 text/plain
	w.Header().Set("Content-Type", "text/plain")

	// 创建一个用于通信的字符串类型的通道
	ch := make(chan string)

	// 启动一个 goroutine 来异步执行任务,并将结果发送到通道
	go func() {
		ch <- "Hello, World!" // 将 "Hello, World!" 传递给通道
	}()

	response := <-ch // 从通道中接收响应

	fmt.Fprint(w, response) // 将响应写入 HTTP 响应中
}

在处理函数 handler 中,我们首先通过 w.Header().Set() 设置响应头部的 Content-Type。然后创建一个通道 ch 用于接收处理结果。

接着,在协程中生成响应字符串并发送到通道中。在主协程中,我们从通道 ch 中接收处理结果,并将结果写入 http.ResponseWriter 中。

这样就能够实现在不阻塞主协程的情况下并发处理请求,并将处理结果正确地写入到 http.ResponseWriter 中。

连接复用:

使用 Keep-Alive 特性启用连接复用,避免频繁建立和断开连接。

func main() {
	// 创建一个 HTTP 服务器实例
	server := &http.Server{
		Addr:    ":8080",                     // 监听地址和端口号
		Handler: http.HandlerFunc(handler),    // 设置处理程序函数
	}

	server.SetKeepAlivesEnabled(true)          // 启用 Keep-Alive 连接

	log.Println("启动 HTTP 服务器...")

	// 启动并监听 HTTP 服务器
	err := server.ListenAndServe()
	if err != nil {
		log.Fatal("HTTP 服务器启动失败:", err)
	}
}

通过调用 server.SetKeepAlivesEnabled(true) 方法,告诉了 HTTP 服务器要启用连接复用。这将使得服务器在处理完一个请求后保持连接打开,以便可以在后续请求中复用这个连接。然后,通过调用 server.ListenAndServe() 方法来启动 HTTP 服务器,并监听在地址 :8080 上。

当有新的请求到达时,HTTP 服务器会使用指定的 handler 函数来处理该请求。

基准测试

基准测试是用于评估代码性能的一种方法,它可以比较不同算法或数据结构的效率、优化代码性能等。下面我们通过实例初步了解其具体的使用。

import (
"testing"
"io"
)

基准测试需要引入testing包, 视演示代码情况而定引入io

func BenchmarkHTTPServer(b *testing.B) {
	// 创建一个测试用的 HTTP 服务器实例
	server := httptest.NewServer(http.HandlerFunc(handler))
	defer server.Close()
	// 使用测试服务器创建 HTTP 客户端
	client := server.Client()
	// 重置计时器并循环执行测试
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		// 发送 GET 请求并接收响应
		resp, err := client.Get(server.URL)
		if err != nil {
			log.Fatal("请求失败:", err)
		}
		// 将响应体内容读取并丢弃
		_, err = io.Copy(io.Discard, resp.Body)
		if err != nil {
			log.Fatal("响应解析失败:", err)
		}
		// 关闭响应体
		resp.Body.Close()
	}
}

这里我们定义一个基准测试函数BenchmarkHTTPServer,该函数通过httptest.NewServer创建一个模拟的 HTTP 服务器,并使用基准测试循环模拟多个并发请求。在基准测试函数中,我们使用client.Get发送 GET 请求并测量性能。

	// 运行基准测试
	result := testing.Benchmark(BenchmarkHTTPServer)
	fmt.Println(result)

main函数中,我们需要运行基准测试函数,并将结果打印出来。

通过在优化前后的代码中添加以上代码后,我们便可以运行程序进行性能测试。

如图可见:

image.png 优化前,执行了 29929 次基准测试迭代,平均操作时间为 43026 纳秒/操作,每个操作约耗时 43.026 微秒。

优化后,执行了 30134 次基准测试迭代,平均操作时间为 38046 纳秒/操作,每个操作约耗时 38.046 微秒。

补充:pprof

go语言中还提供了pprof进行性能调试 具体内容可参照下篇。

实用go pprof使用指南 - 知乎 (zhihu.com)

	mux := http.NewServeMux()
	// 注册 pprof 的路由处理器
	mux.Handle("/debug/pprof/", http.HandlerFunc(pprof.Index))
	mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
	mux.HandleFunc("/debug/pprof/trace", pprof.Trace)

除了该片中所提到的网页导出数据,亦可以使用以下代码加到程序开始运行处。运行之后通过访问http://localhost:8080/debug/pprof/得到如下界面进行数据采集。

(该网页访问localhost:8080,预计需要安装TomCat)

image.png

总结

Go语言提供了便利的工具去分析程序的性能,以此我们再从程序的应用场景对程序进行适当的优化改进。熟练的运用这些工具并熟悉语法对症下药确实很重要,还需要多积累项目经验。

(好事多磨,不断求解总算解决。)