http: add Header.Write method

R=golang-dev, bradfitz, dsymonds
CC=golang-dev
https://golang.org/cl/4426069
diff --git a/src/pkg/http/cgi/child.go b/src/pkg/http/cgi/child.go
index c7d48b9..e8d847d 100644
--- a/src/pkg/http/cgi/child.go
+++ b/src/pkg/http/cgi/child.go
@@ -168,17 +168,8 @@
 		r.header.Add("Content-Type", "text/html; charset=utf-8")
 	}
 
-	// TODO: add a method on http.Header to write itself to an io.Writer?
-	// This is duplicated code.
-	for k, vv := range r.header {
-		for _, v := range vv {
-			v = strings.Replace(v, "\n", "", -1)
-			v = strings.Replace(v, "\r", "", -1)
-			v = strings.TrimSpace(v)
-			fmt.Fprintf(r.bufw, "%s: %s\r\n", k, v)
-		}
-	}
-	r.bufw.Write([]byte("\r\n"))
+	r.header.Write(r.bufw)
+	r.bufw.WriteString("\r\n")
 	r.bufw.Flush()
 }
 
diff --git a/src/pkg/http/fcgi/child.go b/src/pkg/http/fcgi/child.go
index 114052b..5e5f2e2 100644
--- a/src/pkg/http/fcgi/child.go
+++ b/src/pkg/http/fcgi/child.go
@@ -169,15 +169,7 @@
 	}
 
 	fmt.Fprintf(r.w, "Status: %d %s\r\n", code, http.StatusText(code))
-	// TODO(eds): this is duplicated in http and http/cgi
-	for k, vv := range r.header {
-		for _, v := range vv {
-			v = strings.Replace(v, "\n", "", -1)
-			v = strings.Replace(v, "\r", "", -1)
-			v = strings.TrimSpace(v)
-			fmt.Fprintf(r.w, "%s: %s\r\n", k, v)
-		}
-	}
+	r.header.Write(r.w)
 	r.w.WriteString("\r\n")
 }
 
diff --git a/src/pkg/http/header.go b/src/pkg/http/header.go
index 95b0f3d..95140b0 100644
--- a/src/pkg/http/header.go
+++ b/src/pkg/http/header.go
@@ -4,7 +4,14 @@
 
 package http
 
-import "net/textproto"
+import (
+	"fmt"
+	"io"
+	"net/textproto"
+	"os"
+	"sort"
+	"strings"
+)
 
 // A Header represents the key-value pairs in an HTTP header.
 type Header map[string][]string
@@ -35,6 +42,37 @@
 	textproto.MIMEHeader(h).Del(key)
 }
 
