为何使用fmt.Fprintf结合http.ResponseWriter无法正确输出CSS文件?
问题重现
这段Go代码试图对外提供CSS文件服务:
package main import ( "fmt" "io/ioutil" "log" "net/http" ) func main() { b, _ := ioutil.ReadFile("style1.css") // 文件大小7626字节 bigString := string(b) http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) { writer.Header().Set("Content-Type", "text/css") writer.Header().Set("Connection", "keep-alive") writer.Header().Set("Content-Length", fmt.Sprintf("%d", len(bigString))) fmt.Fprintf(writer, bigString) }) log.Fatal(http.ListenAndServe(":3000", nil)) }
执行curl http://localhost:3000/时会报错:curl: (18) transfer closed with 7626 bytes remaining to read,但把fmt.Fprintf(writer, bigString)替换成writer.Write([]byte(bigString))后,一切正常。
问题根源
fmt.Fprintf是格式化输出函数,它会把传入的字符串当成格式化模板解析——如果你的CSS文件里包含%字符(比如CSS的百分比值、特殊选择器或属性里的符号),fmt.Fprintf会将其视为格式化占位符,但你没有传递对应的参数,导致实际输出的内容被截断或篡改,最终输出的字节数远小于你在Content-Length头部声明的7626字节。
curl发现实际收到的字节数和头部承诺的不一致,就会抛出“传输关闭时还有字节未读取”的错误。
而writer.Write是直接将字节切片原封不动写入响应流,不会做任何解析或格式化,输出的字节数和Content-Length完全匹配,所以客户端能正常接收。
解决方案
有两种简单的修复方式:
直接使用原始字节切片写入(推荐,更高效)
跳过字符串转换步骤,直接用读取到的b写入响应,同时Content-Length直接用len(b)计算:// 读取文件后直接保留字节切片 b, err := os.ReadFile("style1.css") // 替代已废弃的ioutil.ReadFile if err != nil { log.Fatal(err) } http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) { writer.Header().Set("Content-Type", "text/css") writer.Header().Set("Connection", "keep-alive") writer.Header().Set("Content-Length", fmt.Sprintf("%d", len(b))) _, err := writer.Write(b) if err != nil { log.Printf("写入响应失败: %v", err) } })用fmt.Fprint替代fmt.Fprintf
如果一定要用fmt系列函数,用fmt.Fprint(writer, bigString)——它不会解析格式化占位符,只是单纯输出字符串内容,输出字节数和原字符串长度一致。
内容的提问来源于stack exchange,提问作者Andrew Arrow

