Introduction#

GitHub this note shows

  • Getting started with Go
  • Create a simple web app
  • Bedrock streaming response
  • Upload file
  • Server static file and folder

Install Go#

First, download go from HERE. For Linux, we can use below command to download a version of Go

wget https://go.dev/dl/go1.21.5.linux-amd64.tar.gz

Second, extract

tar -xvzf go1.21.5.linux-amd64.tar.gz

Next update the PATH environment

echo PATH:PATH_TO_YOUR_GO/go/bin/go:PATH >> ~/.bashrc

Finally, check go version

go version

Hello World#

Let create a new folder called hello

mkdir helloworld

Go into the folder helloworld and init a new go module

go module init hellomodule

Then create a main.go, the project structure look like this

|--hello
|--go.mod
|--go.sum
|--main.go

Content of main.go

package main
import (
"fmt"
)
func main() {
fmt.Println("Hello World")
}

Run the code

go run main.go

HTTP Server#

Create a http server, server mux and serve a static html file

package main
import (
"fmt"
"net/http"
"time"
)
func main () {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// fmt.Fprintf(w, "Hello Minh Tran")
http.ServeFile(w, r, "index.html")
})
server := &http.Server{
Addr: ":3000",
Handler: mux,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
server.ListenAndServe()
}

The html file

HTML File
<html>
<head>
<style>
:root {
box-sizing: border-box;
}
*,
::before,
::after {
box-sizing: inherit;
}
.container {
width: 80%;
margin: auto;
/* background-color: antiquewhite; */
}
.question-form {
position: relative;
}
button {
cursor: pointer;
background-color: orange;
padding: 1em;
padding-left: 1.5em;
padding-right: 1.5em;
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 1em;
}
.text-area {
background-color: azure;
margin-top: 1em;
}
.text-input {
background-color: aquamarine;
width: 100%;
padding: 1em;
font-size: large;
}
</style>
</head>
<body>
<div class="container">
<div class="question-form">
<form>
<input class="text-input" type="text" id="text-input" />
</form>
<button id="submit">Submit</button>
</div>
<div class="text-area">
<p id="story-output"></p>
</div>
</div>
<script>
const storyOutput = document.getElementById('story-output')
const callBedrockStream = async () => {
storyOutput.innerText = ''
const topic = document.getElementById('text-input').value
if (topic) {
try {
const response = await fetch('/bedrock-stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ topic: topic })
})
console.log(response)
const reader = response.body.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) {
break
}
const text = decoder.decode(value)
console.log(text)
storyOutput.innerText += text
}
} catch (error) {
console.log(error)
}
} else {
console.log('Please enter question ...')
}
}
document
.getElementById('submit')
.addEventListener('click', callBedrockStream)
</script>
</body>
</html>

More simple, we can use the default http server and default mux

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello")
})
http.ListenAndServe(":3000", nil)

Template and PostgreSQL#

  • Read a table in postgresql
  • Create a template html
  • Pass data to the template
  • Responsive frontend

First update the import

import (
"bytes"
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"text/template"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/bedrockruntime"
"github.com/aws/aws-sdk-go-v2/service/bedrockruntime/types"
"github.com/aws/aws-sdk-go-v2/service/s3"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)

Create a static serving folder for images link

mux.Handle("/demo/", http.StripPrefix("/demo/", http.FileServer(http.Dir("./static"))))

Let query the table in postresql

type Book struct {
ID uint
Title string
Author string
Amazon string
Image string
Description string
}
func getBooks(db *gorm.DB) []Book {
var books []Book
db.Limit(10).Find(&books)
for _, book := range books {
fmt.Println(book.Title)
}
return books
}

Let query a list of book and pass to a template

mux.HandleFunc("/postgresql", func(w http.ResponseWriter, r *http.Request) {
// query a list of book []Book
books := getBooks(db)
// load template
tmpl, error := template.ParseFiles("./static/book-template.html")
if error != nil {
fmt.Println(error)
}
// pass data to template and write to writer
tmpl.Execute(w, books)
})

The frontend templaet

<html>
<head>
<link
href="https://d2cvlmmg8c0xrp.cloudfront.net/web-css/review-book.css"
rel="stylesheet"
/>
</head>
<body class="bg-gray-100">
{{range $book:= .}}
<div class="mx-auto max-w-5xl">
<div class="md:flex gap-x-5 flex-row mb-8">
<div class="ml-4 bg-white flex-auto w-full">
<h4 class="font-bold mb-8">{{ $book.Title }}</h4>
<h4 class="font-bold mb-8">{{ $book.Author }}</h4>
<img
src="/demo/{{ $book.Image }}"
alt="book-image"
class="float-left h-auto w-64 mr-6"
/>
<p>{{ $book.Description}}</p>
</div>
</div>
</div>
{{end}}
</body>
</html>

