diff --git a/.github/fmt/entrypoint.sh b/.github/fmt/entrypoint.sh
index 1018d5ac197942c8e8b39985efbe434daa361f41..f75e9366992792a0a77813a317461d9bc8a39918 100755
--- a/.github/fmt/entrypoint.sh
+++ b/.github/fmt/entrypoint.sh
@@ -8,7 +8,7 @@ gen() {
 	go list ./... > /dev/null
 	go mod tidy
 
-	go install github.com/golang/protobuf/protoc-gen-go
+	go install golang.org/x/tools/cmd/stringer
 	go generate ./...
 }
 
diff --git a/.github/main.workflow b/.github/main.workflow
index ebd3d575ec95580e4231f1b379730b7f65ecf803..c4947b008bd2ada0490cbfe8c5721fb5227df2a3 100644
--- a/.github/main.workflow
+++ b/.github/main.workflow
@@ -1,6 +1,10 @@
 workflow "main" {
   on = "push"
-  resolves = ["fmt", "test"]
+  resolves = ["fmt", "lint", "test"]
+}
+
+action "lint" {
+  uses = "./.github/lint"
 }
 
 action "fmt" {
diff --git a/dataframe_string.go b/dataframe_string.go
deleted file mode 100644
index 2f862722171491172dacd04ea035d51d69b87d1f..0000000000000000000000000000000000000000
--- a/dataframe_string.go
+++ /dev/null
@@ -1,17 +0,0 @@
-// Code generated by "stringer -type=DataType"; DO NOT EDIT.
-
-package ws
-
-import "strconv"
-
-const _DataFrame_name = "TextBinary"
-
-var _DataFrame_index = [...]uint8{0, 4, 10}
-
-func (i DataType) String() string {
-	i -= 1
-	if i < 0 || i >= DataType(len(_DataFrame_index)-1) {
-		return "DataType(" + strconv.FormatInt(int64(i+1), 10) + ")"
-	}
-	return _DataFrame_name[_DataFrame_index[i]:_DataFrame_index[i+1]]
-}
diff --git a/dataframe.go b/datatype.go
similarity index 91%
rename from dataframe.go
rename to datatype.go
index 251a0fa579636146339b4dd860ff363bf2aa01bc..ae62f0eabd57d94ea0877eee6660e0bd1ff8c25b 100644
--- a/dataframe.go
+++ b/datatype.go
@@ -8,6 +8,7 @@ import (
 //go:generate stringer -type=DataType
 type DataType int
 
+// DataType constants.
 const (
 	Text   DataType = DataType(wscore.OpText)
 	Binary DataType = DataType(wscore.OpBinary)
diff --git a/datatype_string.go b/datatype_string.go
new file mode 100644
index 0000000000000000000000000000000000000000..e4fdf8f7418f17f93a3b1b0e9377976b6d4e11ec
--- /dev/null
+++ b/datatype_string.go
@@ -0,0 +1,25 @@
+// Code generated by "stringer -type=DataType"; DO NOT EDIT.
+
+package ws
+
+import "strconv"
+
+func _() {
+	// An "invalid array index" compiler error signifies that the constant values have changed.
+	// Re-run the stringer command to generate them again.
+	var x [1]struct{}
+	_ = x[Text-1]
+	_ = x[Binary-2]
+}
+
+const _DataType_name = "TextBinary"
+
+var _DataType_index = [...]uint8{0, 4, 10}
+
+func (i DataType) String() string {
+	i -= 1
+	if i < 0 || i >= DataType(len(_DataType_index)-1) {
+		return "DataType(" + strconv.FormatInt(int64(i+1), 10) + ")"
+	}
+	return _DataType_name[_DataType_index[i]:_DataType_index[i+1]]
+}
diff --git a/example_test.go b/example_test.go
index 36b3cbaaa02539a83601f9f4adeffe99136f75b7..f0c2303a60113c4f62dde2a122b83338bf2ba5ad 100644
--- a/example_test.go
+++ b/example_test.go
@@ -8,11 +8,12 @@ import (
 	"time"
 
 	"golang.org/x/time/rate"
+
 	"nhooyr.io/ws"
 	"nhooyr.io/ws/wsjson"
 )
 
-func ExampleEcho() {
+func ExampleAccept_echo() {
 	fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		c, err := ws.Accept(w, r,
 			ws.AcceptSubprotocols("echo"),
@@ -85,7 +86,7 @@ func ExampleAccept() {
 		type myJsonStruct struct {
 			MyField string `json:"my_field"`
 		}
-		err = wsjson.Write(c, myJsonStruct{
+		err = wsjson.Write(r.Context(), c, myJsonStruct{
 			MyField: "foo",
 		})
 		if err != nil {
@@ -118,7 +119,7 @@ func ExampleDial() {
 	type myJsonStruct struct {
 		MyField string `json:"my_field"`
 	}
-	err = wsjson.Write(c, myJsonStruct{
+	err = wsjson.Write(ctx, c, myJsonStruct{
 		MyField: "foo",
 	})
 	if err != nil {
diff --git a/go.mod b/go.mod
index e5a34d28fce93902986d728a422a29bd8bbed89f..4749de6dca2e412095dadd31e737b9fe3425fe3b 100644
--- a/go.mod
+++ b/go.mod
@@ -6,8 +6,8 @@ require (
 	github.com/golang/protobuf v1.3.1
 	github.com/kr/pretty v0.1.0 // indirect
 	go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16
-	golang.org/x/net v0.0.0-20190311183353-d8887717615a
-	golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
-	golang.org/x/tools v0.0.0-20190315191501-e6df0c1bb376
+	golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3
+	golang.org/x/time v0.0.0-20190308202827-9d24e82272b4
+	golang.org/x/tools v0.0.0-20190329215204-73054e8977d1
 	mvdan.cc/sh v2.6.4+incompatible
 )
diff --git a/go.sum b/go.sum
index 40b640343df0734afb3bb28c6a6c21f05a7a03c8..b5d8f2fc56742d71fafbab10559ce86f24d31236 100644
--- a/go.sum
+++ b/go.sum
@@ -8,6 +8,8 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16 h1:3gGa1bM0nG7Ruhu5b7wKnoOOwAD/fJ8iyyAcpOzDG3A=
 go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3 h1:XQyxROzUlZH+WIQwySDgnISgOivlhjIEwaQaJEJrrN0=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -15,7 +17,8 @@ golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/tools v0.0.0-20190315191501-e6df0c1bb376 h1:GfNg/J4IAJguoN+DWPTZ54ElycoBxtQkpxhrbA5edVA=
-golang.org/x/tools v0.0.0-20190315191501-e6df0c1bb376/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190329215204-73054e8977d1 h1:rLRH2E2wN5JjGJSVlBe1ioUkCKgb6eoL9X8bDmtEpsk=
+golang.org/x/tools v0.0.0-20190329215204-73054e8977d1/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 mvdan.cc/sh v2.6.4+incompatible h1:eD6tDeh0pw+/TOTI1BBEryZ02rD2nMcFsgcvde7jffM=
 mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8=
diff --git a/statuscode_string.go b/statuscode_string.go
index d7ba0b553ad2b28856328fb9588657b226b7f296..2d4ab76502bc2b00f28972b75774e4f29afaac63 100644
--- a/statuscode_string.go
+++ b/statuscode_string.go
@@ -4,6 +4,27 @@ package ws
 
 import "strconv"
 
+func _() {
+	// An "invalid array index" compiler error signifies that the constant values have changed.
+	// Re-run the stringer command to generate them again.
+	var x [1]struct{}
+	_ = x[StatusNormalClosure-1000]
+	_ = x[StatusGoingAway-1001]
+	_ = x[StatusProtocolError-1002]
+	_ = x[StatusUnsupportedData-1003]
+	_ = x[StatusNoStatusRcvd-1009]
+	_ = x[StatusAbnormalClosure-1010]
+	_ = x[StatusInvalidFramePayloadData-1011]
+	_ = x[StatusPolicyViolation-1012]
+	_ = x[StatusMessageTooBig-1013]
+	_ = x[StatusMandatoryExtension-1014]
+	_ = x[StatusInternalError-1015]
+	_ = x[StatusServiceRestart-1016]
+	_ = x[StatusTryAgainLater-1017]
+	_ = x[StatusBadGateway-1018]
+	_ = x[StatusTLSHandshake-1019]
+}
+
 const (
 	_StatusCode_name_0 = "StatusNormalClosureStatusGoingAwayStatusProtocolErrorStatusUnsupportedData"
 	_StatusCode_name_1 = "StatusNoStatusRcvdStatusAbnormalClosureStatusInvalidFramePayloadDataStatusPolicyViolationStatusMessageTooBigStatusMandatoryExtensionStatusInternalErrorStatusServiceRestartStatusTryAgainLaterStatusBadGatewayStatusTLSHandshake"
diff --git a/tools.go b/tools.go
index 35194f10888f7ee080f12554a1b3d5313d1beb93..c78042b75e925dbd24dfbbeaf629ae5a06230228 100644
--- a/tools.go
+++ b/tools.go
@@ -5,6 +5,7 @@ package tools
 // See https://github.com/go-modules-by-example/index/blob/master/010_tools/README.md
 import (
 	_ "go.coder.com/go-tools/cmd/goimports"
+	_ "golang.org/x/lint/golint"
 	_ "golang.org/x/tools/cmd/stringer"
 	_ "mvdan.cc/sh/cmd/shfmt"
 )
diff --git a/ws.go b/ws.go
index f56d57323df405743cfb8bb766e0f084e0a66d47..191ccc10ce3ddfd2ce0e17d01e39226849565977 100644
--- a/ws.go
+++ b/ws.go
@@ -2,6 +2,7 @@ package ws
 
 import (
 	"context"
+	"net"
 )
 
 const (
@@ -18,6 +19,11 @@ func (c *Conn) Subprotocol() string {
 	panic("TODO")
 }
 
+// NetConn returns the net.Conn underlying the Conn.
+func (c *Conn) NetConn() net.Conn {
+	panic("TODO")
+}
+
 // MessageWriter returns a writer bounded by the context that will write
 // a WebSocket data frame of type dataType to the connection.
 // Ensure you close the MessageWriter once you have written to entire message.
diff --git a/wscore/header.go b/wscore/header.go
index 3f81df006727cddfe6cb1caa717c212217fd89dc..bcbd41eb02b2db5d0564f7cf6e80e5e6559fed50 100644
--- a/wscore/header.go
+++ b/wscore/header.go
@@ -19,10 +19,12 @@ type Header struct {
 	MaskKey [4]byte
 }
 
+// Bytes returns the bytes of the header.
 func (h Header) Bytes() []byte {
 	panic("TODO")
 }
 
-func ReaderHeader(r io.Reader) []byte {
+// ReadHeader reads a header from the reader.
+func ReadHeader(r io.Reader) []byte {
 	panic("TODO")
 }
diff --git a/wscore/opcode.go b/wscore/opcode.go
index dc36e920bab5b3e64b51d8001cc7119b976d1838..878eb80ec93a7150b34989792e3e52a7a5c9f7b2 100644
--- a/wscore/opcode.go
+++ b/wscore/opcode.go
@@ -4,6 +4,7 @@ package wscore
 //go:generate stringer -type=Opcode
 type Opcode int
 
+// Opcode constants.
 const (
 	OpContinuation Opcode = iota
 	OpText
diff --git a/wscore/opcode_string.go b/wscore/opcode_string.go
index 2bc8a7231367de0c7b36c96f2d5ae3c8fcd5feae..8ed5c1c700689582e53dee8fb6963726ad686fea 100644
--- a/wscore/opcode_string.go
+++ b/wscore/opcode_string.go
@@ -4,6 +4,18 @@ package wscore
 
 import "strconv"
 
+func _() {
+	// An "invalid array index" compiler error signifies that the constant values have changed.
+	// Re-run the stringer command to generate them again.
+	var x [1]struct{}
+	_ = x[OpContinuation-0]
+	_ = x[OpText-1]
+	_ = x[OpBinary-2]
+	_ = x[OpClose-11]
+	_ = x[OpPing-12]
+	_ = x[OpPong-13]
+}
+
 const (
 	_Opcode_name_0 = "OpContinuationOpTextOpBinary"
 	_Opcode_name_1 = "OpCloseOpPingOpPong"
diff --git a/wsjson/wsjon.go b/wsjson/wsjon.go
index 72811e8ea5bffeb2cf83cfcab74da6ec404ba5ac..33dc0154b75df711c58455f5d147e4568e1fc7ca 100644
--- a/wsjson/wsjon.go
+++ b/wsjson/wsjon.go
@@ -1,13 +1,17 @@
 package wsjson
 
 import (
+	"context"
+
 	"nhooyr.io/ws"
 )
 
-func Read(c *ws.Conn, v interface{}) error {
+// Read reads a json message from c into v.
+func Read(ctx context.Context, c *ws.Conn, v interface{}) error {
 	panic("TODO")
 }
 
-func Write(c *ws.Conn, v interface{}) error {
+// Write writes the json message v into c.
+func Write(ctx context.Context, c *ws.Conn, v interface{}) error {
 	panic("TODO")
 }
diff --git a/wspb/wspb.go b/wspb/wspb.go
index 86741f59f9947fc6a114f562dde6cfc3df239d27..9c7b1549f2dbdf3ab5d83ab7dccbf40a7f596fa7 100644
--- a/wspb/wspb.go
+++ b/wspb/wspb.go
@@ -1,15 +1,19 @@
 package wspb
 
 import (
+	"context"
+
 	"github.com/golang/protobuf/proto"
 
 	"nhooyr.io/ws"
 )
 
-func Read(c *ws.Conn, v proto.Message) error {
+// Read reads a protobuf message from c into v.
+func Read(ctx context.Context, c *ws.Conn, v proto.Message) error {
 	panic("TODO")
 }
 
-func Write(c *ws.Conn, v proto.Message) error {
+// Write writes the protobuf message into c.
+func Write(ctx context.Context, c *ws.Conn, v proto.Message) error {
 	panic("TODO")
 }