GO Web Services

From bibbleWiki
Jump to navigation Jump to search

Handling Requests

These are the two main ways to handle requests

  • Handle a handler to handle requests matching a pattern
  • HandleFunc a function to handle requests matching a pattern

Handle

Here is the signature for Handle.

func Handle(pattern string, handler Handler)

Handler is an interface which has the signature

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

So here is how to use this approach. We create a type and implement the interface required.

// Creating a handler type
type fooHandler struct {
  Message string
}

// Implementing the function for the interface
func (f *fooHandler) ServeHTTP(w http.ResponseWrite, r *http.Request) {
  w.Write( []byte(f.Message) )
}


func main() {
  http.Handle("/foo", &fooHandler{Message: "hello Word"})
}

Remember Remember we can construct a struct using a struct literal e.g.

type Student struct {
    Name string
    Age  int
}

pb := &Student{ // pb == &Student{"Bob", 8}
    Name: "Bob",
    Age:  8,
}

Requests

These consist of three parts

  • Method
  • Headers
  • Body

HandleFunc

Perhaps somewhat simpler. We just need to create a function with the correct signature

func main() {
  foo := funct(w http.ResponseWritter, _ *http.Response) {
    w.Write( []byte("hello world") )
  }

  http.HandleFunc("/foo", foo)
}

Handling JSON

Marshal and Unmarshal

Every GO type implements the empty interface so any struct can be marshaled into JSON.

func Marshal(v interface{}) (byte[]. error)

To ensure the struct is marshaled, all the fields need to be exported. In this example surname would not be marchaled.

type foo struct{
  Message string
  Age int
  surname string
}

json,Marshal(&foo{"4Score",56, "Lincoln"})

To Unmarshal

f := foo{}
json.Unmarshal([]byte(`{"Message":"4Score","Age":56, "Name":"Abe"}`), &f)

Demo Data

Demo Type

Lets make a type to encode

package product

// Product
type Product struct {
	ProductID      int    `json:"productId"`
	Manufacturer   string `json:"manufacturer"`
	Sku            string `json:"sku"`
	Upc            string `json:"upc"`
	PricePerUnit   string `json:"pricePerUnit"`
	QuantityOnHand int    `json:"quantityOnHand"`
	ProductName    string `json:"productName"`
}

Make Some Demo Data

Here is some demo data.