This is full responsive fontend

responseive html
<html>
<head>
<style>
.body {
background-color: antiquewhite;
}
.container {
max-width: 800px;
margin-left: auto;
margin-right: auto;
}
.title {
font: bold;
margin-bottom: 8px;
}
.image {
float: left;
height: auto;
width: 128px;
margin-right: 6px;
}
.card {
margin-left: 4px;
margin-right: 4px;
background-color: white;
width: 100%;
}
.grid {
display: grid;
row-gap: 10px;
column-gap: 10px;
grid-template-columns: repeat(1, minmax(0, 1fr));
}
@media (min-width: 35em) {
.grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
</style>
</head>
<body class="body">
<div class="container">
<div class="grid">
{{range $book:= .}}
<div class="card">
<h4 class="title">{{ $book.Image}}</h4>
<h4 class="title">{{ $book.Author }}</h4>
<img src="/demo/{{ $book.Image }}" alt="book-image" class="image" />
<p>
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Rem
quaerat quas corrupti cum blanditiis, sint non officiis minus
molestiae culpa consectetur ex voluptatibus distinctio ipsam.
Possimus sint voluptatum at modi! Lorem ipsum, dolor sit amet
consectetur adipisicing elit. Alias dolore soluta error adipisci
eius pariatur laborum sed impedit. Placeat minus aut perspiciatis
dolor veniam, dolores odio sint eveniet? Numquam, tenetur! Lorem
ipsum dolor sit amet consectetur adipisicing elit. Earum suscipit
porro animi! Ducimus maiores et non. Minima nostrum ipsa voluptas
assumenda consequuntur dicta reprehenderit numquam similique,
nesciunt officiis facere optio. {{ $book.Description}}
</p>
</div>
{{end}}
</div>
</div>
</body>
</html>

Upload File#

  • Upload form
  • Save file to webserver
  • Upload file to S3
  • Create a record in database

Let create a simple frontend which is a form for sending post request (multipart data) to server

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Upload File</title>
</head>
<body>
<form enctype="multipart/form-data" action="/upload" method="post">
<input type="file" name="myFile" />
<input type="submit" value="upload" />
</form>
</body>
</html>

Then create a handler at server to save the uploaded file

func uploadFile(w http.ResponseWriter, r *http.Request, db *gorm.DB) {
// maximum upload file of 10 MB files
r.ParseMultipartForm(10 << 20)
// Get handler for filename, size and heanders
file, handler, error := r.FormFile("myFile")
if error != nil {
fmt.Println("Error")
fmt.Println(error)
return
}
defer file.Close()
fmt.Printf("upload file %v\n", handler.Filename)
fmt.Printf("file size %v\n", handler.Size)
fmt.Printf("MIME header %v\n", handler.Header)
// upload file to s3
// _, error = s3Client.PutObject(context.TODO(), &s3.PutObjectInput{
// Bucket: aws.String("cdk-entest-videos"),
// Key: aws.String("golang/" + handler.Filename),
// Body: file,
// })
// if error != nil {
// fmt.Println("error upload s3")
// }
// Create file
dest, error := os.Create("./static/" + handler.Filename)
if error != nil {
return
}
defer dest.Close()
// Copy uploaded file to dest
if _, err := io.Copy(dest, file); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// create a record in database
db.Create(&Book{
Title: "Database Internals",
Author: "Hai Tran",
Description: "Hello",
Image: handler.Filename,
})
fmt.Fprintf(w, "Successfully Uploaded File\n")
}

Bedrock Stream#

Let call bedrock and resopnse stream to client

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"text/template"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/bedrockruntime"
"github.com/aws/aws-sdk-go-v2/service/bedrockruntime/types"
"github.com/aws/aws-sdk-go-v2/service/s3"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)
type Request struct {
Prompt string `json:"prompt"`
MaxTokensToSample int `json:"max_tokens_to_sample"`
Temperature float64 `json:"temperature,omitempty"`
TopP float64 `json:"top_p,omitempty"`
TopK int `json:"top_k,omitempty"`
StopSequences []string `json:"stop_sequences,omitempty"`
}
type Response struct {
Completion string `json:"completion"`
}
type HelloHandler struct{}
type Query struct {
Topic string `json:"topic"`
}
// promt format
const claudePromptFormat = "\n\nHuman: %s\n\nAssistant:"
// bedrock runtime client
var brc *bedrockruntime.Client
// init bedorck credentials connecting to aws
func init() {
region := os.Getenv("AWS_REGION")
if region == "" {
region = "us-east-1"
}
cfg, err := config.LoadDefaultConfig(context.Background(), config.WithRegion(region))
if err != nil {
log.Fatal(err)
}
brc = bedrockruntime.NewFromConfig(cfg)
s3Client = s3.NewFromConfig(cfg)
}
func bedrock(w http.ResponseWriter, r *http.Request) {
var query Query
var message string
// parse mesage from request
error := json.NewDecoder(r.Body).Decode(&query)
if error != nil {
message = "how to learn japanese as quick as possible?"
panic(error)
}
message = query.Topic
fmt.Println(message)
prompt := "" + fmt.Sprintf(claudePromptFormat, message)
payload := Request{
Prompt: prompt,
MaxTokensToSample: 2048,
}
payloadBytes, error := json.Marshal(payload)
if error != nil {
fmt.Fprintf(w, "ERROR")
// return "", error
}
output, error := brc.InvokeModelWithResponseStream(
context.Background(),
&bedrockruntime.InvokeModelWithResponseStreamInput{
Body: payloadBytes,
ModelId: aws.String("anthropic.claude-v2"),
ContentType: aws.String("application/json"),
},
)
if error != nil {
fmt.Fprintf(w, "ERROR")
// return "", error
}
for event := range output.GetStream().Events() {
switch v := event.(type) {
case *types.ResponseStreamMemberChunk:
//fmt.Println("payload", string(v.Value.Bytes))
var resp Response
err := json.NewDecoder(bytes.NewReader(v.Value.Bytes)).Decode(&resp)
if err != nil {
fmt.Fprintf(w, "ERROR")
// return "", err
}
fmt.Println(resp.Completion)
fmt.Fprintf(w, resp.Completion)
if f, ok := w.(http.Flusher); ok {
f.Flush()
} else {
fmt.Println("Damn, no flush")
}
case *types.UnknownUnionMember:
fmt.Println("unknown tag:", v.Tag)
default:
fmt.Println("union is nil or unknown type")
}
}
}

