ResponseWriter.Write和io.WriteString之间有什么区别?

69

我看过三种向HTTP响应写入内容的方式:

func Handler(w http.ResponseWriter, req *http.Request) {
    io.WriteString(w, "blabla.\n")
}

同时:

func Handler(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("blabla\n"))
}

还有以下内容:

fmt.Fprintf(w, "blabla")

它们之间有什么区别?哪一个更受偏爱使用?

2个回答

104

io.Writer

一个输出流代表了一个你可以写入字节序列的目标。在Go语言中,这通过通用的io.Writer接口来实现:
type Writer interface {
    Write(p []byte) (n int, err error)
}

所有拥有单一Write()方法的对象都可以作为输出使用,例如您磁盘上的文件(os.File),网络连接(net.Conn)或内存缓冲区(bytes.Buffer)。

用于配置HTTP响应并将数据发送到客户端的http.ResponseWriter也是一个io.Writer,通过调用(不一定只调用一次)ResponseWriter.Write()(实现通用io.Writer)来组装要发送的数据(响应体)。这是您对http.ResponseWriter接口实现(关于发送主体)唯一的保证。

WriteString()

现在我们来看一下WriteString()函数。通常,我们希望将文本数据写入io.Writer中。我们可以通过将string转换为[]byte来实现这一点,例如:
w.Write([]byte("Hello"))

这段代码可以正常工作。但是这是一个非常频繁的操作,因此有一种“通常”被接受的方法,由io.StringWriter接口捕获(自Go 1.12以来可用,此前未公开):

type StringWriter interface {
    WriteString(s string) (n int, err error)
}

该方法提供了在编写代码时使用字符串值而不是[]byte的可能性。因此,如果某些实现了io.Writer接口的内容实现了该方法,则可以直接传递字符串值而无需进行[]byte转换。这似乎只是代码中的一个小简化,但它不仅如此。将字符串转换为[]byte需要复制字符串内容(因为Go中的字符串值是不可变的,请在此处阅读更多信息:golang: []byte(string) vs []byte(*string)),因此如果字符串“较大”和/或您必须执行此操作多次,则会产生一些开销。根据io.Writer的性质和实现细节,可能可以在不将其转换为[]byte的情况下编写字符串内容,从而避免上述开销。
作为一个例子,如果一个io.Writer是写入到内存缓冲区的东西(例如bytes.Buffer),它可以利用内置的copy()函数:

copy内置函数将元素从源切片复制到目标切片。(作为特殊情况,它还会将字节从字符串复制到字节切片。)

copy()可用于将string的内容(字节)复制到[]byte中,而无需将string转换为[]byte,例如:
buf := make([]byte, 100)
copy(buf, "Hello")

现在有一个“实用程序”函数io.WriteString(),它将一个string写入到一个io.Writer中。但它是通过首先检查传递的io.Writer的(动态类型)是否具有WriteString()方法来完成这个操作的,如果具有该方法,则使用该方法(其实现可能更有效)。如果传递的io.Writer没有此类方法,则将使用一般的转换为字节片并写入该片方法作为“备用”。

你可能认为这个 WriteString() 只在内存缓冲区的情况下有效,但事实并非如此。Web请求的响应也经常使用内存缓冲区进行缓存,因此在 http.ResponseWriter 的情况下,它可能会提高性能。如果你查看 http.ResponseWriter 的实现:它是未导出的类型 http.response (server.go 目前位于第308行),它确实实现了 WriteString() (目前位于第1212行),因此可以推断出它确实有所改进。

总之,每当你写入 string 值时,建议使用 io.WriteString(),因为它可能更有效率(更快)。

fmt.Fprintf()

你应该把这看作是一种方便易用的方式,可以为你想要写入的数据添加更多格式,但相对来说会牺牲一定的性能。

因此,如果你想要以简单的方式创建格式化的字符串,可以使用 fmt.Fprintf(),例如:

name := "Bob"
age := 23
fmt.Fprintf(w, "Hi, my name is %s and I'm %d years old.", name, age)

这将导致以下字符串被写入:
Hi, my name is Bob and I'm 23 years old.

有一件事情你必须记住:fmt.Fprintf()需要一个格式化字符串,因此它将被预处理而不是按原样写入输出。以下是一个快速的例子:

fmt.Fprintf(w, "100 %%")

您可能期望输出的是"100 %%"(包含2个%字符),但实际上只会发送一个,因为在格式化字符串中%是特殊字符,%%只会在输出中产生一个%

如果您只想使用fmt包写入一个string,请使用fmt.Fprint(),它不需要格式化string

fmt.Fprint(w, "Hello")

使用 fmt 包的另一个好处是你可以写入其他类型的值,而不仅仅是 string,例如:
fmt.Fprint(w, 23, time.Now())