[
  {
    "productId": 1,
    "manufacturer": "Johns-Jenkins",
    "sku": "p5z343vdS",
    "upc": "939581000000",
    "pricePerUnit": "497.45",
    "quantityOnHand": 9703,
    "productName": "sticky note"
  },
  {
    "productId": 2,
    "manufacturer": "Hessel, Schimmel and Feeney",
    "sku": "i7v300kmx",
    "upc": "740979000000",
    "pricePerUnit": "282.29",
    "quantityOnHand": 9217,
    "productName": "leg warmers"
  },
...

Below is a example of how to read the json into a map

func loadProductMap() (map[int]Product, error) {
	fileName := "products.json"
	_, err := os.Stat(fileName)
	if os.IsNotExist(err) {
		return nil, fmt.Errorf("file [%s] does not exist", fileName)
	}

	file, _ := ioutil.ReadFile(fileName)
	productList := make([]Product, 0)
	err = json.Unmarshal([]byte(file), &productList)
	if err != nil {
		log.Fatal(err)
	}
	prodMap := make(map[int]Product)
	for i := 0; i < len(productList); i++ {
		prodMap[productList[i].ProductID] = productList[i]
	}
	return prodMap, nil
}

Implementing Method Types

Create Handler and Determine Mathod

We can now provide a GET method within the handler. The handler determines which method by looking at the request.

func handleProduct(w http.ResponseWriter, r *http.Request) {
...
	switch r.Method {
            // GET
            // POST
            // etc
        }
}

GET Method

We use the Mnmarshal method to encode the json

	case http.MethodGet:
		productsJSON, err := json.Marshal(productList)
		if err != nil {
			w.WriteHeader(http.StatusInternalServerError)
			return
		}
		w.Header().Set("Content-Type", "application/json")
		w.Write(productsJSON)

POST Method

We use the Unmarshal method to decode the json. In the source they replaced the ReadAll Unmarshal in favour of json.NewDocder.

	case http.MethodPost:
		// add a new product to the list

		var newProduct Product

		bodyBytes, err := ioutil.ReadAll(r.Body)
		if err != nil {
			w.WriteHeader(http.StatusBadRequest)
			return
		}
		err = json.Unmarshal(bodyBytes, &newProduct)
		if err != nil {
			w.WriteHeader(http.StatusBadRequest)
			return
		}

		err := json.NewDecoder(r.Body).Decode(&product)
		if err != nil {
			log.Print(err)
			w.WriteHeader(http.StatusBadRequest)
			return
		}


		if newProduct.ProductID != 0 {
			w.WriteHeader(http.StatusBadRequest)
			return
		}
		newProduct.ProductID = getNextID()
		productList = append(productList, newProduct)
		w.WriteHeader(http.StatusCreated)

Managing the Routes

Example Routes

With REST we generally have one route for all products and one for a particular product. e.g. GET /products/123. To manage this we greate different handlers.

	http.Handle("/products", middlewareHandler(productListHandler))
	http.Handle("/products/", middlewareHandler(productItemHandler))

Managing the URL

Here is an example of getting the 123 from the URL. Note we return a 404 if we cannot determine the resource and not a bad request.

func productHandler(w http.ResponseWriter, r *http.Request) {
	urlPathSegments := strings.Split(r.URL.Path, "products/")
	productID, err := strconv.Atoi(urlPathSegments[len(urlPathSegments)-1])
	if err != nil {
		log.Print(err)
		w.WriteHeader(http.StatusNotFound)
		return
	}
	product, listItemIndex := findProductByID(productID)
	if product == nil {
		http.Error(w, fmt.Sprintf("no product with id %d", productID), http.StatusNotFound)
		return
	}
	switch r.Method {
	case http.MethodGet:
...

Middeware

Explanation

I liked this example provided in GO. Just made sense to me. This is like the interceptors in Angular.

func enableCorsMiddleware(handler http.Handler) http.Handler {

  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

  // do stuff before intended handler here 

  handler.ServeHttpp(w,r)

  // do stuff after intended handler here 

})

func intendedFunction(w http.ResponseWriter, r *http.Request) {
  // business logic here
}

func main() {
  intendedHandler := http.HandlerFunc(intendedFunction)
  http.Handle("/foo", enableCorsMiddleware(intendedHandler))
  http.ListenAndServe(":5000", nil)
}

Wrapping Handlers

When using middleware we need to convert our handler to http.handlers. To do this we use the http.handlerFunc. E.g.

// Before Middleware

//	http.HandleFunc("/products", productsHandler)
//	http.HandleFunc("/products/", productHandler)
//	http.ListenAndServe(":5000", nil)

func main() {
	productListHandler := http.HandlerFunc(productsHandler)
	productItemHandler := http.HandlerFunc(productHandler)

	http.Handle("/products", middlewareHandler(productListHandler))
	http.Handle("/products/", middlewareHandler(productItemHandler))
	http.ListenAndServe(":5000", nil)
}

Yet another CORs

Well love diagrams to here are another two. CORs prevents same-site access. Where means different ports, protocols subdomains are not allowed.
GO Cors.png

Below is a sample function to enable CORs. Do it, do it, don't do it.

func enableCorsMiddleware(handler http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Add("Access-Control-Allow-Origin", "*")
		w.Header().Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
		w.Header().Add("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Authorization, X-CSRF-Token, Accept-Encoding")
		handler.ServeHTTP(w, r)
	})
}

GO Approaches

This is a section to suggest some approaches when using GO

Marshaling

Introduction

To provide different view of data based on the header the example below should how this could be accomplished

Define the Type

The same old type as ever. Do not judge me by the password in plain text. It is an example.

type Users []User

