Какая разница между ResponseWriter.Write и io.WriteString?

Я видел три способа записи контента в 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")

Какая разница между ними? Какой из них предпочтительнее использовать?

Ответ 1

io.Writer

Выходной поток представляет цель, в которую вы можете записать последовательность байтов. В Go это фиксируется общим интерфейсом io.Writer:

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

Все, что имеет этот единственный метод Write() может быть использовано в качестве вывода, например, файл на вашем диске ( os.File), сетевое соединение ( net.Conn) или буфер в памяти ( bytes.Buffer).

http.ResponseWriter который используется для настройки HTTP-ответа и отправки данных клиенту, также является io.Writer, данные, которые вы хотите отправить (тело ответа), собираются путем вызова (не обязательно только один раз) ResponseWriter.Write() (для реализации общего io.Writer). Это единственная гарантия, которую вы имеете о реализации интерфейса http.ResponseWriter (относительно отправки тела).

WriteString()

Теперь WriteString() к WriteString(). Часто мы хотим записать текстовые данные в io.Writer. Да, мы можем сделать это, просто преобразовав string в []byte, например

w.Write([]byte("Hello"))

который работает как ожидалось. Однако это очень частая операция, и поэтому существует "общепринятый" метод для этого, io.StringWriter интерфейсом io.StringWriter (доступен начиная с Go 1.12, до этого он не экспортировался):

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

Этот метод дает возможность записать string значение вместо []byte. Поэтому, если что-то (которое также реализует io.Writer) реализует этот метод, вы можете просто передать string значения без преобразования []byte. Это кажется небольшим упрощением в коде, но это больше, чем это. Преобразование string в []byte должно сделать копию содержимого string (поскольку string значения неизменны в Go, подробнее об этом здесь: golang: [] byte (string) vs [] byte (* string)), поэтому есть некоторые накладные расходы, которые становятся заметными, если string "больше" и/или вам приходится делать это много раз.

В зависимости от характера и подробностей реализации io.Writer, может быть возможно записать содержимое string без преобразования его в []byte и, таким образом, избежать упомянутых выше издержек.

Например, если io.Writer - это что-то, что пишет в буфер в памяти (например, bytes.Buffer), он может использовать встроенную функцию copy():

Встроенная функция копирования копирует элементы из исходного слайса в целевой слайс. (В особом случае он также будет копировать байты из строки в секцию байтов.)

Функция copy() может использоваться для копирования содержимого (байтов) string в []byte без преобразования string в []byte, например:

buf := make([]byte, 100)
copy(buf, "Hello")

Теперь есть "служебная" функция io.WriteString() которая записывает string в io.Writer. Но он делает это, сначала проверяя, передан ли (динамический тип) io.Writer метод WriteString(), и если да, то он будет использоваться (реализация которого, вероятно, более эффективна). Если переданный io.Writer не имеет такого метода, тогда общий метод convert-to-byte-slice-and-write-that будет использоваться как "запасной вариант".

Вы можете подумать, что этот WriteString() будет преобладать только в случае буферов в памяти, но это не так. Ответы веб-запросов также часто буферизуются (с использованием буфера в памяти), поэтому это может улучшить производительность и в случае http.ResponseWriter. И если вы посмотрите на реализацию http.ResponseWriter: это не http.response тип http.response ( server.go настоящее время строка # 308), который действительно реализует WriteString() (в настоящее время строка # 1212), так что это подразумевает улучшение.

В общем, всякий раз, когда вы пишете string значения, рекомендуется использовать io.WriteString() как это может быть более эффективным (быстрее).

fmt.Fprintf()

Вы должны смотреть на это как на удобный и простой способ добавить больше форматирования к данным, которые вы хотите записать, в обмен на то, чтобы быть несколько менее производительным.

Поэтому используйте fmt.Fprintf() если вы хотите, чтобы форматированная string создавалась простым способом, например:

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

Что приведет к записи следующей string:

Hi, my name is Bob and I'm 23 years old.

Одна вещь, которую вы не должны забывать: fmt.Fprintf() ожидает строку формата, поэтому она будет предварительно обработана и не будет записана как есть на выходе. В качестве быстрого примера:

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

Вы ожидаете, что "100 %%" будет записано в вывод (с 2 % символов), но будет отправлен только один, так как в строке формата % это специальный символ, а %% приведет только к одному % в выход.

Если вы просто хотите написать string используя пакет fmt, используйте fmt.Fprint() которая не требует string формата:

fmt.Fprint(w, "Hello")

Другое преимущество использования пакета fmt заключается в том, что вы можете записывать значения и других типов, а не только string s, например

fmt.Fprint(w, 23, time.Now())

(Конечно, правила преобразования любого значения в string - и в конце концов в серию байтов - хорошо определены в документе пакета fmt.)

Для "простых" форматированных выходных данных пакет fmt может быть в порядке. Для сложных выходных документов рекомендуется использовать text/template (для общего текста) и html/template (всякий раз, когда вывод представляет собой HTML).

Передача/передача http.ResponseWriter

Для полноты следует отметить, что часто контент, который вы хотите отправить в виде веб-ответа, генерируется "чем-то", поддерживающим "потоковую передачу" результата. Примером может быть ответ JSON, который генерируется из структуры или карты.

В таких случаях часто более эффективно передавать/передавать ваш http.ResponseWriter который является io.Writer для этого, если он поддерживает запись результата в io.Writer на лету.

Хорошим примером этого является генерация ответов JSON. Конечно, вы могли бы json.Marshal() объект в JSON с помощью json.Marshal(), который возвращает вам фрагмент байта, который вы можете просто отправить, вызвав ResponseWriter.Write().

Однако более эффективно сообщить пакету json, что у вас есть io.Writer, и в конечном итоге вы захотите отправить результат на него. Таким образом, нет необходимости сначала генерировать текст JSON в буфере, который вы просто записываете в свой ответ, а затем отбрасываете. Вы можете создать новый json.Encoder, вызвав json.NewEncoder() в который вы можете передать свой http.ResponseWriter как io.Writer, и вызвав Encoder.Encode() после этого, непосредственно Encoder.Encode() результат JSON в ваш Encoder.Encode() записи ответов.

Одним из недостатков здесь является то, что в случае сбоя при генерации ответа JSON вы можете получить частично отправленный/подтвержденный ответ, который вы не сможете вернуть. Если это проблема для вас, у вас нет другого выбора, кроме генерации ответа в буфере, и если маршалинг завершился успешно, вы можете написать полный ответ сразу.

Ответ 2

Как вы можете видеть здесь (ResponseWriter), это интерфейс с методом Write([]byte) (int, error).

Таким образом, в io.WriteString и fmt.Fprintf оба принимают Writer в качестве 1-го аргумента, который также является интерфейсом переноса метода Write(p []byte) (n int, err error)

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

Поэтому, когда вы вызываете io.WriteString(w, "бла"), проверьте здесь

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
}

вы просто вызываете метод записи косвенно, так как передаете переменную 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}