(Of course the rules how to convert any value to a string–and to series of bytes eventually–is well defined, in the doc of the fmt package.)
对于如何将任何值转换为字符串(最终转换为一系列字节)的规则,在fmt包的文档中有明确定义。
For "simple" formatted outputs the fmt package might be OK. For complex output documents do consider using the text/template (for general text) and html/template (whenever the output is HTML).
对于“简单”格式化输出,fmt包可能足够。 对于复杂的输出文档,请考虑使用{{link1:text / template}}(用于一般文本)和{{link2:html / template}}(每当输出是HTML时)。
Passing / handing over http.ResponseWriter 为了完整起见,我们应该提到,通常您想要作为Web响应发送的内容是由支持“流式传输”结果的“某些内容”生成的。 例如,可以从结构体或映射生成JSON响应。
在这种情况下,如果它支持实时将结果写入io.Writer,则将您的http.ResponseWriter(即io.Writer)传递/移交给此something通常更有效。
一个很好的例子就是生成JSON响应。当然,你可以使用json.Marshal()将对象编组为JSON,它会返回一个字节片,你只需调用ResponseWriter.Write()即可简单地发送它。
但是,更有效的方法是让json包知道你有一个io.Writer,并且最终你想要将结果发送到该写入器中。这样就不需要先在缓冲区中生成JSON文本,然后将其写入响应并丢弃。你可以通过调用json.NewEncoder()创建一个新的json.Encoder,将http.ResponseWriter作为io.Writer传递给它,然后调用Encoder.Encode()将直接将JSON结果写入响应写入器中。
这里的一个缺点是,如果生成JSON响应失败,您可能会有部分发送/提交的响应,无法撤回。如果这对您来说是个问题,那么您除了在缓冲区中生成响应之外别无选择,如果编组成功,那么您可以一次性写入完整响应。

所以,如果我发送编组的JSON,使用“write”就足够了,因为它是字节吗? - laike9m
@laike9m 这取决于你如何进行编组。首先不要忘记正确设置响应头(内容类型)。然后,您可以使用 json.Marshal() 进行编组,然后简单地 w.Write() 结果,或者您可以创建一个新的 json.Encoder,将 ResponseWriter 包装为底层/目标写入器。这还取决于您想如何处理错误,因为首先进行编组会给您更多选项来进行操作,与在 json.Encoder 中情况相反,在这种情况下,当检测到错误时,响应可能已经被写入。 - icza
我不太理解这句话: 它还取决于您想如何处理错误,因为首先进行编组会为您提供更多的选项来继续操作,相反,如果使用json.Encoder,则在检测到错误时响应可能已经被写入。 你能解释一下或者举个例子吗? - laike9m
1
如果您使用json.Marshal(),它首先尝试对值进行编组,并将结果作为[]byte和一个error返回,如果编组失败。此时还没有写入到ResponseWriter中,因此如果出现错误,您甚至可以选择发送一个HTML错误页面(不同的内容类型)。@laike9m - icza
2
如果你使用json.Encoder并调用Encoder.Encode(),它会尝试将JSON数据写入ResponseWriter,如果出现错误则返回一个error。但是在返回错误的时候,Encode()可能已经向响应中写入了部分数据(意味着头被提交):你无法“收回”部分写入的数据,也无法在此时修改HTTP头部。如果你想在此时更改响应:那就完了。 - icza
啊,我明白了。谢谢回复。 - laike9m

5

如您可以从这里(ResponseWriter)看到,它是一个带有Write([]byte) (int, error)方法的接口。

因此,在io.WriteStringfmt.Fprintf中,两者都将Writer作为第一个参数,该接口还包装了Write(p []byte) (n int, err error)方法。

type Writer interface {
    Write(p []byte) (n int, err error)
}

当你调用io.WriteString(w,"blah")时,请在此处检查

func WriteString(w Writer, s string) (n int, err error) {
  if sw, ok := w.(stringWriter); ok {
      return sw.WriteString(s)
  }
  return w.Write([]byte(s))
}

或 fmt.Fprintf(w,"blabla") 在这里检查

func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
   p := newPrinter()
   p.doPrintf(format, a)
   n, err = w.Write(p.buf)
   p.free()
   return
}

你只是间接地调用了Write方法,因为你在两个方法中都传递了ResponseWriter变量。

所以,为什么不直接使用w.Write([]byte("blabla\n"))来调用它呢?我希望你得到了答案。

PS:如果你想将其作为JSON响应发送,还有一种不同的方法。

json.NewEncoder(w).Encode(wrapper)
//Encode take interface as an argument. Wrapper can be:
//wrapper := SuccessResponseWrapper{Success:true, Data:data}

在使用Write的示例中,为什么要在“blabla”后添加“\n”? - Victor Perov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接