Skip to content

Instantly share code, notes, and snippets.

@peterhellberg
Created May 16, 2018 19:16
Show Gist options
  • Save peterhellberg/e36274f213f7a2e2b89a3d837fbafbe1 to your computer and use it in GitHub Desktop.
Save peterhellberg/e36274f213f7a2e2b89a3d837fbafbe1 to your computer and use it in GitHub Desktop.
A pretty minimal HTTP server example in Go
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!"))
}
@peterhellberg
Copy link
Author

peterhellberg commented Jun 26, 2020

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
}

@peterhellberg
Copy link
Author

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