DEV Community

Pacharapol Withayasakpunt
Pacharapol Withayasakpunt

Posted on

Practical web server in vanilla Go with clean-up function (I don't really know what I am doing)

So, I tried to write a web server in Golang to fit in with webview/webview, although I currently have an important issue (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...

// 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)
}
Enter fullscreen mode Exit fullscreen mode

In short, I don't really like Go, 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
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)
}
Enter fullscreen mode Exit fullscreen mode

Tested with cURL's

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

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

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
    })
  })
}

Enter fullscreen mode Exit fullscreen mode

Oldest comments (0)