And add a handler to bedrock

mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "./static/bedrock.html")
})

The frontend for chatbot using bedrock

<html>
<head>
<style>
:root {
box-sizing: border-box;
}
*,
::before,
::after {
box-sizing: inherit;
}
.container {
width: 80%;
margin: auto;
/* background-color: antiquewhite; */
}
.question-form {
position: relative;
}
button {
cursor: pointer;
background-color: orange;
padding: 1em;
padding-left: 1.5em;
padding-right: 1.5em;
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 1em;
}
.text-area {
background-color: azure;
margin-top: 1em;
}
.text-input {
background-color: aquamarine;
width: 100%;
padding: 1em;
font-size: large;
}
</style>
</head>
<body>
<div class="container">
<div class="question-form">
<form>
<input class="text-input" type="text" id="text-input" />
</form>
<button id="submit">Submit</button>
</div>
<div class="text-area">
<p id="story-output"></p>
</div>
</div>
<script>
const storyOutput = document.getElementById('story-output')
const callBedrockStream = async () => {
storyOutput.innerText = ''
const topic = document.getElementById('text-input').value
if (topic) {
try {
const response = await fetch('/bedrock-stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ topic: topic })
})
console.log(response)
const reader = response.body.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) {
break
}
const text = decoder.decode(value)
console.log(text)
storyOutput.innerText += text
}
} catch (error) {
console.log(error)
}
} else {
console.log('Please enter question ...')
}
}
document
.getElementById('submit')
.addEventListener('click', callBedrockStream)
</script>
</body>
</html>

Stream Reseponse#

Let create streaming response using flush

package main
import (
"fmt"
"net/http"
"time"
)
type HelloHandler struct {}
func (hh HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
for i:= 0; i < 10; i++ {
fmt.Fprintf(w, "sending first line of data \n")
if f, ok := w.(http.Flusher); ok {
f.Flush()
} else {
fmt.Println("Damn, no flush");
}
time.Sleep(1000 * time.Millisecond)
}
}
func main() {
server := http.Server{
Addr: ":3000",
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
Handler: HelloHandler{},
}
err := server.ListenAndServe()
if err != nil {
fmt.Println("error")
}
}

