Rename module from template
This commit is contained in:
parent
221afdb9b2
commit
e75a4d4607
8 changed files with 480 additions and 0 deletions
10
polyculeconnect/Makefile
Normal file
10
polyculeconnect/Makefile
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
.PHONY: build test
|
||||||
|
|
||||||
|
build:
|
||||||
|
go build -o build/
|
||||||
|
|
||||||
|
test:
|
||||||
|
go test -v ./...
|
||||||
|
|
||||||
|
run: build
|
||||||
|
./build/template
|
109
polyculeconnect/config/config.go
Normal file
109
polyculeconnect/config/config.go
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ListeningMode int64
|
||||||
|
|
||||||
|
func (lm ListeningMode) String() string {
|
||||||
|
mapping := map[ListeningMode]string{
|
||||||
|
ModeNet: "net",
|
||||||
|
ModeUnix: "unix",
|
||||||
|
}
|
||||||
|
val := mapping[lm]
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
func listeningModeFromString(rawVal string) (ListeningMode, error) {
|
||||||
|
mapping := map[string]ListeningMode{
|
||||||
|
"unix": ModeUnix,
|
||||||
|
"net": ModeNet,
|
||||||
|
}
|
||||||
|
if typedVal, ok := mapping[rawVal]; !ok {
|
||||||
|
return ModeNet, fmt.Errorf("invalid listening mode %s", rawVal)
|
||||||
|
} else {
|
||||||
|
return typedVal, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
ModeUnix ListeningMode = iota
|
||||||
|
ModeNet
|
||||||
|
)
|
||||||
|
|
||||||
|
type jsonConf struct {
|
||||||
|
Log struct {
|
||||||
|
Level string `json:"level"`
|
||||||
|
} `json:"log"`
|
||||||
|
Server struct {
|
||||||
|
Host string `json:"host"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
SockPath string `json:"sock"`
|
||||||
|
} `json:"server"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppConfig struct {
|
||||||
|
LogLevel logrus.Level
|
||||||
|
ServerMode ListeningMode
|
||||||
|
Host string
|
||||||
|
Port int
|
||||||
|
SockPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLevel(lvlStr string) logrus.Level {
|
||||||
|
for _, lvl := range logrus.AllLevels {
|
||||||
|
if lvl.String() == lvlStr {
|
||||||
|
return lvl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return logrus.InfoLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ac *AppConfig) UnmarshalJSON(data []byte) error {
|
||||||
|
var jsonConf jsonConf
|
||||||
|
if err := json.Unmarshal(data, &jsonConf); err != nil {
|
||||||
|
return fmt.Errorf("failed to read JSON: %w", err)
|
||||||
|
}
|
||||||
|
ac.LogLevel = parseLevel(jsonConf.Log.Level)
|
||||||
|
|
||||||
|
lm, err := listeningModeFromString(jsonConf.Server.Mode)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse server listening mode: %w", err)
|
||||||
|
}
|
||||||
|
ac.ServerMode = lm
|
||||||
|
ac.SockPath = jsonConf.Server.SockPath
|
||||||
|
ac.Host = jsonConf.Server.Host
|
||||||
|
ac.Port = jsonConf.Server.Port
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultConfig AppConfig = AppConfig{
|
||||||
|
LogLevel: logrus.InfoLevel,
|
||||||
|
ServerMode: ModeNet,
|
||||||
|
Host: "0.0.0.0",
|
||||||
|
Port: 5000,
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(filepath string) (*AppConfig, error) {
|
||||||
|
content, err := os.ReadFile(filepath)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
|
conf := defaultConfig
|
||||||
|
return &conf, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to read config file %q: %w", filepath, err)
|
||||||
|
}
|
||||||
|
var conf AppConfig
|
||||||
|
if err := json.Unmarshal(content, &conf); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse config file: %w", err)
|
||||||
|
}
|
||||||
|
return &conf, nil
|
||||||
|
}
|
137
polyculeconnect/config/config_test.go
Normal file
137
polyculeconnect/config/config_test.go
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestListeningModeString(t *testing.T) {
|
||||||
|
assert.Equal(t, "net", ModeNet.String(), "Unexpected string value")
|
||||||
|
assert.Equal(t, "unix", ModeUnix.String(), "Unexpected string value")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test returning a default config when providing a path that does not exist
|
||||||
|
func TestDefault(t *testing.T) {
|
||||||
|
conf, err := New("/this/path/does/not/exist")
|
||||||
|
if assert.Nil(t, err, "Unexpected error") {
|
||||||
|
assert.Equal(t, defaultConfig, *conf, "Unexpected config")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test creating a valid config (net mode)
|
||||||
|
func TestOKNet(t *testing.T) {
|
||||||
|
tmpPath := t.TempDir()
|
||||||
|
content := `{
|
||||||
|
"log": {
|
||||||
|
"level": "error"
|
||||||
|
},
|
||||||
|
"server": {
|
||||||
|
"mode": "net",
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 8888
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
confPath := path.Join(tmpPath, "config.json")
|
||||||
|
require.Nil(t, os.WriteFile(confPath, []byte(content), 0o644), "Failed to write config")
|
||||||
|
|
||||||
|
expectedConf := AppConfig{
|
||||||
|
LogLevel: logrus.ErrorLevel,
|
||||||
|
ServerMode: ModeNet,
|
||||||
|
Host: "127.0.0.1",
|
||||||
|
Port: 8888,
|
||||||
|
}
|
||||||
|
conf, err := New(confPath)
|
||||||
|
if assert.Nil(t, err, "Unexpected error") {
|
||||||
|
assert.Equal(t, expectedConf, *conf, "Unexpected config")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test creating a valid config (unix mode)
|
||||||
|
func TestOKUnix(t *testing.T) {
|
||||||
|
tmpPath := t.TempDir()
|
||||||
|
content := `{
|
||||||
|
"log": {
|
||||||
|
"level": "error"
|
||||||
|
},
|
||||||
|
"server": {
|
||||||
|
"mode": "unix",
|
||||||
|
"sock": "/run/toto.sock"
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
confPath := path.Join(tmpPath, "config.json")
|
||||||
|
require.Nil(t, os.WriteFile(confPath, []byte(content), 0o644), "Failed to write config")
|
||||||
|
|
||||||
|
expectedConf := AppConfig{
|
||||||
|
LogLevel: logrus.ErrorLevel,
|
||||||
|
ServerMode: ModeUnix,
|
||||||
|
SockPath: "/run/toto.sock",
|
||||||
|
}
|
||||||
|
conf, err := New(confPath)
|
||||||
|
if assert.Nil(t, err, "Unexpected error") {
|
||||||
|
assert.Equal(t, expectedConf, *conf, "Unexpected config")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test creating a valid config, no log level provided, should be info
|
||||||
|
func TestOKNoLogLevel(t *testing.T) {
|
||||||
|
tmpPath := t.TempDir()
|
||||||
|
content := `{
|
||||||
|
"server": {
|
||||||
|
"mode": "net",
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 8888
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
confPath := path.Join(tmpPath, "config.json")
|
||||||
|
require.Nil(t, os.WriteFile(confPath, []byte(content), 0o644), "Failed to write config")
|
||||||
|
|
||||||
|
expectedConf := AppConfig{
|
||||||
|
LogLevel: logrus.InfoLevel,
|
||||||
|
ServerMode: ModeNet,
|
||||||
|
Host: "127.0.0.1",
|
||||||
|
Port: 8888,
|
||||||
|
}
|
||||||
|
conf, err := New(confPath)
|
||||||
|
if assert.Nil(t, err, "Unexpected error") {
|
||||||
|
assert.Equal(t, expectedConf, *conf, "Unexpected config")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test giving an invalid server mode
|
||||||
|
func TestErrMode(t *testing.T) {
|
||||||
|
tmpPath := t.TempDir()
|
||||||
|
content := `{
|
||||||
|
"log": {
|
||||||
|
"level": "error"
|
||||||
|
},
|
||||||
|
"server": {
|
||||||
|
"mode": "toto",
|
||||||
|
"sock": "/run/toto.sock"
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
confPath := path.Join(tmpPath, "config.json")
|
||||||
|
require.Nil(t, os.WriteFile(confPath, []byte(content), 0o644), "Failed to write config")
|
||||||
|
|
||||||
|
_, err := New(confPath)
|
||||||
|
if assert.Error(t, err, "Unexpected nil error") {
|
||||||
|
errMsg := "failed to parse config file: failed to parse server listening mode: invalid listening mode toto"
|
||||||
|
assert.Equal(t, errMsg, err.Error(), "Unexpected error message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInvalidJSON(t *testing.T) {
|
||||||
|
tmpPath := t.TempDir()
|
||||||
|
content := "toto"
|
||||||
|
confPath := path.Join(tmpPath, "config.json")
|
||||||
|
require.Nil(t, os.WriteFile(confPath, []byte(content), 0o644), "Failed to write config")
|
||||||
|
_, err := New(confPath)
|
||||||
|
if assert.Error(t, err, "Unexpected nil error") {
|
||||||
|
errMsg := "failed to parse config file: invalid character 'o' in literal true (expecting 'r')"
|
||||||
|
assert.Equal(t, errMsg, err.Error(), "Unexpected error message")
|
||||||
|
}
|
||||||
|
}
|
15
polyculeconnect/go.mod
Normal file
15
polyculeconnect/go.mod
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
module git.faercol.me/faercol/polyculeconnect/template
|
||||||
|
|
||||||
|
go 1.20
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/sirupsen/logrus v1.9.3
|
||||||
|
github.com/stretchr/testify v1.7.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
|
||||||
|
)
|
16
polyculeconnect/go.sum
Normal file
16
polyculeconnect/go.sum
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
10
polyculeconnect/logger/logger.go
Normal file
10
polyculeconnect/logger/logger.go
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
var L *logrus.Logger
|
||||||
|
|
||||||
|
func Init(level logrus.Level) {
|
||||||
|
L = logrus.New()
|
||||||
|
L.SetLevel(level)
|
||||||
|
}
|
76
polyculeconnect/main.go
Normal file
76
polyculeconnect/main.go
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/template/config"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/template/logger"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/template/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
const stopTimeout = 10 * time.Second
|
||||||
|
|
||||||
|
type cliArgs struct {
|
||||||
|
configPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseArgs() *cliArgs {
|
||||||
|
configPath := flag.String("config", "", "Path to the JSON configuration file")
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
return &cliArgs{
|
||||||
|
configPath: *configPath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
args := parseArgs()
|
||||||
|
|
||||||
|
mainCtx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
conf, err := config.New(args.configPath)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Init(conf.LogLevel)
|
||||||
|
logger.L.Infof("Initialized logger with level %v", conf.LogLevel)
|
||||||
|
|
||||||
|
logger.L.Info("Initializing server")
|
||||||
|
s, err := server.New(conf, logger.L)
|
||||||
|
if err != nil {
|
||||||
|
logger.L.Fatalf("Failed to initialize server: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
go s.Run(mainCtx)
|
||||||
|
|
||||||
|
c := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(c, os.Interrupt)
|
||||||
|
|
||||||
|
logger.L.Info("Application successfully started")
|
||||||
|
|
||||||
|
logger.L.Debug("Waiting for stop signal")
|
||||||
|
select {
|
||||||
|
case <-s.Done():
|
||||||
|
logger.L.Fatal("Unexpected exit from server")
|
||||||
|
case <-c:
|
||||||
|
logger.L.Info("Stopping main application")
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.L.Debugf("Waiting %v for all daemons to stop", stopTimeout)
|
||||||
|
select {
|
||||||
|
case <-time.After(stopTimeout):
|
||||||
|
logger.L.Fatalf("Failed to stop all daemons in the expected time")
|
||||||
|
case <-s.Done():
|
||||||
|
logger.L.Info("web server successfully stopped")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.L.Info("Application successfully stopped")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
107
polyculeconnect/server/server.go
Normal file
107
polyculeconnect/server/server.go
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/template/config"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
httpSrv *http.Server
|
||||||
|
listener net.Listener
|
||||||
|
serverMode config.ListeningMode
|
||||||
|
address string
|
||||||
|
handler *http.ServeMux
|
||||||
|
l *logrus.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUnixListener(sockPath string) (net.Listener, error) {
|
||||||
|
if err := os.Remove(sockPath); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return nil, fmt.Errorf("failed to cleanup previously existing socket: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sock, err := net.Listen("unix", sockPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create unix socket: %w", err)
|
||||||
|
}
|
||||||
|
if err := os.Chmod(sockPath, 0o777); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to set permissions to unix socket: %w", err)
|
||||||
|
}
|
||||||
|
return sock, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(appConf *config.AppConfig, logger *logrus.Logger) (*Server, error) {
|
||||||
|
var listener net.Listener
|
||||||
|
var addr string
|
||||||
|
var err error
|
||||||
|
switch appConf.ServerMode {
|
||||||
|
case config.ModeNet:
|
||||||
|
addr = fmt.Sprintf("%s:%d", appConf.Host, appConf.Port)
|
||||||
|
listener, err = net.Listen("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to init server in net mode: %w", err)
|
||||||
|
}
|
||||||
|
case config.ModeUnix:
|
||||||
|
addr = appConf.SockPath
|
||||||
|
listener, err = newUnixListener(appConf.SockPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to init server in unix mode: %w", err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("unexpected listening mode %v", appConf.ServerMode))
|
||||||
|
}
|
||||||
|
|
||||||
|
m := http.NewServeMux()
|
||||||
|
|
||||||
|
return &Server{
|
||||||
|
handler: m,
|
||||||
|
httpSrv: &http.Server{
|
||||||
|
Handler: m,
|
||||||
|
},
|
||||||
|
listener: listener,
|
||||||
|
l: logger,
|
||||||
|
serverMode: appConf.ServerMode,
|
||||||
|
address: addr,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) initMux() {
|
||||||
|
s.handler.HandleFunc("/", s.statusHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Run(ctx context.Context) {
|
||||||
|
s.ctx, s.cancel = context.WithCancel(ctx)
|
||||||
|
s.initMux()
|
||||||
|
switch s.serverMode {
|
||||||
|
case config.ModeNet:
|
||||||
|
s.l.Infof("Server listening on host %q", s.address)
|
||||||
|
case config.ModeUnix:
|
||||||
|
s.l.Infof("Server listening on unix socket %q", s.address)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
if err := s.httpSrv.Serve(s.listener); err != nil {
|
||||||
|
s.l.Errorf("failed to serve HTTP server: %s", err.Error())
|
||||||
|
}
|
||||||
|
s.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Done() <-chan struct{} {
|
||||||
|
return s.ctx.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) statusHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write([]byte("Hello world!"))
|
||||||
|
}
|
Loading…
Reference in a new issue