GO Web Services: Difference between revisions
(10 intermediate revisions by the same user not shown) | |||
Line 366: | Line 366: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
=Structuring Code for Services= | ==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 | 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 | ||
<syntaxhighlight lang="go"> | <syntaxhighlight lang="go"> | ||
Line 399: | Line 399: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
==Usage== | ===Usage=== | ||
So in the controller, we are using MVC, we can check the header to provide different views. | So in the controller, we are using MVC, we can check the header to provide different views. | ||
<syntaxhighlight lang="go"> | <syntaxhighlight lang="go"> | ||
Line 414: | Line 414: | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
==Log== | |||
Example of using the zap logger. | |||
<syntaxhighlight lang="go"> | |||
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...) | |||
} | |||
</syntaxhighlight> | |||
==OAuth== | |||
===Introduction=== | |||
Another example of OAUTH but this time in GO. This is what we are going to implement.<br> | |||
[[File:OAuth GO.png| 600px]]<br> | |||
We are going to be using DDD or Domain Driven Design with the Clean Architecture<br> | |||
[[File:DDD Domain Driven Design.png|500px]] | |||
===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.<br> | |||
[[File:OAuth Project.png|300px]] | |||
===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.<br> | |||
[[File:OAuth OID.png|500px]] | |||
=Persisting Data= | =Persisting Data= |
Latest revision as of 06:27, 29 September 2021
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.
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.
We are going to be using DDD or Domain Driven Design with the Clean Architecture
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.
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.
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)