= Golang Boot Camp
:toc: manual
== Getting started
* https://golang.org/doc/
=== Check go version
[source, go]
----
# go version
go version go1.21.5 linux/arm64
----
=== package, import, main
[source, go]
----
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
----
=== Run main methods
[source, go]
----
$ go run hello.go
Hello, World!
----
=== Call External Methods
[source, go]
----
package main
import "fmt"
import "rsc.io/quote"
func main() {
fmt.Println("Hello, World!")
fmt.Println(quote.Glass())
fmt.Println(quote.Go())
fmt.Println(quote.Hello())
fmt.Println(quote.Opt())
}
----
== Getting started from real project
link:k8s-bigip-ctlr.adoc[project k8s-bigip-ctlr]
== Work with Go standard project
=== Project Structure
[source, go]
----
# tree greetings/
greetings/
├── cmd
│ └── greetings
│ └── main.go
└── pkg
└── greetings
├── greetings.go
└── greetings_test.go
----
A standard Go project structure can vary depending on the size and nature of the project, but there are some common conventions that many Go developers follow:
* *cmd*: The `cmd` directory is for the main applications of your project. Each application can have its own subdirectory. For example, myapp could be the main entry point for your application
* *internal*: The `internal` directory is for packages that are only used within your project, not meant for external use.
* *pkg*: The pkg directory contains libraries and packages that are meant to be used by other projects. Each package within pkg can have its own subdirectory.
=== Init and Setup Dependencies
[source, go]
----
go mod init github.com/kylinsoong/golang/greetings
go mod tidy
----
=== Run
[source, go]
----
go run cmd/greetings/main.go
----
=== How main module call pkg module
[source, go]
----
package main
import (
"github.com/kylinsoong/golang/greetings/pkg/greetings"
)
func main() {
names := []string{"Gladys", "Samantha", "Darrin", "Kylin"}
messages, err := greetings.Hellos(names)
}
----
=== Run Unit Test
[source, go]
----
go test ./pkg/greetings/
----
=== Build
[source, go]
----
go build -o a.out cmd/greetings/*.go
----
=== Run Binary File
[source, go]
----
# ./a.out
----
== Data Struct
=== simple map
[source, go]
----
processedResources := make(map[string]bool)
processedResources["foo.yaml"] = true
processedResources["bar.yaml"] = false
processedResources["zoo.yaml"] = false
for key, value := range processedResources {
fmt.Printf("%s: %v\n", key, value)
}
fmt.Println(processedResources["zoo.yaml"])
value, exists := processedResources["coo.yaml"]
if exists {
fmt.Printf("coo.yaml: %v\n", value)
} else {
fmt.Println("coo.yaml not exist")
}
----
=== simple struct
[source, go]
----
type WatchedNamespaces struct {
Namespaces []string
NamespaceLabel string
}
func main() {
watchedNamespaces := WatchedNamespaces{
Namespaces: []string{"namespace1", "namespace2"},
NamespaceLabel: "watched",
}
fmt.Println(watchedNamespaces.Namespaces)
fmt.Println(watchedNamespaces.NamespaceLabel)
}
----
=== struct with func field
Using a Go struct with a function field offers flexibility and allows you to encapsulate behavior within the struct while enabling dynamic customization.
[source, go]
----
type Manager struct {
queueLen int
processAgentLabels func(map[string]string, string, string) bool
}
func customProcessAgentLabels(labels map[string]string, namespace string, name string) bool {
fmt.Printf("Custom Processing Agent Labels: %v, Namespace: %s, Name: %s\n", labels, namespace, name)
return true
}
func main() {
appMgr := Manager{
queueLen: 10,
processAgentLabels: customProcessAgentLabels,
}
appMgr.processAgentLabels(map[string]string{"key": "value"}, "exampleNamespace", "exampleName")
}
----
== Fundamentals
=== object-oriented programming: interface composition
Go does not support traditional interface inheritance like some other object-oriented programming languages. Instead, Go uses a concept called "interface composition" or "embedding" to achieve similar goals without relying on classical inheritance.
In Go, you can embed one interface within another to create a new interface that includes the methods of the embedded interface.
[source, go]
.*Interface*
----
type Interface interface {
Add(item interface{})
Len() int
Get() (item interface{}, shutdown bool)
Done(item interface{})
ShutDown()
ShutDownWithDrain()
ShuttingDown() bool
}
----
[source, go]
.*DelayingInterface*
----
type DelayingInterface interface {
Interface
AddAfter(item interface{}, duration time.Duration)
}
----
[source, go]
.*RateLimitingInterface*
----
type RateLimitingInterface interface {
DelayingInterface
AddRateLimited(item interface{})
Forget(item interface{})
NumRequeues(item interface{}) int
}
----
== Multi-threads
=== goroutine
The goroutine is a lightweight thread of execution managed by the Go runtime. Goroutines enable concurrent programming in a way that is more efficient and scalable compared to traditional threads.
[source, go]
----
package main
import (
"fmt"
"time"
)
func printNumbers() {
for i := 1; i <= 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Printf("%d \n", i)
}
}
func main() {
go printNumbers()
for i := 1; i <= 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Printf("A%d \n", i)
}
}
----
=== channel: send and receive data
Channels are a typed conduit through which you can send and receive values with the channel operator *<-*:
* ch <- v send v to channel
* v := <-ch receive from channel, and assign value to v
[source, go]
----
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // send sum to c
}
func Test_Send_Receive(t *testing.T) {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c
fmt.Println(x, y, x+y)
}
----
=== channel: communication and synchronization between goroutines
In Go, channels are a powerful mechanism for communication and synchronization between goroutines. They provide a way for one goroutine to send data to another goroutine.
[source, go]
----
func numberGenerator(ch chan int, wg *sync.WaitGroup) {
defer wg.Done()
for i := 1; i <= 5; i++ {
ch <- i // Send numbers 1 to 5 to the channel
}
close(ch) // Close the channel to signal no more data will be sent
}
func squareCalculator(ch chan int, resultCh chan int, wg *sync.WaitGroup) {
defer wg.Done()
for num := range ch {
square := num * num
resultCh <- square // Send squared result to the resultCh channel
}
close(resultCh) // Close the resultCh channel to signal no more results will be sent
}
func resultPrinter(resultCh chan int, wg *sync.WaitGroup) {
defer wg.Done()
for result := range resultCh {
fmt.Println("Squared Result:", result)
}
}
func main() {
numberCh := make(chan int)
resultCh := make(chan int)
var wg sync.WaitGroup
wg.Add(3)
go numberGenerator(numberCh, &wg)
go squareCalculator(numberCh, resultCh, &wg)
go resultPrinter(resultCh, &wg)
wg.Wait()
}
----
=== channel: multiple chanels with select case statement
The select statement in Go is used to choose from multiple communication operations. It allows a goroutine to wait on multiple communication operations, blocking until one of them can proceed.
[source, go]
----
func simple_worker(c chan string) {
c <- fmt.Sprintf("Hello from Channel %v", c)
}
func Test_Multiple_Chan_With_Select(t *testing.T) {
ch1 := make(chan string)
ch2 := make(chan string)
go simple_worker(ch1)
go simple_worker(ch2)
select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
case <-time.After(3 * time.Second):
fmt.Println("Timed out waiting for messages.")
}
}
----
=== channel: signal completion of sub goroutine
[source, go]
----
func worker(ch chan struct{}) {
fmt.Println("Worker is starting...")
time.Sleep(2 * time.Second)
fmt.Println("Worker is done!")
ch <- struct{}{}
}
func main() {
doneCh := make(chan struct{})
go worker(doneCh)
<-doneCh
fmt.Println("Main function exiting.")
}
----
=== using signal to control application interruptiong and termination
In Go, the `os/signal` package provides a way to intercept signals sent to the program, such as termination signals (SIGINT for interrupt and SIGTERM for terminate). The signal usually wrapped with a channel that can be used to control application interruptiong and termination.
[source, go]
----
func main() {
fmt.Println("Started to run tasks...")
signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
sig := <-signals
fmt.Printf("Received signal: %v\n", sig)
}
----
=== defer statements a Last In, First Out (LIFO) execution order
[source, go]
----
func main() {
defer fmt.Println("This will be executed third.")
defer fmt.Println("This will be executed second.")
defer fmt.Println("This will be executed first.")
fmt.Println("Hello, Go!")
}
----
=== thread-safe via defer and sync
[source, go]
----
type Counter struct {
value int
mu sync.Mutex
}
func (c *Counter) increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
func (c *Counter) getValue() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
----
== K8S
=== How to repeatedly calls a provided function
The `k8s.io/apimachinery/pkg/util/wait` provides utilities for waiting and timing operations. Specifically, `wait.Until` is a function that repeatedly calls a provided function until the stop channel is closed or a timeout is reached.
[source, go]
.*wait.Until*
----
func exampleWork() {
fmt.Println("Doing some work...")
time.Sleep(2 * time.Second)
}
func main() {
stopCh := make(chan struct{})
go wait.Until(exampleWork, time.Second, stopCh)
time.Sleep(5 * time.Second)
close(stopCh)
time.Sleep(1 * time.Second)
fmt.Println("Main goroutine exiting...")
}
----
=== How to use kubernetes rate limited workqueue
Refer to link:#object-oriented-programming-interface-composition[object-oriented programming: interface composition] for more details about `k8s.io/client-go/util/workqueue` and `RateLimitingInterface` implemenration.
=== How to create kubernetes client
There are 2 stps necessary to create a kubeClient:
* Create Kubernetes Rest Config, If your application run in Kubernetes, the use the certifications keys in Namespace default ServiceAccount, if your application run outside Kubernetes, then you need pass `~/.kube/config` file to create Rest Config
* Create Kubernetes Client via Kubernetes Rest Config
[source, go]
----
import "k8s.io/client-go/kubernetes"
import "k8s.io/client-go/rest"
import "k8s.io/client-go/tools/clientcmd"
var kubeClient kubernetes.Interface
var config *rest.Config
var err error
if *inCluster {
config, err = rest.InClusterConfig()
} else {
config, err = clientcmd.BuildConfigFromFlags("", *kubeConfig)
}
if err != nil {
log.Fatalf("[INIT] error creating configuration: %v", err)
}
kubeClient, err = kubernetes.NewForConfig(config)
if err != nil {
log.Fatalf("[INIT] error connecting to the client: %v", err)
}
----
=== How to use kubernetes client-side caching mechanism and cache api create a configmap watcher
`k8s.io/client-go/tools/cache` is a client-side caching mechanism. It is useful for reducing the number of server calls you'd otherwise need to make. Reflector watches a server and updates a Store. Two stores are provided; one that simply caches objects (for example, to allow a scheduler to list currently available nodes), and one that additionally acts as a FIFO queue (for example, to allow a scheduler to process incoming pods).
[source, go]
----
cfgMapInformer = cache.NewSharedIndexInformer(
cache.NewFilteredListWatchFromClient(
restClientv1,
Configmaps,
namespace,
everything,
),
&v1.ConfigMap{},
resyncPeriod,
cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
)
cfgMapInformer.AddEventHandlerWithResyncPeriod(
&cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) { enqueueConfigMap(obj, OprTypeCreate) },
UpdateFunc: func(old, cur interface{}) { enqueueConfigMap(cur, OprTypeUpdate) },
DeleteFunc: func(obj interface{}) { enqueueConfigMap(obj, OprTypeDelete) },
},
resyncPeriod,
)
----