Created
May 16, 2018 19:16
-
-
Save peterhellberg/e36274f213f7a2e2b89a3d837fbafbe1 to your computer and use it in GitHub Desktop.
A pretty minimal HTTP server example in Go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"io/ioutil" | |
"log" | |
"net/http" | |
"os" | |
"time" | |
) | |
func main() { | |
logger := log.New(os.Stdout, "", 0) | |
hs := setup(logger) | |
logger.Printf("Listening on http://0.0.0.0%s\n", hs.Addr) | |
hs.ListenAndServe() | |
} | |
func setup(logger *log.Logger) *http.Server { | |
return &http.Server{ | |
Addr: getAddr(), | |
Handler: newServer(logWith(logger)), | |
ReadTimeout: 5 * time.Second, | |
WriteTimeout: 10 * time.Second, | |
IdleTimeout: 60 * time.Second, | |
} | |
} | |
func getAddr() string { | |
if port := os.Getenv("PORT"); port != "" { | |
return ":" + port | |
} | |
return ":8383" | |
} | |
func newServer(options ...Option) *Server { | |
s := &Server{logger: log.New(ioutil.Discard, "", 0)} | |
for _, o := range options { | |
o(s) | |
} | |
s.mux = http.NewServeMux() | |
s.mux.HandleFunc("/", s.index) | |
return s | |
} | |
type Option func(*Server) | |
func logWith(logger *log.Logger) Option { | |
return func(s *Server) { | |
s.logger = logger | |
} | |
} | |
type Server struct { | |
mux *http.ServeMux | |
logger *log.Logger | |
} | |
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |
if r.Method != "GET" { | |
w.WriteHeader(http.StatusMethodNotAllowed) | |
return | |
} | |
s.log("%s %s", r.Method, r.URL.Path) | |
s.mux.ServeHTTP(w, r) | |
} | |
func (s *Server) log(format string, v ...interface{}) { | |
s.logger.Printf(format+"\n", v...) | |
} | |
func (s *Server) index(w http.ResponseWriter, r *http.Request) { | |
w.Write([]byte("Hello, world!")) | |
} |
Example with a domain package and a service: https://play.golang.org/p/Yft7Ftg-nFL
package main
import (
"fmt"
"log"
"net/http"
"net/http/httptest"
"os"
"play.ground/api"
"play.ground/app"
"play.ground/services/userservice"
)
func main() {
logger := log.New(os.Stdout, "", 0)
us := userservice.New(
&app.User{ID: "1", Name: "Peter Hellberg"},
&app.User{ID: "2", Name: "Sumukha Pk"},
)
h := api.NewHandler(us, api.LogWith(logger))
// Following just for the playground example (in the real code you’d call ListenAndServe)
getRequest := func(handler http.Handler, path string) {
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, path, nil)
handler.ServeHTTP(w, r)
fmt.Println(w.Body.String())
}
getRequest(h, "/")
getRequest(h, "/users")
getRequest(h, "/users/1")
getRequest(h, "/users/3")
}
-- go.mod --
module play.ground
-- api/handler.go --
package api
import (
"encoding/json"
"net/http"
"strings"
"play.ground/app"
)
type Option func(*Handler)
func LogWith(logger app.Logger) Option {
return func(h *Handler) {
h.logger = logger
}
}
type Handler struct {
app.UserService
logger app.Logger
mux *http.ServeMux
}
func NewHandler(us app.UserService, options ...Option) *Handler {
h := &Handler{UserService: us}
for _, o := range options {
o(h)
}
h.mux = http.NewServeMux()
h.mux.HandleFunc("/", h.index)
h.mux.HandleFunc("/users", h.users)
h.mux.HandleFunc("/users/", h.user)
return h
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.log("%s %s", r.Method, r.URL.Path)
h.mux.ServeHTTP(w, r)
}
func (h *Handler) log(format string, v ...interface{}) {
if h.logger != nil {
h.logger.Printf(format+"\n", v...)
}
}
func (h *Handler) index(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
w.Write([]byte("Hello, world!\n"))
}
func (h *Handler) users(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(h.AllUsers())
}
func (h *Handler) user(w http.ResponseWriter, r *http.Request) {
id := strings.TrimPrefix(r.URL.Path, "/users/")
u, err := h.GetUser(id)
if err != nil {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(u)
}
-- app/app.go --
package app
import "fmt"
var ErrUserNotFound = fmt.Errorf("user not found")
type Logger interface {
Printf(format string, v ...interface{})
}
type User struct {
ID string
Name string
}
type UserService interface {
AllUsers() []*User
GetUser(id string) (*User, error)
}
-- services/userservice/userservice.go --
package userservice
import "play.ground/app"
type Service struct {
data map[string]*app.User
}
func New(users ...*app.User) *Service {
s := &Service{data: map[string]*app.User{}}
for _, u := range users {
s.data[u.ID] = u
}
return s
}
func (s *Service) AllUsers() []*app.User {
users := []*app.User{}
for _, user := range s.data {
users = append(users, user)
}
return users
}
func (s *Service) GetUser(id string) (*app.User, error) {
if u, ok := s.data[id]; ok {
return u, nil
}
return nil, app.ErrUserNotFound
}
Relying on the Enhanced routing patterns in 1.22
(h.mux.HandleFunc("GET /users/{id}", h.user)
, r.PathValue("id")
, etc.)
Playground https://go.dev/play/p/Rh2remscvb8
package main
import (
"fmt"
"log"
"net/http"
"net/http/httptest"
"os"
"play.ground/api"
"play.ground/app"
"play.ground/services/userservice"
)
func main() {
logger := log.New(os.Stdout, "", 0)
us := userservice.New(
&app.User{ID: "1", Name: "Peter Hellberg"},
&app.User{ID: "2", Name: "David Nguyen"},
)
h := api.NewHandler(us, api.LogWith(logger))
// Following just for the playground example (in the real code you’d call ListenAndServe)
request := func(handler http.Handler, method, path string) {
w := httptest.NewRecorder()
r := httptest.NewRequest(method, path, nil)
handler.ServeHTTP(w, r)
fmt.Println(w.Body.String())
}
request(h, "GET", "/")
request(h, "POST", "/")
request(h, "GET", "/users")
request(h, "GET", "/users/1")
request(h, "GET", "/users/3")
request(h, "PUT", "/users/1")
}
-- go.mod --
module play.ground
-- api/handler.go --
package api
import (
"encoding/json"
"net/http"
"play.ground/app"
)
type Option func(*Handler)
func LogWith(logger app.Logger) Option {
return func(h *Handler) {
h.logger = logger
}
}
type Handler struct {
app.UserService
logger app.Logger
mux *http.ServeMux
}
func NewHandler(us app.UserService, options ...Option) *Handler {
h := &Handler{UserService: us}
for _, o := range options {
o(h)
}
h.mux = http.NewServeMux()
h.mux.HandleFunc("GET /", h.index)
h.mux.HandleFunc("GET /users", h.users)
h.mux.HandleFunc("GET /users/{id}", h.user)
return h
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.log("%s %s", r.Method, r.URL.Path)
h.mux.ServeHTTP(w, r)
}
func (h *Handler) log(format string, v ...interface{}) {
if h.logger != nil {
h.logger.Printf(format+"\n", v...)
}
}
func (h *Handler) index(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, world!\n"))
}
func (h *Handler) users(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(h.AllUsers())
}
func (h *Handler) user(w http.ResponseWriter, r *http.Request) {
u, err := h.GetUser(r.PathValue("id"))
if err != nil {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(u)
}
-- app/app.go --
package app
import "fmt"
var ErrUserNotFound = fmt.Errorf("user not found")
type Logger interface {
Printf(format string, v ...interface{})
}
type User struct {
ID string
Name string
}
type UserService interface {
AllUsers() []*User
GetUser(id string) (*User, error)
}
-- services/userservice/userservice.go --
package userservice
import "play.ground/app"
type Service struct {
data map[string]*app.User
}
func New(users ...*app.User) *Service {
s := &Service{data: map[string]*app.User{}}
for _, u := range users {
s.data[u.ID] = u
}
return s
}
func (s *Service) AllUsers() []*app.User {
users := []*app.User{}
for _, user := range s.data {
users = append(users, user)
}
return users
}
func (s *Service) GetUser(id string) (*app.User, error) {
if u, ok := s.data[id]; ok {
return u, nil
}
return nil, app.ErrUserNotFound
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Example with more than one package: https://play.golang.org/p/ukMvKCQE4kh