type User struct {
	Id          int64  `json:"id"`
	FirstName   string `json:"first_name"`
	LastName    string `json:"last_name"`
	Email       string `json:"email"`
	DateCreated string `json:"date_created"`
	Status      string `json:"status"`
	Password    string `json:"password"`
}

Define a Marshaller

This provides a way give different data based on the flag. It seems to me to be more like a DTO approach in C# but in the demo the user_dto was what I would call the entity.

package users

import "encoding/json"

type PublicUser struct {
	Id          int64  `json:"id"`
	DateCreated string `json:"date_created"`
	Status      string `json:"status"`
}

type PrivateUser struct {
	Id          int64  `json:"id"`
	FirstName   string `json:"first_name"`
	LastName    string `json:"last_name"`
	Email       string `json:"email"`
	DateCreated string `json:"date_created"`
	Status      string `json:"status"`
	Password    string `json:"password"`
}

func (users Users) Marshall(isPublic bool) interface{} {

	result := make([]interface{}, len(users))
	for index, user := range users {
		result[index] = user.Marshall(isPublic)
	}
	return result
}

func (user *User) Marshall(isPublic bool) interface{} {
	if isPublic {
		return PublicUser{
			Id:          user.Id,
			DateCreated: user.DateCreated,
			Status:      user.Status,
		}
	}
	userJson, _ := json.Marshal(user)
	var privateUser PrivateUser
	json.Unmarshal(userJson, &privateUser)
	return privateUser
}

Structuring Code for Services

One of the hardest parts of learning new languages is not writing the code but doing it in a way which is perceived as best practice. For the service layer of a RESTful API you define the interface and then implement the functionality which makes test much easier as you are able to mock. An example is shown below

var (
	UsersService usersServiceInterface = &usersService{}
)

type usersService struct {
}

type usersServiceInterface interface {
	GetUser(userId int64) (*users.User, *errors.RestErr)
	CreateUser(user users.User) (*users.User, *errors.RestErr)
	UpdateUser(isPartial bool, user users.User) (*users.User, *errors.RestErr)
	DeleteUser(userId int64) *errors.RestErr
	SearchUser(status string) (users.Users, *errors.RestErr)
}

func (s *usersService) GetUser(userId int64) (*users.User, *errors.RestErr) {

	// Create empty User
	result := &users.User{Id: userId}

	//
	if err := result.Get(); err != nil {
		return nil, err
	}

	return result, nil
}
...

Usage

So in the controller, we are using MVC, we can check the header to provide different views.

func Search(c *gin.Context) {

	status := c.Query("status")

	users, err := services.Search(status)
	if err != nil {
		c.JSON(err.Status, err)
		return
	}
	c.JSON(http.StatusOK, users.Marshall(c.GetHeader("X-Public") == "true"))
}

Log

Example of using the zap logger.

package logger

import (
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
)

var (
	log *zap.Logger
)

func init() {
	logConfig := zap.Config{
		OutputPaths: []string{"stdout"},
		Level:       zap.NewAtomicLevelAt(zap.InfoLevel),
		Encoding:    "json",
		EncoderConfig: zapcore.EncoderConfig{
			LevelKey:     "level",
			TimeKey:      "time",
			MessageKey:   "msg",
			EncodeTime:   zapcore.ISO8601TimeEncoder,
			EncodeLevel:  zapcore.LowercaseColorLevelEncoder,
			EncodeCaller: zapcore.ShortCallerEncoder,
		},
	}

	var err error
	if log, err = logConfig.Build(); err != nil {
		panic(err)
	}
}

func GetLogger() *zap.Logger {
	return log
}

func Info(msg string, tags ...zap.Field) {
	log.Info(msg, tags...)
}

func Error(msg string, err error, tags ...zap.Field) {
	tags = append(tags, zap.NamedError("error", err))
	log.Error(msg, tags...)
}

OAuth

Introduction

Another example of OAUTH but this time in GO. This is what we are going to implement.
OAuth GO.png
We are going to be using DDD or Domain Driven Design with the Clean Architecture
DDD Domain Driven Design.png

