![]() |
|
![]() |
Golang powers some of the most relevant IT projects of the last decade, such as Docker or Kubernetes. A lot of this success can be traced back to its simplicity, efficiency, tooling and developer experience.
In this workshop, we assume that the participants are already fluent with another programming language, such as Java, Python or C#. We will cover the go basics, walk along some real-world examples and build some REST/GraphQL and Data Analysis usecases.
Why should you look at the GO programming language? Go is an open source programming language. But go takes a lot of getting used to and is reduced to the essentials. You can build anything from small helper scripts for pipelines to microservices to highly complex monoliths with GO. The advantages of Go are a strong standardization of the tools, a high applicability of the binaries and a high performance of the build process. We will discuss further advantages of Go in more detail.
brew install go
go build
the go compilergo fmt
format your go codego get
load/installs dependenciesgo run
compiles and executes go code directlygo test
compiles and executes go testcasesgo mod [download|edit|graph|init|tidy|vendor|verify|why]
manages modulesgo generate
generates code from source filesgo version
determines the go version usedgo vet
finds potential errors in the applicationgo bug
report errorgo doc
standard tool to view documentation and source codego env
set and read go environment variablesgo fix
makes adjustments to new go versionsgo tool
lists toolsbreak, case, chan, const, continue
default, defer, else, fallthrough
for, func, go, goto, if, import
interface, map, package, range
return, select, struct, switch
type, var
func add(x int, y int) int {
return x + y
}
func add(x, y int) int {
return x + y
}
func swap(x, y string) (string, string) {
return y, x
}
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return
}
func process(sample int, fn func(int) int) int {
return fn(sample)
}
func adder(term int) func(a int) int {
return func(a int) int { return a + term }
}
func increment(term int) int {
return adder(1)(term)
}
func main() {
sample := 23
fmt.Println(process(sample, increment),
process(sample, adder(100)))
}
var hot, wet, far bool
var tool string
var x int
var x, y int = 1, 2
var hot, wet, tool = true, false, "screwdriver"
x := 3
hot, wet, tool := true, false, "screwdriver"
for i := 0; i < 10; i++ {
sum += i
}
sum := 1
for sum < 1000; {
sum += sum
}
for {
}
if x < 0 {
return "something"
}
if a < b {
return a
} else {
return b
}
os := runtime.GOOS
switch os {
case "darwin":
fmt.Println("OS X.")
case "linux":
fmt.Println("Linux.")
default:
// freebsd, openbsd, plan9, windows...
fmt.Printf("%s.\n", os)
}
func main() {
defer fmt.Println("world")
fmt.Println("hello")
}
...
func main() {
file, _ := os.Open("README.md")
defer file.Close()
buffer := make([]byte, 1024)
bytesRead, _ := file.Read(buffer)
fmt.Printf("Content: %s\n", buffer[:bytesRead])
}
...
type Vertex struct {
X int
Y int
}
func main() {
v := Vertex{1, 2}
v.X = 4
fmt.Println(v.X)
}
var tools [2]string
a[0] = "screwdriver"
a[1] = "hammer"
primes := [6]int{2, 3, 5, 7, 11, 13}
primes := [6]int{2, 3, 5, 7, 11, 13}
var s []int = primes[1:4]
tour of go
is a good starting pointThis task is supposed to demonstrate basic console I/O in Go.
The program should print the text Hello World! This is Go.
to the standard output.
// main.go
package main
import "fmt"
func main() {
fmt.Println("Hello World! This is Go.")
}
Run a go file directly
go run main.go
Hello World! This is Go.
Compile and run a go file:
go build main.go
./main
Hello World! This is Go.
GOOS=linux GOARCH=arm go build main.go
$ file main
main: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked
Compile for other operating systems:
GOOS=windows go build main.go
$file main
main: Mach-O 64-bit executable x86_64
windows/amd64
binary on darwin/arm64
GOOS=windows GOARCH=amd64 go build main.go
darwin/arm64
binary on windows/amd64
$Env:GOOS="darwin"; $Env:GOARCH="amd64"; go build main.go
$ ./main
Hello World! This is Go.
$ git clone git@github.com:opentofu/opentofu.git
GOPATH
variables, go will install packages here$ go env GOPATH
/Users/grohmio/go
GOBIN
path to your PATH
, so binaries build by go can be found, if GOBIN
=="" default is ${GOPATH}/bin
#.zshrc
...
# go binaries
export PATH="/Users/grohmio/go/bin:$PATH"
CGO_ENABLED=1
$ cd opentofu/
$ go build -o /Users/grohmio/go/bin/ ./cmd/tofu
go: downloading github.com/apparentlymart/go-shquot v0.0.1
go: downloading github.com/mitchellh/cli v1.1.5
go: downloading github.com/mattn/go-shellwords v1.0.4
go: downloading github.com/hashicorp/terraform-svchost v0.1.1
$ tofu -h
Usage: tofu [global options] <subcommand> [args]
The available commands for execution are listed below.
The primary workflow commands are given first, followed by
less common or more advanced commands.
Main commands:
init Prepare your working directory for other commands
validate Check whether the configuration is valid
plan Show changes required by the current configuration
apply Create or update infrastructure
destroy Destroy previously-created infrastructure
opentofu
toolThis task is supposed to demonstrate Docker image build for Go applications.
The program should print the text Hello World! This is Go.
to the standard output in a docker container.
// main.go
package main
import "fmt"
func main() {
fmt.Println("Hello World! This is Go.")
}
# Dockerfile
# build stage
FROM golang:1.17.6-alpine AS build
RUN mkdir -p /app
WORKDIR /app
# app src
COPY . .
RUN GOOS=linux GOARCH=amd64 go build -o /bin/app main.go
# result stage
FROM scratch
COPY --from=build /bin/app /bin/app
ENTRYPOINT ["/bin/app"]
$ docker build -t hello-image .
$ docker images hello-image
REPOSITORY TAG IMAGE ID CREATED SIZE
hello-image latest d23a3532deaf 4 minutes ago 1.77MB
$ docker run --rm -it --name hello-con hello-image
Hello World! This is Go.
In this task, you will learn about writing tests in Go by developing a prime tester in TDD-style.
main.go
file// main.go
package main
func main() {
}
// main_test.go
package main
import "testing"
func TestPrimeCheckerTheNaiveWay(t *testing.T) {
t.Run("should return FALSE when no prime number given", func(t *testing.T) {
if IsPrime(4) == true {
t.Fatal("Reported IsPrime=true for 4")
}
})
t.Run("should return TRUE when prime number given", func(t *testing.T) {
if IsPrime(7) == false {
t.Fatal("Reported IsPrime=false for 7")
}
})
}
go test main.go main_test.go
./main_test.go:9:6: undefined: IsPrime
./main_test.go:15:6: undefined: IsPrime
./main_test.go:35:14: undefined: IsPrime
FAIL command-line-arguments [build failed]
FAIL
The test will fail, because the function IsPrime
is yet to be implemented.
We will learn a better way to run Go tests later
// main.go
// ...
import (
"math"
)
func IsPrime(value int) (result bool) {
for i := 2; i <= int(math.Floor(math.Sqrt(float64(value)))); i++ {
if value %i == 0 {
return false
}
}
return value > 1
}
// ...
go test main.go main_test.go
ok command-line-arguments 0.102s
Cut down redundancy in your tests:
// main_test.go
// ...
func TestPrimeCheckerTableDriven(t *testing.T) {
cases := []struct {
input int
expectedResult bool
}{
{1, false},
{2, true},
{3, true},
{4, false},
{7, true},
}
for _, e := range cases {
t.Run("Should return expected result ", func(t *testing.T) {
result := IsPrime(e.input)
if result != e.expectedResult {
t.Fatalf("Unexpected Result input=%d expected=%t actual=%t",
e.input,
e.expectedResult,
result)
}
})
}
}
$ go test -cover main.go main_test.go
ok command-line-arguments 0.191s coverage: 100.0% of statements
go test -coverprofile=coverage.out main.go main_test.go
mode: set
/Users/grohmio/repos/cc/gophers/golang-for-developers/examples/03-prime-checker/main.go:7.14,8.2 0 0
/Users/grohmio/repos/cc/gophers/golang-for-developers/examples/03-prime-checker/main.go:10.39,11.67 1 1
/Users/grohmio/repos/cc/gophers/golang-for-developers/examples/03-prime-checker/main.go:16.2,16.18 1 1
/Users/grohmio/repos/cc/gophers/golang-for-developers/examples/03-prime-checker/main.go:11.67,12.19 1 1
/Users/grohmio/repos/cc/gophers/golang-for-developers/examples/03-prime-checker/main.go:12.19,14.4 1 1
coverage
tool to generate a graphical reportgo tool cover -html=coverage.out main.go main_test.go
In this task we want to create different modules in GO. Every single module uses a different logging mechanism. We use logrus
, zap
and golog
in our example.
Lets write a new file and import logrus.
//main.go
package main
import (
log "github.com/sirupsen/logrus"
)
func main() {
log.WithFields(log.Fields{
"logger": "logrus",
}).Info("Hello from logrus")
}
$ go mod init grohm.io/hello-logrus/v2
go.mod
will be generatedmodule grohm.io/hello-logrus/v2
go 1.17
go.sum
is created$ go get github.com/sirupsen/logrus
main.go
$ go run main.go
INFO[0000] Hello from logrus logger=logrus
Now lets write a new file and import zap.
//main.go
package main
import (
"go.uber.org/zap"
"time"
)
func main() {
logger, _ := zap.NewProduction()
defer logger.Sync()
logger.Info("Hello from zap.",
// Structured context as strongly typed Field values.
zap.String("logger", "zap"),
zap.Duration("backoff", time.Second),
)
}
main.go
$ go mod init grohm.io/hello-zap/v2
$ go get -u go.uber.org/zap
$ go run main.go
{"level":"info","ts":1644345019.7559521,"caller":"02-hello-zap/main.go:11","msg":"Hello from zap.","logger":"zap","backoff":1}
Dockerfile
for modules# Dockerfile modules
# build stage
FROM golang:1.17.6-alpine AS build
RUN mkdir -p /app
WORKDIR /app
# build src
COPY go.mod .
COPY go.sum .
RUN go mod download
# app src
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /bin/app
# result stage
FROM scratch
COPY --from=build /bin/app /bin/app
ENTRYPOINT ["/bin/app"]
# Dockerfile
# ...
RUN GOOS=linux GOARCH=amd64 go build -o /bin/app main.go
# ...
# Dockerfile modules
# ...
# build src
COPY go.mod .
COPY go.sum .
RUN go mod download
# ...
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /bin/app
# ...
$ docker build -t hello-zap-image .
$ docker run --rm -it --name hello-zap-con hello-zap-image
{"level":"info","ts":1644350565.6308103,"caller":"app/main.go:11","msg":"Hello from zap.","logger":"zap","backoff":1}
Now lets write a new file and import golog.
//main.go
package main
import ( "github.com/kataras/golog" )
func main() {
golog.SetLevel("debug")
golog.Println("This is a raw message, no levels, no colors.")
golog.Info("This is an info message, with colors (if the output is terminal)")
golog.Warn("This is a warning message")
golog.Error("This is an error message")
golog.Debug("This is a debug message")
golog.Fatal(`Fatal will exit no matter what,
but it will also print the log message if logger's Level is >=FatalLevel`)
}
$ go mod init grohm.io/hello-golog/v2
$ go get -u github.com/kataras/golog
$ go run main.go
2022/02/09 19:12 This is a raw message, no levels, no colors.
[INFO] 2022/02/09 19:12 This is an info message, with colors (if the output is terminal)
[WARN] 2022/02/09 19:12 This is a warning message
[ERRO] 2022/02/09 19:12 This is an error message
[DBUG] 2022/02/09 19:12 This is a debug message
[FTAL] 2022/02/09 19:12 Fatal will exit no matter what,
but it will also print the log message if logger's Level is >=FatalLevel
exit status 1
In this task, we want to explicitly add all dependencies to one of our modules to bypass external dependencies in the build process. We use the vendoring feature of go for this.
//main.go
package main
import (
"go.uber.org/zap"
"time"
)
func main() {
logger, _ := zap.NewProduction()
defer logger.Sync()
logger.Info("Hello from zap.",
zap.String("logger", "zap"),
zap.Duration("backoff", time.Second),
)
}
$ go mod init grohm.io/hello-zap-vendor
$ go get go.uber.org/zap
$ go mod vendor
.
├── go.mod
├── go.sum
├── main.go
└── vendor
├── go.uber.org
│ ├── atomic
│ ├── multierr
│ └── zap
└── modules.txt
In this task, we want to build and serve some golang documentation. https://github.com/go-gurus/go_tour_src/tree/main/documentation
$ mkdir documentation
$ cd documentation
$ go mod init grohm.io/documentation
main.go
with some documentation// Package documentation provides a prime number check function and is a documentation showcase.
package documentation
func main() {
}
// prime_checker/prime_checker.go -- <REMOVE THIS COMMENT>
// Package prime_checker provides a prime number check function.
package prime_checker
import (
"math"
)
// IsPrime check if int value is a prime number.
// It returns a boolean, true if it is prime number, false if not.
func IsPrime(value int) (result bool) {
for i := 2; i <= int(math.Floor(math.Sqrt(float64(value)))); i++ {
if value%i == 0 {
return false
}
}
return value > 1
}
$ go install -v golang.org/x/tools/cmd/godoc@latest
$ ~/go/bin/godoc -http :6060
you should see something like this
// prime_checker/prime_checker_example_test.go -- <REMOVE THIS COMMENT>
package prime_checker
import "fmt"
func ExampleIsPrime() {
res := IsPrime(7)
fmt.Println(res)
//Output: true
}
//Output: false
and try again$ go test ./...
$ ~/go/bin/godoc -http :6060
you should see something like this
In this task we want to create a program that initializes a memory intensive variable. In a refactoring we want to introduce pointers and clarify their advantages.
// main.go
func init_image(image [][]int) [][]int {
for key_1, val_1 := range image {
for key_2, _ := range val_1 {
image[key_1][key_2] = rand.Intn(256)
}
}
return image
}
main
method create a slice matrix and define the ranges, finally use the init method and print result// main.go
// ...
func main() {
image := make([][]int, 1000)
for i := 0; i < 1000; i++ {
image[i] = make([]int, 1000)
}
image = init_image(image)
fmt.Println(image)
}
Now let's see how pointers improve the program and use less memory.
*T
is a pointer to a T
value, zero value is nilvar p *int
&
operator generates a pointer to its operandi := 42
p = &i
*
operator denotes the pointer's underlying valuefmt.Println(*p) // read i through the pointer p
*p = 21 // set i through the pointer p
// main.go
package main
import (
"fmt"
"math/rand"
)
func init_image(image_pointer *[][]int) *[][]int {
for key_1, val_1 := range *image_pointer {
for key_2, _ := range val_1 {
(*image_pointer)[key_1][key_2] = rand.Intn(256)
}
}
return image_pointer
}
func main() {
image := make([][]int, 1000)
for i := 0; i < 1000; i++ {
image[i] = make([]int, 1000)
}
pointer := &image
pointer = init_image(pointer)
fmt.Println(*pointer)
}
// main.go
package main
import (
"fmt"
"math/rand"
)
func init_image(image_pointer *[][]int) {
for key_1, val_1 := range *image_pointer {
for key_2, _ := range val_1 {
(*image_pointer)[key_1][key_2] = rand.Intn(256)
}
}
}
func main() {
image := make([][]int, 1000)
for i := 0; i < 1000; i++ {
image[i] = make([]int, 1000)
}
init_image(&image)
fmt.Println(image)
}
In this task, we want to use interfaces to build a go service that is able to use logrus, zap and golog as logger.
The service is configured via environment variable LOGGER=[logrus|zap|golog]
// main.go
package main
type logInterface interface {
Info(string)
Error(string)
}
// ...
golog_facade/
golog_facade.go
logrus_facade/
logrus_facade.go
zap_facade/
zap_facade.go
main.go
logrus
logging facadeLogrusStruct
has the two methods that are needed to fulfill the interface logInterface
// logrus_facade/logrus_facade.go
package logrus_facade
import ("github.com/sirupsen/logrus")
type LogrusStruct struct{}
func (logger LogrusStruct) Info(msg string) {
logrus.Debug(msg + " (logrus)")
}
func (logger LogrusStruct) Error(msg string) {
logrus.Error(msg + " (logrus)")
}
golog
logging facade// golog_facade/golog_facade.go
package golog_facade
import ("github.com/kataras/golog")
type GologStruct struct{}
func (s GologStruct) Info(msg string) {
golog.Debug(msg + " (golog)")
}
func (s GologStruct) Error(msg string) {
golog.Error(msg + " (golog)")
}
zap
logging facade// zap_facade/zap_facade.go
package zap_facade
import ("go.uber.org/zap")
type ZapStruct struct { logger zap.Logger}
func (s ZapStruct) Info(msg string) {
defer s.logger.Sync()
s.logger.Info(msg + " (zap)")
}
func (s ZapStruct) Error(msg string) {
defer s.logger.Sync()
s.logger.Debug(msg + " (zap)")
}
func NewZapStruct() ZapStruct {
logger, _ := zap.NewProduction()
result := ZapStruct{*logger}
return result
}
// main.go
// ...
func resolveLogger() logInterface {
var result logInterface
if os.Getenv("LOGGER") == "logrus" {
result = logrus_facade.LogrusStruct{}
} else if os.Getenv("LOGGER") == "zap" {
result = zap_facade.NewZapStruct()
} else if os.Getenv("LOGGER") == "golog" {
result = golog_facade.GologStruct{}
} else {
fmt.Println("Unknown logger, please set $LOGGER envvar.")
}
return result
}
// main.go
// ...
var logFacade logInterface = resolveLogger()
func doSomething() {
logFacade.Info("I really dont care which logging tool is used to put this info")
time.Sleep(time.Second)
logFacade.Error("I really dont care which logging tool is used to put this error")
}
func main() { doSomething() }
$ go mod init grohm.io/interfaces/v2
$ go get github.com/sirupsen/logrus
$ go get -u go.uber.org/zap
$ go get -u github.com/kataras/golog
$ LOGGER=golog go run main.go
[INFO] 2022/02/12 20:20 i really dont care which logging tool is used to put this info (golog)
[ERRO] 2022/02/12 20:20 i really dont care which logging tool is used to put this error (golog)
$ LOGGER=logrus go run main.go
INFO[0000] i really dont care which logging tool is used to put this info (logrus)
ERRO[0001] i really dont care which logging tool is used to put this error (logrus)
$ LOGGER=zap go run main.go
{"level":"info","ts":1644693743.249532,"caller":"zap_facade/zap_facade.go:14","msg":"i really dont care which logging tool is used to put this info (zap)"}
{"level":"error","ts":1644693744.2508721,"caller":"zap_facade/zap_facade.go:19","msg":"i really dont care which logging tool is used to put this error (zap)",
"stacktrace":"grohm.io/interfaces/v2/zap_facade.ZapStruct.Error\n\t/Users/grohmio/repos/cc/gitlab/golang_workshop/examples/05_interfaces/zap_facade/zap_facade.go:19\nmain.doSomething\n\t/Users/grohmio/repos/cc/gitlab/golang_workshop/examples/05_interfaces/main.go:35\nmain.main\n\t/Users/grohmio/repos/cc/gitlab/golang_workshop/examples/05_interfaces/main.go:39\nruntime.main\n\t/usr/local/opt/go/libexec/src/runtime/proc.go:255"}
In this task we want to inject a mock to be able to test function calls on the interface.
$ go install github.com/golang/mock/mockgen@v1.6.0
$ go get github.com/golang/mock/gomock
$ mockgen -source=main.go -destination mock_main/mock_main.go
//./mock_main/mock_main.go
package mock_main
// ...
// MocklogInterface is a mock of logInterface interface.
type MocklogInterface struct {
ctrl *gomock.Controller
recorder *MocklogInterfaceMockRecorder
}
//...
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MocklogInterface) EXPECT() *MocklogInterfaceMockRecorder {
return m.recorder
}
// Error mocks base method.
func (m *MocklogInterface) Error(arg0 string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Error", arg0)
}
//..
func resolveLogger() logInterface
, change// main.go
// ...
func resolveLogger() logInterface {
// ...
// main.go
// ...
var resolveLogger = func() logInterface {
// ...
// main_test.go
package main
// ...
func Test_doSomething(t *testing.T) {
ctrl := gomock.NewController(t)
// mock the log facade in main
logFacadeMock := mock_main.NewMocklogInterface(ctrl)
// inject mock via mocked function
resolveLogger = func() logInterface {
return logFacadeMock
}
logFacade = resolveLogger()
// asserts that the first calls to Info() and Error() is passed the correct strings
// anything else will fail
logFacadeMock.EXPECT().Info("I really dont care which logging tool is used to put this info")
logFacadeMock.EXPECT().Error("I really dont care which logging tool is used to put this error")
doSomething()
}
$ LOGGER=logrus go test
PASS
ok grohm.io/interfaces/v2 1.444s
Let's talk about errors and error handling in Go.
Function returning an error:
func fail() error {
return fmt.Errorf("This did not work out.")
}
Check if an error occurred:
err := fail()
if err != nil {
panic(err)
}
An error has no effect unless actively handled
Use-cases for distinguishable errors:
var noStageNameProvided = errors.New("No Stage name provided")
var invalidStageNameProvidedError = errors.New("Invalid stage name provided")
func resolveService() (string, error) {
stageName := os.Getenv(stageEnvironmentKey)
switch stageName {
case "dev":
return "https://dev.fake", nil
case "staging":
return "https://stage.my.cloud", nil
case "":
return "UNKNOWN_ENVIRONMENT", noStageNameProvided
default:
return "UNKNOWN_ENVIRONMENT", invalidStageNameProvidedError
}
}
...
serviceUrl, err := resolveService()
if err != nil {
switch {
case errors.Is(err, noStageNameProvided):
fmt.Println("No stage name provided. Using default.")
serviceUrl = "https://default.my.cloud"
case errors.Is(err, invalidStageNameProvidedError):
panic(err)
}
}
Use-Cases:
The previous function with wrapped errors:
var noStageNameProvided = errors.New("No Stage name provided")
var invalidStageNameProvidedError = errors.New("Invalid stage name provided")
func resolveService() (serviceUrl string, err error) {
stageName := os.Getenv(stageEnvironmentKey)
serviceUrl = "UNKNOWN_ENVIRONMENT"
switch stageName {
case "dev":
serviceUrl = "https://dev.fake"
case "staging":
serviceUrl = "https://stage.my.cloud"
case "":
err = noStageNameProvided
default:
err = fmt.Errorf("%w . %s is not a known stageName",
invalidStageNameProvidedError, stageName)
}
return
}
// ...
Handling an invalid input
//...
serviceUrl, err := resolveService()
if err != nil {
switch {
case errors.Is(err, noStageNameProvided):
fmt.Println("No stage name provided. Using default.")
serviceUrl = "https://default.my.cloud"
case errors.Is(err, invalidStageNameProvidedError):
panic(err)
}
}
Output:
panic: Invalid stage name provided . Gophers! is not a known stageName
In this task, we want to use go routines and channels to build a simple go load balancer. The loadbalancer will spawn parallel go routines in the background, each of these routines will process a time intensive workload.
// main.go
package main
import ("fmt")
func SendDataToChannel(integerChannel chan int, value int) {
integerChannel <- value
}
func main() {
integerChannel := make(chan int)
go SendDataToChannel(integerChannel, 42)
value := <-integerChannel
fmt.Println(value)
}
➜ go run main.go
42
What is this program doing?
// main.go
package main
import (
"fmt"
"math/rand"
"strconv"
"time"
)
func sendDataToChannel(input chan int) {
for {
value := rand.Intn(50)
input <- value
time.Sleep(time.Duration(value))
}
}
// main.go
// ...
func processChannel(id int, input chan int, output chan string) {
for {
value, _ := <-input
result := value * 1000
time.Sleep(time.Duration(result))
output <- "worker: " + strconv.Itoa(id) + ", input: " + strconv.Itoa(value) + ", result: " + strconv.Itoa(result)
}
}
// main.go
// ...
func main() {
rand.Seed(time.Now().UnixNano())
input := make(chan int)
output := make(chan string)
go sendDataToChannel(input)
go processChannel(0, input, output)
go processChannel(1, input, output)
//...
for {
select {
case x, ok := <-output:
if ok {
fmt.Println(x)
} else {
fmt.Println("Channel closed!")
}
}
}
}
➜ go run main.go
worker: 8, input: 20, result: 20000
worker: 7, input: 14, result: 14000
worker: 5, input: 115, result: 115000
worker: 5, input: 16, result: 16000
worker: 3, input: 297, result: 297000
worker: 7, input: 232, result: 232000
worker: 6, input: 431, result: 431000
worker: 2, input: 719, result: 719000
worker: 0, input: 660, result: 660000
worker: 1, input: 682, result: 682000
worker: 9, input: 728, result: 728000
worker: 7, input: 389, result: 389000
worker: 8, input: 766, result: 766000
// main.go
func main() {
// ...
for i := 0; i < 10; i++ {
go processChannel(i, input, output)
}
go sendDataToChannel(input)
// ...
}
// main.go
// ...
func main() {
// ...
input := make(chan int)
output := make(chan string)
defer close(input)
defer close(output)
// ...
}
// main.go
func sendDataToChannel(input chan int) {
for i := 0; i < 100; i++ {
value := rand.Intn(50)
input <- value
time.Sleep(time.Duration(value))
}
}
// main.go
func processChannel(id int, input chan int, output chan string) {
for {
var result int
select {
case value := <-input:
result = value * 1000
output <- "worker: " + strconv.Itoa(id) + ", input: " + strconv.Itoa(value) + ", result: " + strconv.Itoa(result)
default:
// do nothing
}
time.Sleep(time.Duration(result))
}
}
// main.go
package main
import (
"fmt"
"math/rand"
"strconv"
"time"
)
func sendDataToChannel(input chan int) {
for i := 0; i < 100; i++ {
value := rand.Intn(50)
input <- value
time.Sleep(time.Duration(value))
}
}
func processChannel(id int, input chan int, output chan string) {
for {
var result int
select {
case value := <-input:
result = value * 1000
output <- "worker: " + strconv.Itoa(id) + ", input: " + strconv.Itoa(value) + ", result: " + strconv.Itoa(result)
default:
// do nothing
}
time.Sleep(time.Duration(result))
}
}
func main() {
rand.Seed(time.Now().UnixNano())
input := make(chan int)
output := make(chan string)
defer close(input)
defer close(output)
for i := 0; i < 10; i++ {
go processChannel(i, input, output)
}
go sendDataToChannel(input)
for {
select {
case x, ok := <-output:
if ok {
fmt.Println(x)
} else {
fmt.Println("Channel closed!")
}
}
}
}
We want to create a go program that provides various functions for processing lists of different types. Then we will optimize this program with generics and refactor the functions to show their advantages.
// main.go
package main
import "fmt"
func SumInts(m map[string]int64) int64 {
var sum int64
for _, val := range m {
sum += val
}
return sum
}
// main.go
// ...
func SumFloats(m map[string]float64) float64 {
var sum float64
for _, val := range m {
sum += val
}
return sum
}
// main.go
// ...
func main() {
ints := map[string]int64{
"1st": 34,
"2nd": 12,
}
floats := map[string]float64{
"1st": 35.98,
"2nd": 26.99,
}
fmt.Printf("Non-Generic Sums: %v and %v\n",
SumInts(ints),
SumFloats(floats))
}
$ go run main.go
Non-Generic Sums: 46 and 62.97
Now lets switch to a generic function.
package main
import "fmt"
func SumIntsOrFloats[CompType comparable, ValueType int64 | float64](m map[CompType]ValueType) ValueType {
var sum ValueType
for _, val := range m {
sum += val
}
return sum
}
[CompType comparable, ValueType int64 | float64]
func ... (m map[CompType]ValueType) ValueType {
var sum ValueType
main
function again// main.go
// ...
func main() {
ints := map[string]int64{
"1st": 34,
"2nd": 12,
}
floats := map[string]float64{
"1st": 35.98,
"2nd": 26.99,
}
fmt.Printf("Generic Sums: %v and %v\n",
SumIntsOrFloats[string, int64](ints),
SumIntsOrFloats[string, float64](floats))
}
$ go run main.go
Generic Sums: 46 and 62.97
Now lets add some minor improvements to the code.
// main.go
// ...
type Number interface {
int64 | float64
}
// main.go
// ...
func SumNumbers[CompType comparable, Number int64 | float64](m map[CompType]Number) Number {
var sum Number
for _, val := range m {
sum += val
}
return sum
}
CompType
is not needed in the function call// main.go
// ...
func main() {
fmt.Printf("Generic Sums with Constraint: %v and %v\n",
SumNumbers(ints),
SumNumbers(floats))
}
$ go run main.go
Generic Sums with Constraint: 46 and 62.97
github.com/samber/lo contains many helpers for:
Gin is a web framework written in Go (Golang). It features a martini-like API with performance that is up to 40 times faster thanks to httprouter. If you need performance and good productivity, you will love Gin.
$ go get -u github.com/gin-gonic/gin
// main.go
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}
$ go run main.go
{
message: "pong"
}
... //go:linkname must refer to declared function or variable
$ go get -u golang.org/x/sys
$ go mod init grohm.io/gin-ping-pong
$ go run main.go
The Beer Fridge
in a functional style
func SetupApi(r *gin.Engine, temperatureProvider func() float32) {
api := r.Group("/api")
{
api.GET("/temperature",
composeGetTemperatureHandler(temperatureProvider))
}
}
in a functional style
func getRandomTemperature() float32 {
return 4 + rand.Float32()*3
}
func composeGetTemperatureHandler(
temperatureProvider func() float32) func(context *gin.Context) {
return func(context *gin.Context) {
context.JSON(200, gin.H{
"temperature": temperatureProvider(),
})
}
}
in a functional style
GET http://localhost:8080/api/temperature
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Fri, 17 Jun 2022 13:48:16 GMT
Content-Length: 24
{
"temperature": 5.813981
}
type GetBeersFilterQuery struct {
Origin string `form:"origin"`
}
applyQueryFilter := func(context *gin.Context, beers []Beer) []Beer {
var query GetBeersFilterQuery
if context.ShouldBindQuery(&query) == nil {
// Evaluate query here
}
return beers
}
Filter method in Go 1.18 with generics
if context.ShouldBindQuery(&query) == nil {
beers = lo.Filter[Beer](beers, func(it Beer, _ int) bool {
return query.Origin == "" || it.Origin == query.Origin
})
}
i.E. adding HATEOAS
mapHATEOAS := func(beers []Beer) []HAETEOASResource[Beer] {
return lo.Map[Beer, HAETEOASResource[Beer]](beers,
func(it Beer, _ int) HAETEOASResource[Beer] {
return HAETEOASResource[Beer]{
Data: it,
Links: map[string]string{
"info": "/" + it.urlsafePathToken(),
"deposit": "/" + it.urlsafePathToken() + "/deposit",
},
}
})
}
Our beer fridge provides HTTP/REST style APIs now. With Go, it is also possible to provide a GraphQL interface. Lets dive into it.
Approaches
As a thirsty employee, I want:
go run github.com/99designs/gqlgen init
# graph/beer.graphqls
type Beer {
id: ID!
manufacturer: String!
name: String!
origin: String!
type: String!
percentage : Float!
ibu: Int
}
Of course the GraphQL type documentation is passed through to the generated Go code
# graph/beer.graphqls
"""
Beer defines key criteria of a beer
"""
type Beer {
id: ID!
manufacturer: String!
name: String!
"""
Origin of the beer as ISO country code
"""
origin: String!
type: String!
percentage : Float!
ibu: Int
}
To expose data to clients, we need a Query specification.
# graph/schema.graphqls
type Query {
beers: [Beer!]!
}
go run github.com/99designs/gqlgen generate
// graph/resolver.go
type Resolver struct {
BeerResolver func () []*model.Beer
}
// service/BeerService.go
func GetBeers() []*model.Beer {
return funnyFakeBeerList
}
// graph/schema.resolvers.go
// Beers is the resolver for the beers field.
func (r *queryResolver) Beers(ctx context.Context) ([]*model.Beer, error) {
return r.BeerResolver(), nil
}
GraphiQL Web Interface: http://localhost:8080
curl 'http://localhost:8080/query' \
--data-raw '{"query":"..."'} \
query{
beers {
id
name
manufacturer
ibu
percentage
}
}
{
"data": {
"beers": [
{
"id": "OMTR",
"name": "Oostmalle Trappist",
"manufacturer": "Oostmalle",
"ibu": 48,
"percentage": 14.1
} // more...
query{
beers {
origin
manufacturer
percentage
}
}
"data": {
"beers": [
{
"origin": "BE",
"manufacturer": "Oostmalle",
"percentage": 14.1
}, ///more
Job done.
Wait..
Haven't we mentioned filtering before?
# graph/schema.graphqls
type Query {
beers(minPercentage: Float = 0.0) : [Beer!]!
}
go run github.com/99designs/gqlgen generate
// graph/schema.resolvers.go
func (r *queryResolver) Beers(_ context.Context,
minPercentage *float64) ([]*model.Beer, error) {
if *minPercentage < 0.0 {
return nil, errors.New("percentage must be bigger or equal to 0")
}
beersFiltered := lo.Filter[*model.Beer](r.BeerResolver(),
func(it *model.Beer, _ int) bool {
return it.Percentage >= *minPercentage
})
return beersFiltered, nil
}
Return all beers with >10% and where they are from.
query{
beers(minPercentage : 10) {
name
origin
manufacturer
percentage
}
}
{
"data": {
"beers": [
{
"name": "Rituel Quatorze",
"origin": "BE",
"manufacturer": "Grim Fandango",
"percentage": 14.9
}
// more
]
}
}
We want to build the beer-fridge
service again.
This service using the interface first approach with go-swagger
code generation.
go-swagger
via docker on mac and linuxdocker pull quay.io/goswagger/swagger
alias swagger='docker run --rm -it --user $(id -u):$(id -g) -e \
GOPATH=$(go env GOPATH):/go -v $HOME:$HOME -w $(pwd) quay.io/goswagger/swagger'
swagger version
go-swagger
via docker on windowsdocker run --rm -it --env GOPATH=/go -v %CD%:/go/src -w /go/src quay.io/goswagger/swagger
$ swagger version
version: v0.29.0
commit: 53696caa1e8a4e5b483c87895d54eda202beb3b0
swagger init spec \
--title "A beer fridge service" \
--description "Beer fridge service build with go-swagger" \
--version 1.0.0 \
--scheme http \
--consumes application/io.grohm.go-workshop.beer-fridge.v1+json \
--produces application/io.grohm.go-workshop.beer-fridge.v1+json
swagger.yml
# swagger.yml
consumes:
- application/io.grohm.go-workshop.beer-fridge.v1+json
info:
description: Beer fridge service build with go-swagger
title: A beer fridge service
version: 1.0.0
paths: {}
produces:
- application/io.grohm.go-workshop.beer-fridge.v1+json
schemes:
- http
swagger: "2.0"
➜ swagger validate swagger.yml
2022/07/01 19:57:02
The swagger spec at "swagger.yml" is valid against swagger specification 2.0
2022/07/01 19:57:02
The swagger spec at "swagger.yml" showed up some valid but possibly unwanted constructs.
2022/07/01 19:57:02 See warnings below:
2022/07/01 19:57:02 - WARNING: spec has no valid path defined
beer
and teperature
of our fridge# swagger.yml
# ...
definitions:
beer:
type: object
required:
- title
- origin
- volume-percentage
properties:
id:
type: integer
format: int64
readOnly: true
title:
type: string
minLength: 1
origin:
type: string
minLength: 1
volume-percentage:
type: number
format: float
minLength: 1
temperature:
type: integer
format: int64
readOnly: true
/beers
and /temperature
paths:
/beers:
get:
tags:
- beers
parameters:
- name: limit
in: query
type: integer
format: int32
default: 10
responses:
200:
description: list the beer operations
schema:
type: array
items:
$ref: "#/definitions/beer"
/temperature:
get:
tags:
- fridge
responses:
200:
description: return the current fridge temperature
schema:
$ref: "#/definitions/temperature"
➜ go mod init grohm.io/beer-fridge-go-swagger
➜ swagger generate server -A beer-fridge -f ./swagger.yml
➜ go get -u -f ./...
➜ tree
.
├── cmd
│ └── beer-fridge-server
│ └── main.go
├── go.mod
├── go.sum
├── models
│ ├── beer.go
│ └── temperature.go
├── restapi
│ ├── configure_beer_fridge.go
│ ├── doc.go
│ ├── embedded_spec.go
│ ├── operations
│ │ ├── beer_fridge_api.go
│ │ ├── beers
│ │ │ ├── get_beers.go
│ │ │ ├── get_beers_parameters.go
│ │ │ ├── get_beers_responses.go
│ │ │ └── get_beers_urlbuilder.go
│ │ └── fridge
│ │ ├── get_temperature.go
│ │ ├── get_temperature_parameters.go
│ │ ├── get_temperature_responses.go
│ │ └── get_temperature_urlbuilder.go
│ └── server.go
└── swagger.yml
7 directories, 19 files
➜ go run cmd/beer-fridge-server/main.go
2022/08/28 16:34:16 Serving beer fridge at http://127.0.0.1:54746
"operation beers.GetBeers has not yet been implemented"
➜ go build -o beer-fridge-server ./cmd/beer-fridge-server/main.go
➜ ./beer-fridge-server --help
➜ ./beer-fridge-server --port 8080
2022/08/05 19:18:18 Serving beer fridge at http://127.0.0.1:8080
"operation beers.GetBeers has not yet been implemented"
error
definition/beers
, add beer# swagger.yml
definitions:
# ...
error:
type: object
required:
- message
properties:
code:
type: integer
format: int64
message:
type: string
# ...
paths:
/beers:
#...
post:
tags:
- todos
operationId: addOne
parameters:
- name: body
in: body
schema:
$ref: "#/definitions/beer"
responses:
201:
description: Created
schema:
$ref: "#/definitions/beer"
default:
description: error
schema:
$ref: "#/definitions/error"
/beers
, delete a beer# swagger.yml
/paths:
beers:
delete:
tags:
- beers
operationId: destroyOne
responses:
204:
description: Deleted
default:
description: error
schema:
$ref: "#/definitions/error"
consumes:
- application/io.grohm.go-workshop.beer-fridge.v1+json
info:
description: Beer fridge service build with go-swagger
title: A beer fridge service
version: 1.0.0
produces:
- application/io.grohm.go-workshop.beer-fridge.v1+json
schemes:
- http
swagger: "2.0"
definitions:
beer:
type: object
required:
- title
- origin
- volume-percentage
properties:
id:
type: integer
format: int64
readOnly: true
title:
type: string
minLength: 1
origin:
type: string
minLength: 1
volume-percentage:
type: number
format: float
minLength: 1
temperature:
type: integer
format: int64
readOnly: true
error:
type: object
required:
- message
properties:
code:
type: integer
format: int64
message:
type: string
paths:
/beers:
get:
operationId: getAllBeers
tags:
- beers
parameters:
- name: limit
in: query
type: integer
format: int32
default: 20
responses:
200:
description: list the beer operations
schema:
type: array
items:
$ref: "#/definitions/beer"
post:
tags:
- beers
operationId: addOne
parameters:
- name: body
in: body
schema:
$ref: "#/definitions/beer"
responses:
201:
description: Created
schema:
$ref: "#/definitions/beer"
default:
description: error
schema:
$ref: "#/definitions/error"
/beers/{id}:
delete:
tags:
- beers
operationId: destroyOne
parameters:
- type: integer
format: int64
name: id
in: path
required: true
responses:
204:
description: Deleted
default:
description: error
schema:
$ref: "#/definitions/error"
/temperature:
get:
operationId: getTemperature
tags:
- fridge
responses:
200:
description: return the current fridge temperature
schema:
$ref: "#/definitions/temperature"
swagger generate server -A beer-fridge -f ./swagger.yml
go get -u -f ./...
// beer_container/beer_container.go
package beers
import (
"grohm.io/beer-fridge-go-swagger/models"
"github.com/go-openapi/errors"
"sync"
"sync/atomic"
)
var beerList = make(map[int64]*models.Beer)
var lastID int64
var beerListLock = &sync.Mutex{}
func newBeerID() int64 {
return atomic.AddInt64(&lastID, 1)
}
// beer_container/beer_container.go
//...
func AddBeer(beer *models.Beer) error {
if beer == nil {
return errors.New(500, "beer must be present")
}
beerListLock.Lock()
defer beerListLock.Unlock()
newID := newBeerID()
beer.ID = newID
beerList[newID] = beer
return nil
}
// beer_container/beer_container.go
//...
func DeleteBeer(id int64) error {
beerListLock.Lock()
defer beerListLock.Unlock()
_, exists := beerList[id]
if !exists {
return errors.NotFound("not found: item %d", id)
}
delete(beerList, id)
return nil
}
// beer_container/beer_container.go
//...
func AllBeers(limit int32) (result []*models.Beer) {
result = make([]*models.Beer, 0)
for _, beer := range beerList {
if len(result) >= int(limit) {
return
}
result = append(result, beer)
}
return
}
// restapi/configure_beer_fridge.go
// ...
func configureAPI(api *operations.BeerFridgeAPI) http.Handler {
// ...
api.BeersAddOneHandler = beers.AddOneHandlerFunc(func(params beers.AddOneParams) middleware.Responder {
if err := beer_container.AddBeer(params.Body); err != nil {
return beers.NewAddOneDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
return beers.NewAddOneCreated().WithPayload(params.Body)
})
api.BeersDestroyOneHandler = beers.DestroyOneHandlerFunc(func(params beers.DestroyOneParams) middleware.Responder {
beer_container.DeleteBeer(params.ID)
return beers.NewDestroyOneNoContent()
})
api.BeersGetAllBeersHandler = beers.GetAllBeersHandlerFunc(func(params beers.GetAllBeersParams) middleware.Responder {
mergedParams := beers.NewGetAllBeersParams()
if params.Limit != nil {
mergedParams.Limit = params.Limit
}
return beers.NewGetAllBeersOK().WithPayload(beer_container.AllBeers(*mergedParams.Limit))
})
}
temperature
// temperature/temperature.go
package temperature
import (
"grohm.io/beer-fridge-go-swagger/models"
"math/rand"
)
func GetTemperature() models.Temperature {
min := 5
max := 10
return models.Temperature(rand.Intn(max-min) + min)
}
// restapi/configure_beer_fridge.go
// ...
func configureAPI(api *operations.BeerFridgeAPI) http.Handler {
// ...
api.FridgeGetTemperatureHandler = fridge.GetTemperatureHandlerFunc(func(params fridge.GetTemperatureParams) middleware.Responder {
return fridge.NewGetTemperatureOK().WithPayload(temperature.GetTemperature())
})
}
go build -o beer-fridge-server ./cmd/beer-fridge-server/main.go
./beer-fridge-server --port 8080
curl -i localhost:8080/beers -d "{\"title\":\"Three Floyds Brewing Co.\", \"origin\":\"Munster, Ind.\", \"volume-percentage\": 5}" -H 'Content-Type: application/io.grohm.go-workshop.beer-fridge.v1+json'
curl -i localhost:8080/beers -d "{\"title\":\"The Alchemist Heady Topper\", \"origin\":\"Waterbury, Vt.\", \"volume-percentage\": 6}" -H 'Content-Type: application/io.grohm.go-workshop.beer-fridge.v1+json'
curl -i localhost:8080/beers -d "{\"title\":\"Founders KBS (Kentucky Breakfast Stout)\", \"origin\":\"Grand Rapids, Mich.\", \"volume-percentage\": 7}" -H 'Content-Type: application/io.grohm.go-workshop.beer-fridge.v1+json'
curl -i localhost:8080/beers
curl -i localhost:8080/temperature
curl -i localhost:8080/beers/1 -X DELETE -H 'Content-Type: application/io.grohm.go-workshop.beer-fridge.v1+json'
$ curl -i localhost:8080/beers/3 -H 'Content-Type: application/io.grohm.go-workshop.beer-fridge.v1+json'
HTTP/1.1 405 Method Not Allowed
Allow: DELETE
Content-Type: application/json
Date: Sat, 27 Aug 2022 17:56:28 GMT
Content-Length: 68
{"code":405,"message":"method GET is not allowed, but [DELETE] are"}%
# build stage
FROM golang:1.17.6-alpine AS build
RUN mkdir -p /app
WORKDIR /app
# build src
COPY go.mod .
COPY go.sum .
RUN go mod download
# app src
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o beer-fridge-server ./cmd/beer-fridge-server/main.go
# result stage
FROM scratch
COPY --from=build /app/beer-fridge-server /beer-fridge-server
EXPOSE 8080
EXPOSE 443
EXPOSE 80
ENTRYPOINT ["/beer-fridge-server", "--port", "8080"]
Let's build a simple CLI tool and learn something about parsing commandlines, first-class functions and function types in Go along with a little generics.
Compute a credibility score for a website, taking into account
We won't reinvent the wheel, so we will use:
A feature computes a score from a document.
// pkg/scoring/scoring.go
type Feature func(document *goquery.Document) float64
A set of features:
//pkg/scoring/scoring.go
type FeatureSet []Feature
A feature registry contains a set of registrations
// pkg/scoring/scoringFeatureRegistry.go
type FeatureRegistry struct {
registrations []FeatureRegistration
}
A registration consists of:
// pkg/scoring/scoringFeatureRegistry.go
type FeatureRegistration struct {
Feature
Title string
Tags []string
}
Lets start with describing our expectations in a test
// pkg/scoring/scoringFeatureRegistry_test.go
func TestRegisterScoringFeature(t *testing.T) {
fakeFeature := func(_ *goquery.Document) float64 {
return 1
}
fakeRegistration := FeatureRegistration{
Feature: fakeFeature,
Title: "FAKE_REGISTRATION",
Tags: []string{"FAKE"},
}
t.Run("should return the previously registered feature when only one registered", func(t *testing.T) {
registry := FeatureRegistry{}
registry.Register(fakeRegistration)
features := registry.GetFeatures()
assert.Len(t, features, 1)
})
//pkg/scoring/scoringFeatureRegistry.go
func (f *FeatureRegistry) Register(registrations ...FeatureRegistration) {
for _, registration := range registrations {
f.registrations = append(f.registrations, registration)
}
}
func (f *FeatureRegistry) GetFeatures() FeatureSet {
return f.registrations
}
// pkg/scoring/scoringFeatureRegistry_test.go
func TestRegisterScoringFeature(t *testing.T) {
// ...
fakeRegistration := FeatureRegistration{
Feature: fakeFeature,
Title: "FAKE_REGISTRATION",
Tags: []string{"FAKE"},
}
fakeRegistration2 := FeatureRegistration{
Feature: fakeFeature,
Title: "FAKE_REGISTRATION2",
Tags: []string{"FAKE2"},
}
t.Run("should return all registered features when multiple registered and no filter provided", func(t *testing.T) {
registry := FeatureRegistry{}
registry.Register(fakeRegistration, fakeRegistration2)
features := registry.GetFeatures()
assert.Len(t, features, 2)
})
t.Run("should return features matching filter", func(t *testing.T) {
registry := FeatureRegistry{}
registry.Register(fakeRegistration, fakeRegistration2)
features := registry.GetFeatures("FAKE2")
assert.Len(t, features, 1)
})
}
Lets use lo to filter our subscriptions.
// pkg/scoring/scoringFeatureRegistry.go
func (f *FeatureRegistry) GetFeatures(includeTags ...string) FeatureSet {
filteredRegistrations := lo.Filter[FeatureRegistration](f.registrations,
func(it FeatureRegistration, _ int) bool {
return len(includeTags) == 0 || lo.Some[string](includeTags, it.Tags)
})
return lo.Map[FeatureRegistration, Feature](filteredRegistrations,
func(it FeatureRegistration, _ int) Feature {
return it.Feature
})
}
The result datatype
// pkg/scoring/wordCount.go
type WordFrequencyResult struct {
TotalWords int
CountedWords int
WordCounts map[string]int
}
// pkg/scoring/wordCount.go
func WordCount(doc *goquery.Document) WordFrequencyResult {
result := WordFrequencyResult{
WordCounts: make(map[string]int),
}
text := doc.Find("p").Contents().Text()
words := strings.Split(text, " ")
result.TotalWords = len(words)
for i := 0; i < len(words); i++ {
wordSanitized := strings.ToUpper(sanitizeString.ReplaceAllString(words[i], ""))
if len(wordSanitized) > 4 {
result.WordCounts[wordSanitized]++
}
}
for _, v := range result.WordCounts {
result.CountedWords += v
}
return result
}
// pkg/scoring/wordCount.go
func wordCountRegistration() FeatureRegistration {
return FeatureRegistration{
Feature: ScoreWrapper[WordFrequencyResult](WordCount),
Title: "Word Count",
Tags: []string{"CONTENT"},
}
}
// pkg/scoring/scoringFeatureRegistry.go
func NewDefaultRegistry() (r FeatureRegistry) {
r.Register(affiliateLinkCountRegistration())
return
}
// pkg/scoring/scoreWrapper.go
// Scoreable constraints to types providing a method to be scored
type Scoreable interface {
Score() float64
}
// ScoreWrapper returns a function converting its result to a score
func ScoreWrapper[T Scoreable](scoreFunction func(doc *goquery.Document) T) Feature {
return func(document *goquery.Document) float64 {
result := scoreFunction(document)
return result.Score()
}
}
// cmd/score/score.go
func main() {
var targetUrl = flag.String("url", "", "URL of the site to be parsed")
flag.Parse()
if len(*targetUrl) == 0 {
flag.Usage()
}
score := scoring.Score(*targetUrl)
fmt.Printf("Website=%s Score=%f", *targetUrl, score)
}
// pkg/scoring/scoring.go
func computeScore(features FeatureSet, document *goquery.Document) (score float64) {
for _, feature := range features {
score += feature(document)
}
return
}
func Score(url string, featureTags ...string) (score float64) {
registry := NewDefaultRegistry()
features := registry.GetFeatures(featureTags...)
document, _ := download.DownloadWebsite(url)
return computeScore(features, document)
}
$ website_score --url https://grohm.io
Website=https://grohm.io Score=244.200000
// pkg/scoring/affiliateLinkCount.go
type LinkCountResult struct {
TotalLinks int `json:"TotalLinks"`
LocalLinks int `json:"LocalLinks"`
AffiliateLinks int `json:"AffiliateLinks"`
MaskedAffiliateLinks int `json:"MaskedAffiliateLinks"`
ShortendedUrls int `json:"ShortendedUrls"`
}
func (l LinkCountResult) Score() float64 {
return float64(l.TotalLinks + l.LocalLinks - l.AffiliateLinks*2 - l.MaskedAffiliateLinks*4 - l.ShortendedUrls)
}
// pkg/scoring/affiliateLinkCount.go
var affiliateLinksExpression, _ = regexp.Compile("(\\.amazon\\.)")
var maskedAffiliateLinksExpression, _ = regexp.Compile("(amzn\\.to)")
var shortenedUrlExpression, _ = regexp.Compile("(bit\\.ly|tinyurl\\.com)")
func AffiliateLinkCount(doc *goquery.Document) LinkCountResult {
result := LinkCountResult{}
localdomain := doc.Url.Host
doc.Find("a").Each(func(i int, s *goquery.Selection) {
result.TotalLinks++
link, exists := s.Attr("href")
if exists {
if strings.Contains(link, localdomain) {
result.LocalLinks++
}
if affiliateLinksExpression.MatchString(link) {
result.AffiliateLinks++
}
if maskedAffiliateLinksExpression.MatchString(link) {
result.MaskedAffiliateLinks++
}
if shortenedUrlExpression.MatchString(link) {
result.ShortendedUrls++
}
}
})
return result
}
// pkg/scoring/affiliateLinkCount.go
func affiliateLinkCountRegistration() FeatureRegistration {
return FeatureRegistration{
Feature: ScoreWrapper[LinkCountResult](AffiliateLinkCount),
Title: "Affiliate Link Count",
Tags: []string{"MARKETING", "AFFILIATE", "UNTRUSTWORTHY"},
}
}
// pkg/scoring/scoringFeatureRegistry.go
func NewDefaultRegistry() (r FeatureRegistry) {
r.Register(affiliateLinkCountRegistration(), wordCountRegistration())
return
}
Implement a CLI parameter to specify the feature filter query
We want to build a simple pipeline for one of our go services.
main.go
// main.go
package main
import (
"fmt"
"math"
)
func main() {
var i int
fmt.Printf("Enter a number to check: ")
_, _ = fmt.Scanf("%d", &i)
var result = IsPrime(i)
fmt.Printf("result=%t ", result)
}
func IsPrime(value int) (result bool) {
for i := 2; i <= int(math.Floor(math.Sqrt(float64(value)))); i++ {
if value%i == 0 {
return false
}
}
return value > 1
}
main_test.go
// main_test.go
package main
import (
"testing"
)
func TestPrimeCheckerTheNaiveWay(t *testing.T) {
t.Run("should return FALSE when no prime number given", func(t *testing.T) {
if IsPrime(4) == true {
t.Fatal("Reported IsPrime=true for 4")
}
})
t.Run("should return TRUE when prime number given", func(t *testing.T) {
if IsPrime(7) == false {
t.Fatal("Reported IsPrime=true for 7")
}
})
}
func TestPrimeCheckerTableDriven(t *testing.T) {
cases := []struct {
input int
expectedResult bool
}{
{1, false},
{2, true},
{3, true},
{4, false},
{7, true},
}
for _, e := range cases {
t.Run("Should return expected result ", func(t *testing.T) {
result := IsPrime(e.input)
if result != e.expectedResult {
t.Fatalf("Unexpected Result input=%d expected=%t actual=%t", e.input, e.expectedResult, result)
}
})
}
}
go mod init grohm.io/pipelines_showcase
go.sum
.gitlab-ci.yml
# .gitlab-ci.yml
image: golang:1.19-alpine
# .gitlab-ci.yml
# ...
stages:
- lint
- test
- build
# .gitlab-ci.yml
# ...
fmt:
stage: lint
script:
- go fmt
- go vet
# .gitlab-ci.yml
# ...
test:
stage: test
script:
- go test -coverprofile=coverage.out
- go tool cover -html=coverage.out -o coverage.html
artifacts:
paths:
- coverage.html
expire_in: 1 week
# .gitlab-ci.yml
# ...
build:
stage: build
script:
- go build
artifacts:
paths:
- pipelines_showcase
expire_in: 1 week
# Dockerfile
# build stage
FROM golang:1.19-alpine AS build
RUN mkdir -p /app
WORKDIR /app
# build src
COPY go.mod .
COPY go.sum .
RUN go mod download
# app src
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /bin/app
# result stage
FROM scratch
COPY --from=build /bin/app /bin/app
ENTRYPOINT ["/bin/app"]
# .gitlab-ci.yml
# ...
docker-build:
stage: containerize
image:
name: gcr.io/kaniko-project/executor:debug
entrypoint: [ "" ]
variables:
KUBERNETES_CPU_REQUEST: 1
KUBERNETES_CPU_LIMIT: 1
KUBERNETES_MEMORY_REQUEST: 2048Mi
KUBERNETES_MEMORY_LIMIT: 2048Mi
needs: ["build"]
script:
- mkdir -p /kaniko/.docker
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $CI_REGISTRY_IMAGE:$CI_PIPELINE_IID
# .gitlab-ci.yml
# ...
release_job:
stage: release_tagging
image: registry.gitlab.com/gitlab-org/release-cli:latest
needs:
- job: docker-build
artifacts: true
script:
- echo "running release_job for $CI_PIPELINE_IID"
release:
name: 'Release $CI_PIPELINE_IID'
description: 'Created using the release-cli'
tag_name: '$CI_PIPELINE_IID'
ref: '$CI_COMMIT_SHA'
# .gitlab-ci.yml
# ...
stages:
- lint
- test
- build
- containerize
- release_tagging
workflow:
rules:
- if: $CI_COMMIT_TAG
when: never
- when: always
# ...
We want to deploy a simple go service from sources.
We use fly.io
as a simple solution.
brew install flyctl
curl -L https://fly.io/install.sh | sh
iwr https://fly.io/install.ps1 -useb | iex
flyctl auth signup
flyctl auth login
main.go
file// main.go
package main
import (
"embed"
"html/template"
"log"
"net/http"
"os"
)
//go:embed templates/*
var resources embed.FS
var t = template.Must(template.ParseFS(resources, "templates/*"))
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
data := map[string]string{
"Region": os.Getenv("FLY_REGION"),
}
t.ExecuteTemplate(w, "index.html.tmpl", data)
})
log.Println("listening on", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}
templates/index.html.tmpl
<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
<h1>Golang for Developers Workshop on Fly.io</h1>
{{ if .Region }}
<h2>I'm running in the {{.Region}} region</h2>
{{end}}
</body>
</html>
go mod init grohm.io/flyio
$ flyctl launch
Creating app in /Users/grohmio/repos/cc/gophers/golang-for-developers/examples/17-deployment/beer-fridge-gs
Scanning source code
Detected a Go app
Using the following build configuration:
Builder: paketobuildpacks/builder:base
Buildpacks: gcr.io/paketo-buildpacks/go
? Choose an app name (leave blank to generate one): flyio-grohmio
fly.toml
is generated# fly.toml file generated for flyio-grohmio on 2022-12-10T00:26:40+01:00
app = "flyio-grohmio"
kill_signal = "SIGINT"
kill_timeout = 5
processes = []
[build]
builder = "paketobuildpacks/builder:base"
buildpacks = ["gcr.io/paketo-buildpacks/go"]
[env]
PORT = "8080"
[experimental]
allowed_public_ports = []
auto_rollback = true
[[services]]
http_checks = []
internal_port = 8080
processes = ["app"]
protocol = "tcp"
script_checks = []
[services.concurrency]
hard_limit = 25
soft_limit = 20
type = "connections"
[[services.ports]]
force_https = true
handlers = ["http"]
port = 80
[[services.ports]]
handlers = ["tls", "http"]
port = 443
[[services.tcp_checks]]
grace_period = "1s"
interval = "15s"
restart_limit = 0
timeout = "2s"
$ flyctl deploy
flyctl