go实现的本地微型单体web系统
post /book 创建一个新图书条目
post /book/ 更新一个图书条目
get /book/ 返回特定图书条目信息
get /book 返回所有图书条目信息
delete /book/ 删除特定图书条目
项目结构
go get github.com/gorilla/mux
goprojects/bookstore/store/store.go
package store
import "errors"
var (
ErrNotFound = errors.New("not found")
ErrExist = errors.New("exist")
)
type Book struct {
Id string `json:"id"` // 图书ISBN ID
Name string `json:"name"` // 图书名称
Authors []string `json:"authors"` // 图书作者
Press string `json:"press"` // 出版社
}
type Store interface {
Create(*Book) error // 创建图书
Update(*Book) error // 更新图书
Get(string) (Book, error) // 获取图书
GetAll() ([]Book, error) // 获取所有图书
Delete(string) error // 删除图书
}
goprojects/bookstore/store/factory/factory.go
package factory
/*
工厂模式
*/
import (
"bookstore/store"
"fmt"
"sync"
)
var (
providersMu sync.RWMutex
providers = make(map[string]store.Store)
)
func Register(name string, p store.Store) {
providersMu.Lock()
defer providersMu.Unlock()
if p == nil {
panic("store: Register provider is nil")
}
if _, dup := providers[name]; dup {
panic("store: Register called twice for provider" + name)
}
providers[name] = p
}
func New(providerName string) (store.Store, error) {
providersMu.RLock()
p, ok := providers[providerName]
providersMu.RUnlock()
if !ok {
return nil, fmt.Errorf("store: unknown provider %s", providerName)
}
return p, nil
}
goprojects/bookstore/internal/store/memstore.go
package store
import (
"bookstore/store"
"bookstore/store/factory"
"sync"
)
type MemStore struct {
sync.RWMutex
books map[string]*store.Book
}
func init() {
factory.Register("mem", &MemStore{
books: make(map[string]*store.Book),
})
}
func (ms *MemStore) Create(book *store.Book) error {
ms.Lock()
defer ms.Unlock()
if _, ok := ms.books[book.Id]; ok {
return store.ErrExist
}
nbook := *book
ms.books[book.Id] = &nbook
return nil
}
func (ms *MemStore) Update(book *store.Book) error {
ms.Lock()
defer ms.Unlock()
oldBook, ok := ms.books[book.Id]
if !ok {
return store.ErrNotFound
}
nbook := *oldBook
if book.Name != "" {
nbook.Name = book.Name
}
if book.Authors != nil {
nbook.Authors = book.Authors
}
if book.Press != "" {
nbook.Press = book.Press
}
ms.books[book.Id] = &nbook
return nil
}
func (ms *MemStore) Get(id string) (store.Book, error) {
ms.RLock()
defer ms.RUnlock()
t, ok := ms.books[id]
if ok {
return *t, nil
}
return store.Book{}, store.ErrNotFound
}
func (ms *MemStore) GetAll() ([]store.Book, error) {
ms.RLock()
defer ms.RUnlock()
allbooks := make([]store.Book, 0, len(ms.books))
for _, book := range ms.books {
allbooks = append(allbooks, *book)
}
return allbooks, nil
}
func (ms *MemStore) Delete(id string) error {
ms.Lock()
defer ms.Unlock()
if _, ok := ms.books[id]; !ok {
return store.ErrNotFound
}
delete(ms.books, id)
return nil
}
goprojects/bookstore/server/server.go
package server
import (
"bookstore/server/middleware"
"bookstore/store"
"context"
"encoding/json"
"github.com/gorilla/mux"
"net/http"
"time"
)
type BookStoreServer struct {
s store.Store
srv *http.Server
}
func NewBookStoreServer(addr string, s store.Store) *BookStoreServer {
srv := &BookStoreServer{
s: s,
srv: &http.Server{
Addr: addr,
},
}
router := mux.NewRouter()
router.HandleFunc("/book", srv.createBookHandler).Methods("POST")
router.HandleFunc("/book/{id}", srv.updateBookHandler).Methods("POST")
router.HandleFunc("/book/{id}", srv.getBookHandler).Methods("GET")
router.HandleFunc("/book", srv.getAllBooksHandler).Methods("GET")
router.HandleFunc("/book/{id}", srv.delBookHandler).Methods("DELETE")
srv.srv.Handler = middleware.Logging(middleware.Validating(router))
return srv
}
func (bs *BookStoreServer) ListenAndServe() (<-chan error, error) {
var err error
errChan := make(chan error)
go func() {
err = bs.srv.ListenAndServe()
errChan <- err
}()
select {
case err = <-errChan:
return nil, err
case <-time.After(time.Second):
return errChan, nil
}
}
func (bs *BookStoreServer) Shutdown(ctx context.Context) error {
return bs.srv.Shutdown(ctx)
}
func (bs *BookStoreServer) createBookHandler(w http.ResponseWriter, req *http.Request) {
dec := json.NewDecoder(req.Body)
var book store.Book
if err := dec.Decode(&book); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := bs.s.Create(&book); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
}
func (bs *BookStoreServer) updateBookHandler(w http.ResponseWriter, req *http.Request) {
id, ok := mux.Vars(req)["id"]
if !ok {
http.Error(w, "no id found in request", http.StatusBadRequest)
return
}
dec := json.NewDecoder(req.Body)
var book store.Book
if err := dec.Decode(&book); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
book.Id = id
if err := bs.s.Update(&book); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
}
func (bs *BookStoreServer) getBookHandler(w http.ResponseWriter, req *http.Request) {
id, ok := mux.Vars(req)["id"]
if !ok {
http.Error(w, "no id found in request", http.StatusBadRequest)
return
}
book, err := bs.s.Get(id)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
response(w, book)
}
func (bs *BookStoreServer) getAllBooksHandler(w http.ResponseWriter, req *http.Request) {
books, err := bs.s.GetAll()
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
response(w, books)
}
func (bs *BookStoreServer) delBookHandler(w http.ResponseWriter, req *http.Request) {
id, ok := mux.Vars(req)["id"]
if !ok {
http.Error(w, "no id found in request", http.StatusBadRequest)
return
}
err := bs.s.Delete(id)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
}
func response(w http.ResponseWriter, v interface{}) {
data, err := json.Marshal(v)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(data)
}
goprojects/bookstore/server/middleware/middleware.go
package middleware
import (
"log"
"mime"
"net/http"
)
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("recv a %s request from %s", r.Method, r.RemoteAddr)
next.ServeHTTP(w, r)
})
}
func Validating(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
contentType := r.Header.Get("Content-Type")
mediaType, _, err := mime.ParseMediaType(contentType)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if mediaType != "application/json" {
http.Error(w, "invalid Content-Type", http.StatusUnsupportedMediaType)
return
}
next.ServeHTTP(w, r)
})
}
goprojects/bookstore/cmd/bookstore/main.go
package main
import (
_ "bookstore/internal/store"
"bookstore/server"
"bookstore/store/factory"
"context"
"log"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
s, err := factory.New("mem")
if err != nil {
panic(err)
}
srv := server.NewBookStoreServer(":8080", s)
errChan, err := srv.ListenAndServe()
if err != nil {
log.Println("web server start failed:", err)
return
}
log.Println("web server start ok")
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
select {
case err = <-errChan:
log.Println("web server run failed:", err)
return
case <-c:
log.Println("bookstore program is exiting...")
ctx, cf := context.WithTimeout(context.Background(), time.Second)
defer cf()
err = srv.Shutdown(ctx)
}
if err != nil {
log.Println("bookstore program exit error:", err)
return
}
log.Println("bookstore program exit ok")
}
all:
go build bookstore/cmd/bookstore
go run main.go
访问: localhost:8080