Project Structure

Here is the project structure where the http is the presentation layer, domain is the domain layer and the repository is the data layer.
OAuth Project.png

Object Interaction Diagram

The Service is injected with an instance of the repository DB and the Presentation layer is injected with the domain. This means the presentation layer and the data layer are independent of the domain layer.
OAuth OID.png

Persisting Data

Connection to Database

Nothing too difficult here.

package services

import (
  "database/sql"
  "strconv"
  "log"
)

type DatabaseConnection struct {
  Db *sql.DB
}

func NewDatabaseConnection(
  dbHost string,
  dbPort int,
  dbName, dbUser, dbPass string) *DatabaseConnection {

  var connectionString = 
    dbUser + ":" + dbPass + "@tcp(" + dbHost + ":" + strconv.Itoa(dbPort) + ")/" + 
    dbName + "?parseTime=true&autocommit=false"

   dbConnection, err := sql.Open("mysql", connectionString)
   if err != nil {
    log.Fatal("Error connecting to Database. Error is %s", err.Error())
   }
}

Doing a SELECT

We create a Select Statement using prepare to do this once as it is expenseive

func CreateSelectStatement(databaseConnection *DatabaseConnection) *sql.Stmt {

  selectStatement, err := databaseConnection.Db.Prepare(
   "SELECT NEW_CASES, NEW_DEATHS, NEW_RECOVERED, TOTAL_CASES, TOTAL_DEATHS, TOTAL_RECOVERED, RECORD_DATE_TIME FROM COVID_DATA WHERE ALPHA2CODE = ? AND YEAR = ? AND DAY = ?")

  if err != nil {
     log.Fatal("Error Creating Select Statement. Error was %s", err.Error())
  }

  return selectStatement
}

Now we can bind the arguments to the statement

