A simple example of how to create a reusable Go module with commonly used tools.
The included tools are:
- Read JSON
- Write JSON
- Produce a JSON encoded error response
- Write XML
- Read XML
- Produce an XML encoded error response
- Upload a file to a specified directory
- Download a static file
- Get a random string of length n
- Post JSON to a remote service
- Create a directory, including all parent directories, if it does not already exist
- Create a URL safe slug from a string
- ContainsElement checks if a value exists in a slice
- Creating a QUERY map from a SQL file
go get -u github.com/tsawler/toolbox
package main
import (
"fmt"
"github.com/tsawler/toolbox"
)
func main() {
// create a variable of type toolbox.Tools, so we can use this variable
// to call the methods on that type
var tools toolbox.Tools
// get a random string
rnd := tools.RandomString(10)
fmt.Println(rnd)
}
In a handler, for example:
// JSONPayload is the type for JSON data that we receive
type JSONPayload struct {
Name string `json:"name"`
Data string `json:"data"`
}
// SomeHandler is the handler to accept a post request consisting of json payload
func (app *Config) SomeHandler(w http.ResponseWriter, r *http.Request) {
var tools toolbox.Tools
// read json into var
var requestPayload JSONPayload
_ = tools.ReadJSON(w, r, &requestPayload)
// do something with the data here...
// create the response we'll send back as JSON
resp := toolbox.JSONResponse{
Error: false,
Message: "logged",
}
// write the response back as JSON
_ = tools.WriteJSON(w, http.StatusAccepted, resp)
}
To download a static file, and force it to download instead of displaying in a browser:
// DownloadAFile downloads an arbitrary file
func (app *Config) DownloadAFile(w http.ResponseWriter, r *http.Request) {
var tools toolbox.Tools
tools.DownloadStaticFile(w, r, "./data", "file.pdf", "file.pdf")
}
To create a directory if it does not already exist:
// SomeHandler is some kind of handler
func (app *Config) SomeHandler(w http.ResponseWriter, r *http.Request) {
var tools toolbox.Tools
err := tools.CreateDirIfNotExist("./myDir")
if err != nil {
// do something with the error...
}
// keep going in the handler...
}
To upload a file to a specific directory, with this for HTML:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-0evHe/X+R7YkIZDRvuzKMRqM+OrBnVFBL6DOitfPri4tjfHxaWutUpFmBp4vmVor" crossorigin="anonymous">
<title>Upload test</title>
</head>
<body>
<div class="container">
<div class="row">
<div class="col">
<h1 class="mt-2">Upload a file</h1>
<hr>
<form action="http://localhost:8080/upload" method="post" enctype="multipart/form-data">
<div class="mb-3">
<label for="fileUpload" class="form-label">Choose file(s) to upload...</label>
<input class="form-control" type="file" id="fileUpload" name="uploaded" multiple>
</div>
<input class="btn btn-primary" type="submit" value="Upload file">
</form>
</div>
</div>
</div>
</body>
</html>
And this for a Go application:
package main
import (
"fmt"
"github.com/tsawler/toolbox"
"log"
"net/http"
)
func main() {
// handle html route (http://localhost:8080/)
http.Handle("/", http.StripPrefix("/", http.FileServer(http.Dir("."))))
// Post handler
http.HandleFunc("/upload", func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
t := toolbox.Tools{
MaxFileSize: 1024 * 1024 * 1024,
AllowedFileTypes: []string{"image/gif", "image/png", "image/jpeg"},
}
// Upload the file(s). Note that if you don't want the files to be renamed,
// you can add an optional final parameter -- true will rename the files (the default)
// and false will preserve the original filenames, for example:
// files, err := t.UploadFiles(r, "./uploads", false)
// n.b.: if the "./uploads" directory does not exist, we attempt to create it.
files, err := t.UploadFiles(r, "./uploads")
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// the returned variable, files, will be a slice of the type toolbox.UploadedFile
_, _ = w.Write([]byte(fmt.Sprintf("Uploaded %d file(s) to the uploads folder", len(files))))
})
// print a log message
log.Println("Starting server on port 8080")
// start the server
http.ListenAndServe(":8080", nil)
}
To make a JSON post to a remote URI, with this html:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3"
crossorigin="anonymous">
<title>JSON functionality</title>
<style>
label{
font-weight: bold;
}
</style>
</head>
<body>
<div class="container">
<div class="row">
<div class="col">
<h1 class="mt-2">JSON functionality</h1>
<hr>
<form>
<div class="mb-3">
<label for="json" class="form-label">JSON to Send:</label>
<textarea style="font-family: Courier,sans-serif" class="form-control"
id="json" name="json" rows="5">
{
"action": "some action",
"message": "some message"
}
</textarea>
</div>
<a id="pushBtn" class="btn btn-primary">Push JSON</a>
</form>
<hr>
<p><strong>Response from server:</strong></p>
<div style="outline: 1px solid silver; padding: 2em">
<pre id="response">No response from server yet...</pre>
</div>
</div>
</div>
</div>
<script>
let pushBtn = document.getElementById("pushBtn");
let jsonPayload = document.getElementById("json")
let serverResponse = document.getElementById("response");
pushBtn.addEventListener("click", function () {
const payload = jsonPayload.value;
const headers = new Headers();
const body = {
method: 'POST',
body: payload,
headers: headers,
}
headers.append("Content-Type", "application/json");
fetch("http://localhost:8081/receive-post", body)
.then((response) => response.json())
.then((data) => {
serverResponse.innerHTML = JSON.stringify(data, undefined, 4);
})
.catch((error) => {
serverResponse.innerHTML = "<br><br>Error: " + error;
})
})
</script>
</body>
</html>
You can use this kind of Go code:
package main
import (
"github.com/tsawler/toolbox"
"log"
"net/http"
)
func main() {
// create a default server mux
mux := http.NewServeMux()
// register routes
mux.Handle("/", http.StripPrefix("/", http.FileServer(http.Dir("."))))
mux.HandleFunc("/receive-post", receivePost)
mux.HandleFunc("/remote-service", remoteService)
// print a log message
log.Println("Starting server on port 8081")
// start the server
err := http.ListenAndServe(":8081", mux)
if err != nil {
log.Fatal(err)
}
}
// RequestPayload describes the JSON that this service accepts as an HTTP Post request
type RequestPayload struct {
Action string `json:"action"`
Message string `json:"message"`
}
// ResponsePayload is the structure used for sending a JSON response
type ResponsePayload struct {
Message string `json:"message"`
StatusCode int `json:"status_code,omitempty"`
}
func receivePost(w http.ResponseWriter, r *http.Request) {
// get the posted json and decode it
var requestPayload RequestPayload
var t toolbox.Tools
err := t.ReadJSON(w, r, &requestPayload)
if err != nil {
_ = t.ErrorJSON(w, err)
return
}
// Call remote service. Note that we are ignoring the first return parameter, which is the
// entire response from the remote service, but you have access to it if you need it.
_, statusCode, err := t.PushJSONToRemote("http://localhost:8081/remote-service", requestPayload)
if err != nil {
_ = t.ErrorJSON(w, err)
return
}
// send response
payload := ResponsePayload{
Message: "hit the service ok",
StatusCode: statusCode,
}
err = t.WriteJSON(w, http.StatusAccepted, payload)
if err != nil {
log.Println(err)
}
}
// remoteService just simulates calling some remote API
func remoteService(w http.ResponseWriter, r *http.Request) {
payload := ResponsePayload{
Message: "OK",
}
var t toolbox.Tools
_ = t.WriteJSON(w, http.StatusOK, payload)
}
To slugify a string, we simply remove all non URL safe characters and return the original string with a hyphen where spaces would be. Example:
package main
import (
"fmt"
"github.com/tsawler/toolbox"
)
func main() {
toSlugify := "hello, world! These are unsafe chars: こんにちは世界*!&^%"
fmt.Println("To slugify:", toSlugify)
var tools toolbox.Tools
slug, err := tools.Slugify(toSlugify)
if err != nil {
fmt.Println(err)
}
fmt.Println("Slugified:", slug)
}
Output from this is:
To slugify: hello, world! These are unsafe chars: こんにちは世界*!&^%
Slugified: hello-world-these-are-unsafe-chars
It is a method we often use when writing code. Is the value we are looking for present in the slice? The type of this slice can be of any type. Example:
package main
import(
"fmt"
"github.com/tsawler/toolbox"
)
func main(){
var tools toolbox.Tools
tests := []test{{name: "abc"}, {name: "def"}}
t1 := test{name: "def"}
t2 := test{name: "xyz"}
if tools.ContainsElement(t1, tests) {
fmt.Println("This slice contains the key you are looking for.")
}
if !tools.ContainsElement(t2, tests) {
fmt.Println("This slice does not contain the key you are looking for.")
}
}
This tool aims to facilitate the use of the go language's database/sql standard library. Writing SQL queries directly in the code can make it messy, so writing SQL queries in .sql files and then calling them from the code helps prevent code clutter, allowing SQL queries to be centralized in one place for better organization. Example:
package main
import(
"database/sql"
"encoding/json"
"log"
"github.com/tsawler/toolbox"
)
type City struct {
ID int `json:"id,omitempty"`
Name string `json:"name,omitempty"`
CountryID int `json:"country_id,omitempty"`
}
var DB *sql.DB
var QUERY map[string]string
func main(){
var tools toolbox.Tools
// Load Sql
QUERY = make(map[string]string)
if query, err := tools.LoadSQLQueries("./testdata/test.sql"); err != nil {
log.Fatalf("Load Sql Error: %v", err)
} else {
QUERY = query
}
var cities []City
rows, err := DB.Query(QUERY["CITIES"])
if err != nil {
log.Printf("GET Cities Error %v", err)
return
}
defer func(rows *sql.Rows) {
_ = rows.Close()
}(rows)
for rows.Next() {
var city City
_ = rows.Scan(&city.ID, &city.Name, &city.CountryID)
cities = append(cities, city)
}
if marshal, err := json.MarshalIndent(cities, "", " "); err != nil {
return
}else{
log.Println(string(marshal))
}
}