Practical web server in vanilla Go with clean-up function (I don't really know what I am doing)
Tags: go, beginners, help
Published: 2020-08-29 03:24:57

So, I tried to write a web server in Golang to fit in with [webview/webview](https://github.com/webview/webview), although I currently have [an important issue](https://github.com/webview/webview/issues/458) (which is `cgo`)...

There are Go language-specific concepts I don't really understand.

- Channel
- `<-`

Will signals works  in Windows, which is not POSIX?

And there are things I clearly don't like...

```go
// Scoping of error variables.
// Is there a better naming conventions?
// IMO, these kind of namings are very typo-prone.
_, e1 := f1()
if e1 != nil {
  log.Fatal(e1)
}
data, e2 := f1()
if e2 != nil {
  log.Fatal(e2)
}
```

In short, [I don't really like Go](https://dev.to/patarapolw/how-can-i-learn-to-like-golang-and-use-it-a-lot-38dj), but industries and job markets demand it.

I haven't done JSON serialization / deserialization yet. Not to mention middlewares and auth. However, I do cover

- `req.Query`
- `req.Body`
- `res.Write`
- Status codes

```go
package main

import (
	"context"
	"fmt"
	"io/ioutil"
	"log"
	"net"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"
)

func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = "0"
	}

	listener, err := net.Listen("tcp", "localhost:"+port)
	if err != nil {
		log.Fatal(err)
	}

	http.Handle("/", http.FileServer(http.Dir("./dist")))
	http.HandleFunc("/api/file", func(w http.ResponseWriter, r *http.Request) {
		f := r.URL.Query()["filename"]
		if len(f) == 0 {
			throwHTTP(&w, fmt.Errorf("filename not supplied"), http.StatusNotFound)
			return
		}
		filename := f[0]

		if r.Method == "GET" {
			data, eReadFile := ioutil.ReadFile(filename)
			if eReadFile != nil {
				throwHTTP(&w, eReadFile, http.StatusInternalServerError)
				return
			}
			w.Write(data)
			return
		} else if r.Method == "PUT" {
			data, eReadAll := ioutil.ReadAll(r.Body)
			if eReadAll != nil {
				throwHTTP(&w, eReadAll, http.StatusInternalServerError)
				return
			}
			eWriteFile := ioutil.WriteFile(filename, data, 0666)
			if eWriteFile != nil {
				throwHTTP(&w, eWriteFile, http.StatusInternalServerError)
				return
			}
			w.WriteHeader(http.StatusCreated)
			return
		} else if r.Method == "DELETE" {
			eRemove := os.Remove(filename)
			if eRemove != nil {
				throwHTTP(&w, eRemove, http.StatusInternalServerError)
				return
			}
			w.WriteHeader(http.StatusCreated)
			return
		}

		throwHTTP(&w, fmt.Errorf("unsupported method"), http.StatusNotFound)
	})

	go func() {
		// True port will be autogenerated
		// And Addr() generated accordingly
		log.Println("Listening at:", "http://"+listener.Addr().String())
		if err := http.Serve(listener, nil); err != http.ErrServerClosed {
			log.Fatal(err)
		}
	}()

	// Cleaning up should be done in 10 seconds
	onExit(10 * time.Second)
}

func onExit(timeout time.Duration) {
	signals := make(chan os.Signal, 1)
	signal.Notify(signals, os.Interrupt, syscall.SIGTERM)

	<-signals

	log.Println("Cleaning up...")

	ctx, cancel := context.WithTimeout(context.Background(), timeout)
	defer cancel()

	// onExit proper
	func() {
		time.Sleep(2 * time.Second)
	}()

	if _, ok := ctx.Deadline(); ok {
		log.Println("Clean-up finished. Closing...")
		// secs := (time.Until(deadline) + time.Second/2) / time.Second
		// log.Printf("Clean-up finished %ds before deadline\n", secs)
	} else {
		log.Fatal(fmt.Sprintf("Clean-up timeout. Not finished within %ds.", timeout/time.Second))
	}
}

func throwHTTP(w *http.ResponseWriter, e error, code int) {
	http.Error(*w, e.Error(), code)
	log.Println(e, code)
}
```

Tested with cURL's

```sh
% PORT=3000 go run .
% curl -i -X PUT --data 'hello' http://127.0.0.1:3000/api/file\?filename\=test.txt
```

I have another nice way to connect with frontend. You can argue that sending SQL and its parameters might be a better way...

```ts
import Loki from 'lokijs'

class LokiRestAdaptor {
  loadDatabase (dbname: string, callback: (data: string | null | Error) => void) {
    fetch(`/api/file?filename=${encodeURIComponent(dbname)}`)
      .then((r) => r.text())
      .then((r) => callback(r))
      .catch((e) => callback(e))
  }

  saveDatabase (dbname: string, dbstring: string, callback: (e: Error | null) => void) {
    fetch(`/api/file?filename=${encodeURIComponent(dbname)}`, {
      method: 'PUT',
      body: dbstring
    })
      .then(() => callback(null))
      .catch((e) => callback(e))
  }

  deleteDatabase (dbname: string, callback: (data: Error | null) => void) {
    fetch(`/api/file?filename=${encodeURIComponent(dbname)}`, {
      method: 'DELETE'
    })
      .then(() => callback(null))
      .catch((e) => callback(e))
  }
}

// eslint-disable-next-line import/no-mutable-exports
export let loki: Loki

export async function initDatabase () {
  return new Promise((resolve) => {
    loki = new Loki('db.loki', {
      adapter: new LokiRestAdaptor(),
      autoload: true,
      autoloadCallback: () => {
        resolve()
      },
      autosave: true,
      autosaveInterval: 4000
    })
  })
}

```