+// Write writes a header in wire format.
+func (h Header) Write(w io.Writer) os.Error {
+	return h.WriteSubset(w, nil)
+}
+
+// WriteSubset writes a header in wire format.
+// If exclude is not nil, keys where exclude[key] == true are not written.
+func (h Header) WriteSubset(w io.Writer, exclude map[string]bool) os.Error {
+	keys := make([]string, 0, len(h))
+	for k := range h {
+		if exclude == nil || !exclude[k] {
+			keys = append(keys, k)
+		}
+	}
+	sort.SortStrings(keys)
+	for _, k := range keys {
+		for _, v := range h[k] {
+			v = strings.Replace(v, "\n", " ", -1)
+			v = strings.Replace(v, "\r", " ", -1)
+			v = strings.TrimSpace(v)
+			if v == "" {
+				continue
+			}
+			if _, err := fmt.Fprintf(w, "%s: %s\r\n", k, v); err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}
+
 // CanonicalHeaderKey returns the canonical format of the
 // header key s.  The canonicalization converts the first
 // letter and any letter following a hyphen to upper case;
diff --git a/src/pkg/http/header_test.go b/src/pkg/http/header_test.go
new file mode 100644
index 0000000..7e24cb0
--- /dev/null
+++ b/src/pkg/http/header_test.go
@@ -0,0 +1,71 @@
+// Copyright 2011 The Go Authors.  All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package http
+
+import (
+	"bytes"
+	"testing"
+)
+
+var headerWriteTests = []struct {
+	h        Header
+	exclude  map[string]bool
+	expected string
+}{
+	{Header{}, nil, ""},
+	{
+		Header{
+			"Content-Type":   {"text/html; charset=UTF-8"},
+			"Content-Length": {"0"},
+		},
+		nil,
+		"Content-Length: 0\r\nContent-Type: text/html; charset=UTF-8\r\n",
+	},
+	{
+		Header{
+			"Content-Length": {"0", "1", "2"},
+		},
+		nil,
+		"Content-Length: 0\r\nContent-Length: 1\r\nContent-Length: 2\r\n",
+	},
+	{
+		Header{
+			"Expires":          {"-1"},
+			"Content-Length":   {"0"},
+			"Content-Encoding": {"gzip"},
+		},
+		map[string]bool{"Content-Length": true},
+		"Content-Encoding: gzip\r\nExpires: -1\r\n",
+	},
+	{
+		Header{
+			"Expires":          {"-1"},
+			"Content-Length":   {"0", "1", "2"},
+			"Content-Encoding": {"gzip"},
+		},
+		map[string]bool{"Content-Length": true},
+		"Content-Encoding: gzip\r\nExpires: -1\r\n",
+	},
+	{
+		Header{
+			"Expires":          {"-1"},
+			"Content-Length":   {"0"},
+			"Content-Encoding": {"gzip"},
+		},
+		map[string]bool{"Content-Length": true, "Expires": true, "Content-Encoding": true},
+		"",
+	},
+}
+
+func TestHeaderWrite(t *testing.T) {
+	var buf bytes.Buffer
+	for i, test := range headerWriteTests {
+		test.h.WriteSubset(&buf, test.exclude)
+		if buf.String() != test.expected {
+			t.Errorf("#%d:\n got: %q\nwant: %q", i, buf.String(), test.expected)
+		}
+		buf.Reset()
+	}
+}
diff --git a/src/pkg/http/request.go b/src/pkg/http/request.go
index b8e9a21..70440d9 100644
--- a/src/pkg/http/request.go
+++ b/src/pkg/http/request.go
@@ -300,7 +300,7 @@
 	// from Request, and introduce Request methods along the lines of
 	// Response.{GetHeader,AddHeader} and string constants for "Host",
 	// "User-Agent" and "Referer".
-	err = writeSortedHeader(w, req.Header, reqExcludeHeader)
+	err = req.Header.WriteSubset(w, reqExcludeHeader)
 	if err != nil {
 		return err
 	}
diff --git a/src/pkg/http/response.go b/src/pkg/http/response.go
index 1f725ec..a65c2b1 100644
--- a/src/pkg/http/response.go
+++ b/src/pkg/http/response.go
@@ -8,11 +8,9 @@
 
 import (
 	"bufio"
-	"fmt"
 	"io"
 	"net/textproto"
 	"os"
-	"sort"
 	"strconv"
 	"strings"
 )
@@ -192,7 +190,7 @@
 	}
 
 	// Rest of header
-	err = writeSortedHeader(w, resp.Header, respExcludeHeader)
+	err = resp.Header.WriteSubset(w, respExcludeHeader)
 	if err != nil {
 		return err
 	}
@@ -213,27 +211,3 @@
 	// Success
 	return nil
 }
-
-func writeSortedHeader(w io.Writer, h Header, exclude map[string]bool) os.Error {
-	keys := make([]string, 0, len(h))
-	for k := range h {
-		if exclude == nil || !exclude[k] {
-			keys = append(keys, k)
-		}
-	}
-	sort.SortStrings(keys)
-	for _, k := range keys {
-		for _, v := range h[k] {
-			v = strings.Replace(v, "\n", " ", -1)
-			v = strings.Replace(v, "\r", " ", -1)
-			v = strings.TrimSpace(v)
-			if v == "" {
-				continue
-			}
-			if _, err := fmt.Fprintf(w, "%s: %s\r\n", k, v); err != nil {
-				return err
-			}
-		}
-	}
-	return nil
-}
diff --git a/src/pkg/http/server.go b/src/pkg/http/server.go
index 96d2cb6..d155f06 100644
--- a/src/pkg/http/server.go
+++ b/src/pkg/http/server.go
@@ -309,7 +309,7 @@
 		text = "status code " + codestring
 	}
 	io.WriteString(w.conn.buf, proto+" "+codestring+" "+text+"\r\n")
-	writeSortedHeader(w.conn.buf, w.header, nil)
+	w.header.Write(w.conn.buf)
 	io.WriteString(w.conn.buf, "\r\n")
 }