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