Let create streaming response using echo framework

package main
import (
"encoding/json"
"net/http"
"time"
"github.com/labstack/echo/v4"
)
type (
Geolocation struct {
Altitude float64
Latitude float64
Longitude float64
}
)
var (
locations = []Geolocation{
{-97, 37.819929, -122.478255},
{1899, 39.096849, -120.032351},
{2619, 37.865101, -119.538329},
{42, 33.812092, -117.918974},
{15, 37.77493, -122.419416},
}
)
func main_echo() {
e := echo.New()
e.GET("/", func(c echo.Context) error {
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
c.Response().WriteHeader(http.StatusOK)
enc := json.NewEncoder(c.Response())
for _, l := range locations {
if err := enc.Encode(l); err != nil {
return err
}
c.Response().Flush()
time.Sleep(1 * time.Second)
}
return nil
})
e.Logger.Fatal(e.Start(":1323"))
}

JavaScript Client#

Let write some javascript code to send request to server and update streaming response text to browser

const storyOutput = document.getElementById('story-output')
const callBedrockStream = async () => {
storyOutput.innerText = ''
const topic = document.getElementById('text-input').value
if (topic) {
try {
const response = await fetch('/bedrock-stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ topic: topic })
})
console.log(response)
const reader = response.body.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) {
break
}
const text = decoder.decode(value)
console.log(text)
storyOutput.innerText += text
}
} catch (error) {
console.log(error)
}
} else {
console.log('Please enter question ...')
}
}
document.getElementById('submit').addEventListener('click', callBedrockStream)

Docker#

Stop and remove all container images

docker stop $(docker ps -a -q)
docker rmi -f $(docker images -aq)

Delete all existing images

docker rmi -f $(docker images -aq)

Build and tag a new container image

docker build --tag entest .

Stop all process running on port 3000

sudo kill -9 $(lsof -i:3000 -t)

AppRunner#

Let create app runner for hosting the go app. First, let create an autoscaling

import { Stack, StackProps, aws_apprunner, aws_iam } from 'aws-cdk-lib'
import { Effect } from 'aws-cdk-lib/aws-iam'
import { Construct } from 'constructs'
export class AppRunnerStack extends Stack {
constructor(scope: Construct, id: string, props: StackProps) {
super(scope, id, props)
const role = new aws_iam.Role(this, 'RoleForAppRunnerPullEcr', {
assumedBy: new aws_iam.ServicePrincipal('build.apprunner.amazonaws.com'),
roleName: 'RoleForAppRunnerPullEcr'
})
role.addToPolicy(
new aws_iam.PolicyStatement({
effect: Effect.ALLOW,
resources: ['*'],
actions: ['ecr:*']
})
)
const autoscaling = new aws_apprunner.CfnAutoScalingConfiguration(
this,
'AutoScalingForGoApp',
{
autoScalingConfigurationName: 'AutoScalingForGoApp',
// min number instance
minSize: 1,
// max number instance
maxSize: 10,
// max concurrent request per instance
maxConcurrency: 100
}
)
const apprunner = new aws_apprunner.CfnService(this, 'GoAppService', {
serviceName: 'GoAppServiceDemo',
sourceConfiguration: {
authenticationConfiguration: {
accessRoleArn: role.roleArn
},
autoDeploymentsEnabled: false,
imageRepository: {
imageIdentifier:
'11122223333444.dkr.ecr.us-east-1.amazonaws.com/go-app:latest',
imageRepositoryType: 'ECR',
imageConfiguration: {
port: '3000',
runtimeEnvironmentVariables: [
{
name: 'BUCKET',
value: 'demo'
}
]
// startCommand: "",
}
}
},
instanceConfiguration: {
cpu: '1 vCPU',
memory: '2 GB'
},
observabilityConfiguration: {
observabilityEnabled: false
},
autoScalingConfigurationArn: autoscaling.ref
})
apprunner.addDependency(autoscaling)
}
}

UserData#

UserData can be used when launching a new EC2 instance, so it will install GO and clone the repository for the web app

cd /home/ec2-user/
wget https://go.dev/dl/go1.21.5.linux-amd64.tar.gz
tar -xvf go1.21.5.linux-amd64.tar.gz
echo 'PATH=/home/ec2-user/go/bin/go:$PATH' >> ~/.bashrc
wget https://github.com/cdk-entest/swinburne-dn-cos20019/archive/refs/heads/main.zip
unzip main
cd swinburne-dn-cos20019-main/
go mod tidy
go run main.go

Reference#