func (covidDataTable *CovidDataTable) Select(
  alpha2Code string,
  year, day int) (bool, *models.CovidData) {

  // Get the rows
  rows, err := covidDataTable.selectStatement.Query(&alpha2Code, &year, &day)
  if err != nil {
    log.Fatal("Error occurred selecting covid data. Error was  %s", err.Error())
    return false, nil
  }
...

And then we can iterate over the results and make sure we close the rows if there is an error.

...

defer rows.Close()

  ok := rows.Next()
  if !ok {
     log.Fatal("Error occurred Selecting covid data for Country %s, Year %d, Day.", alpha2Code, year, day)
  }

  var newConfirmed, newDeaths, newRecovered int
  var totalConfirmed, totalDeaths, totalRecovered int
  var recordDateTime time.Time

  err = rows.Scan(
    &newConfirmed, &newDeaths, &newRecovered,
    &totalConfirmed, &totalDeaths, &totalRecovered, &recordDateTime)

    if err != nil {
      log.Fatal("Error occurred selecting covid data. Error was  %s", err.Error())
                return false, nil
    }
...

Doing a INSERT

We create a Insert Statement using prepare to do this once as it is expenseive

func CreateInsertStatement(databaseConnection *DatabaseConnection) *sql.Stmt {

  insertStatement, err := databaseConnection.Db.Prepare(
    "INSERT INTO COVID_DATA(ALPHA2CODE, YEAR, DAY, NEW_CASES, NEW_DEATHS, NEW_RECOVERED, TOTAL_CASES, TOTAL_DEATHS, TOTAL_RECOVERED, RECORD_DATE_TIME) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")

  if err != nil {
     log.Fatal("Error Creating Insert Statement. Error was %s", err.Error())
  }
  return insertStatement
}

And now we can to the insert

func (covidDataTable *CovidDataTable) Insert(
  tx *sql.Tx,
  alpha2Code string,
  year, day int,
  covidData *models.CovidData) bool {

  // Do the insert
  result, err := tx.Stmt(covidDataTable.insertStatement).Exec(
    &alpha2Code,
    &year,
    &day,
    covidData.NewConfirmed,
    covidData.NewDeaths,
    covidData.NewRecovered,
    covidData.TotalConfirmed,
    covidData.TotalDeaths,
    covidData.TotalRecovered,
    covidData.RecordDateTime)

    var myError = covidDataTable.insertStatement.Close()
    if myError != nil {
      log.Fatal("Yes my son %s", myError.Error())
    }

    // Check for Error
    if err != nil {
      log.Fatal("Error occurred inserting covid data. Error Executing statement. Error was  %s", err.Error())
    }

    rows, err := result.RowsAffected()
    if err != nil {
      logFatal("Error occurred inserting covid data.  Error getting row affected. Error was  %s", err.Error())
    }

    // All good
    return true
}

Managing Database Resources

Connection Pools

These are the parameters available.

  • Connection Max Lifetime - Sets maximum amount of time a connection may be used
  • Max Idle Connections - Sets the maxium number in idle connection pool - default is 2
  • Max Open Connections - Sets the maximum open connections

Using Contexts

These allow you to perform tasks with timeouts. Useful to ensure resource are no exhausted.

func getProduct(productID int) (*Product, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()
	row := database.DbConn.QueryRowContext(ctx, `SELECT 
	productId, 
	manufacturer, 
	sku, 
	upc, 
	pricePerUnit, 
	quantityOnHand, 
	productName 
	FROM products 
	WHERE productId = ?`, productID)

	product := &Product{}
	err := row.Scan(
		&product.ProductID,
		&product.Manufacturer,
		&product.Sku,
		&product.Upc,
		&product.PricePerUnit,
		&product.QuantityOnHand,
		&product.ProductName,
	)

	if err == sql.ErrNoRows {
		return nil, nil
	} else if err != nil {
		log.Println(err)
		return nil, err
	}
	return product, nil
}

File Uploading

To upload files we could

  • encode the file to base64 and convert to Json
  • multipart/form-data type which uses HTTP form submit

Encode

To decode the string we would need to

str := "SGVsbG8gV29ybGQ="

output, err := base64.StdEncoding.DecodString(str)
if err != nill {
  log.Fatal(err)
}

fmt.Printf("%q\n", output)

Using Multipart

Uploading

The ParsMultiPartForm will start using disk if the limit is exceeded.
For the roll left bit

op1 << op2 Shift bits of op1 left by distance op2; fills with zero bits on the right-hand side
5    << 20
0101 << 00000000000000000000
010100000000000000000000 (binary) = 50000 (Bytes)


func uploadFileHandler(w http.ResponseWriter, r *http.Request) {
  r.ParseMultipartForm(5 << 20) // 5 Mb
  file, handler, err := r.FormFile("receipt")
  if err != nil {
    log.Print(err)
    w.WriteHeader(http.StatusBadRequest)
    return
  }

  defer file.Close()

  f, err := os.OpenFile(filepath.Join(ReceiptDirectory, handler.Filename), os.O_WRONLY|os.O_CREATE, 0666)
  
   defer f.Close()
   io.Copy(f, file)
}

Downloading

And here is an example of Downloading

  fileName := urlPathSegments[1:][0]
  file, err := os.Open(filepath.Join(ReceiptDirectory, fileName))
  defer file.Close()
  if err != nil {
	w.WriteHeader(http.StatusNotFound)
	return
  }
  
  fHeader := make([]byte, 512)
  file.Read(fHeader)
  fContentType := http.DetectContentType(fHeader)

  stat, err := file.Stat()
  if err != nil {
	w.WriteHeader(http.StatusInternalServerError)
	return
  }

  fSize := strconv.FormatInt(stat.Size(), 10)
  w.Header().Set("Content-Disposition", "attachment; filename="+fileName)
  w.Header().Set("Content-Type", fContentType)
  w.Header().Set("Content-Length", fSize)
  file.Seek(0, 0)
  io.Copy(w, file)

WebSockets

Introduction

Previously we used to poll for changes. With WebSockets we can solve this problem.

  • Client sends HTTP GET request
    • Connection: Upgrade
    • Upgrade: Websocket
    • Sec-WebSocket-Key: key
  • Server Responds with status code "101"
    • Switching Protocols
    • Upgrade: Websocket
    • Connection: Upgrade
    • Sec-WebSocket-Accept: key

Creating WebSocket

For GO the package is golang.org/x which means
These packages are part of the Go Project but outside the main Go tree. They are developed under looser compatibility requirements than the Go core. Install them with "go get".

I understand there are alternatives. Googling showed Gorilla to be one of the most popular.

Create Handler

So we are going to use golang. Let create a handler.

package test

import (
	"net/http"

	"golang.org/x/net/websocket"
)

func SetupRoutes(apiBasePath string) {
	http.Handle("/websocket", websocket.Handler(testSocket))
}

The interface is

type Handler func(*Conn)

Implement the Handler Version 1

Now implement the handler.

package test

import (
	"fmt"
	"log"
	"time"

	"golang.org/x/net/websocket"
)

type message struct {
	Data string `json:"data"`
	Type string `json:"type"`
}

func testSocket(ws *websocket.Conn) {

	// we can verify that the origin is an allowed origin
	fmt.Printf("origin: %s\n", ws.Config().Origin)

	done := make(chan struct{})
	go func(c *websocket.Conn) {
		for {
			var msg message
			if err := websocket.JSON.Receive(ws, &msg); err != nil {
				log.Println(err)
				break
			}
			fmt.Printf("received message %s\n", msg.Data)
		}
		close(done)
	}(ws)

loop:
	for {

		select {

		// End the infinate loop
		case <-done:
			fmt.Println("connection was closed, lets break out of here")
			break loop

		// Do Send of Data
		default:
			// Construct Data
			testData := TestData{"FRED1", "FRED2"}

			if err := websocket.JSON.Send(ws, testData); err != nil {
				log.Println(err)
				break
			}

			fmt.Println("Send some data")

			// pause for 10 seconds before sending again
			time.Sleep(10 * time.Second)
		}
	}

	fmt.Println("closing the connection")
	defer ws.Close()
}

Implement the Handler Version 2

This is a second version. I think it is better but do not like the sending requiring to wait. Could create a second channel I guess

package test

import (
	"errors"
	"log"
	"time"

	"golang.org/x/net/websocket"
)

type message struct {
	Data string `json:"data"`
	Type string `json:"type"`
}

func processSend(ws *websocket.Conn, errorChannel chan error) {

	// Constructing Data to send
	testSendData := TestData{"Prop Value 1", "Prop Value 2"}

	// Let do it every 10 seconds
	tickChannel := time.NewTicker(10 * time.Second)

	go func(c *websocket.Conn, errorChannel chan error) {
		for ; true; <-tickChannel.C {
			log.Println("ProcessSend: Sending the data")
			if err := websocket.JSON.Send(ws, testSendData); err != nil {
				log.Println("ProcessSend: Got a send error")
				log.Println(err)
				break
			}
		}
	}(ws, errorChannel)

	log.Println("ProcessSend: Finished kicking of send")
}

func processReceive(ws *websocket.Conn, errorChannel chan error) {

	go func(c *websocket.Conn, errorChannel chan error) {
		for {
			var msg message
			if err := websocket.JSON.Receive(ws, &msg); err != nil {
				log.Println("ProcessReceive: We going to print receive error")
				log.Println(err)
				errorChannel <- errors.New("error receiving data")
				break
			}
			log.Printf("ProcessReceive: Received message %s\n", msg.Data)
		}
	}(ws, errorChannel)

	log.Println("ProcessReceive: Finished kicking of receive")
}

func testSocket(ws *websocket.Conn) {

	log.Printf("testSocket: Receive a request from origin: %s\n", ws.Config().Origin)

	// Set up error channel to share with send and receive
	errorChannel := make(chan error)

	processReceive(ws, errorChannel)
	processSend(ws, errorChannel)

loop:
	for {

		select {

		// Receive close of channel
		case <-errorChannel:
			log.Println("testSocket: ErrorChannel received message. Closing the socket")
			ws.Close()
			log.Println("testSocket: Waiting for send to stop")
			time.Sleep(11 * time.Second)
			log.Println("testSocket: Completed waiting for send to stop")
			break loop
		}

	}
	log.Println("testSocket: Closing the channel")
}

Testing a Websocket

We can use the devtools within chrome

let ws = new WebSocket("ws://localhost/localhost:5000/websocket")
ws.send(JSON.stringify({"data":"test data", "type":"Test"})

Templates

Introduction

This is like pug and esj where you provide data and a markup which has placeholders for the data.

package main

import (
  "html/template"
  "os"
)

type BlogPost struct {
  Title   string
  Content string
}

func main() {
  post := BlogPost{"First Post", "This is the blog post content section"}
  tmpl, err := template.New("blog-tmpl").Parse("<h1>{{.Title}}</h1><div><p>{{.Content}}</p></div>")
  if err != nil {
	panic(err)
  }
  err = tmpl.Execute(os.Stdout, post)
  if err != nil {
	panic(err)
  }
}

Pipelines

This is a way of chaining operations together in a template.

  // Pass argument to function .SaySomething(text string)
  {{ .SaySomething " Hello "}}

  // Same as
  {{ " Hello " | .SaySomething }}

  // And
  {{ " Hello " | .SaySomething | printf "%s %s" " World"  }}

Looping

I have not checked the code and just wanted an awareness of the concept. I think I would prefer to format on the client but this could be useful when connecting to other reporting system. No surprises but here is an example. Here is the template

<!doctype html>
<head>
</head>
<body style="height: 100%;" }>
<div style="background: #6a7d87; width: 100%; z-index: 100; top: 0;
  left: 0; box-shadow: 0 1px 20px transparentize($second-color, 0.5);">
</div>

<div style="font-size: 1.5rem; font-weight: bold; color: #212529; display: block; font-family: Roboto, 'Helvetica Neue', sans-serif;font-stretch: normal; font-weight: bold; text-align: left; padding: .75rem 1.25rem; background-color: rgba(0,0,0,.03); border-bottom: 1px solid rgba(0,0,0,.125); ">
Product Summary Report
</div>
  <table style="width: 100%; height: 100%; margin-top: .5em;">
    <tr>
      <th>Row</th>
      <th>Product Name</th>
      <th>Quantity On Hand</th>
    </tr>
    {{range $index, $element := .}}
       {{if mod $index 2}} <tr style="background:#6a7d87;"> {{else}} 
    <tr> {{end}}
      <td>
        {{$index}}
      </td>
      <td>
	{{.ProductName}}
      </td>
      <td>
	{{.QuantityOnHand}}
      </td>
     </tr>
     {{end}}
    </table>

</body>
</html>

And the code for the template demonstrating looping.

    t := template.New("report.gotmpl").Funcs(template.FuncMap{"mod": func(i, x int) bool { return i%x == 0 }})
    t, err = t.ParseFiles(path.Join("templates", "report.gotmpl"))
    if err != nil {
	log.Print(err)
	w.WriteHeader(http.StatusInternalServerError)
	return
    }

    w.Header().Set("Content-Disposition", "Attachment")
    var tpl bytes.Buffer
    err = t.Execute(&tpl, products)

Functions

There are functions just like ejs and pug. The ones listed and here is the link https://pkg.go.dev/text/template#hdr-Functions

*and {{if and true true true}} {{end}}
*or  {{if or true false true}} {{end}}
*index {{index . 1}}
*len {{len .}}
*not {{if not false}}
*print, printf, println {{println "hey"}}

Custom Functions

We can write our own function but they must return a single value or value,err.

// Usage
tmpl := "{{range &index, $element := .}}{{if mod $index 2}}{{.}}{{end}}}{{end}}}"

// Declare
fm := template.FuncMap{"mod": func(i,j int) bool {return i%} == 0 }}
// Add to Template
tmpl, _ := template.New("tmplt").Funcs(fm).Parse(tmpl)