- Getting started
- Getting started from real project
- Work with Go standard project
- Data Struct
- Fundamentals
- Multi-threads
- goroutine
- channel: send and receive data
- channel: communication and synchronization between goroutines
- channel: multiple chanels with select case statement
- channel: signal completion of sub goroutine
- using signal to control application interruptiong and termination
- defer statements a Last In, First Out (LIFO) execution order
- thread-safe via defer and sync
- K8S
# 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.
package main
import (
"github.com/kylinsoong/golang/greetings/pkg/greetings"
)
func main() {
names := []string{"Gladys", "Samantha", "Darrin", "Kylin"}
messages, err := greetings.Hellos(names)
}
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")
}
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)
}
Using a Go struct with a function field offers flexibility and allows you to encapsulate behavior within the struct while enabling dynamic customization.
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")
}
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.
type Interface interface {
Add(item interface{})
Len() int
Get() (item interface{}, shutdown bool)
Done(item interface{})
ShutDown()
ShutDownWithDrain()
ShuttingDown() bool
}
type DelayingInterface interface {
Interface
AddAfter(item interface{}, duration time.Duration)
}
type RateLimitingInterface interface {
DelayingInterface
AddRateLimited(item interface{})
Forget(item interface{})
NumRequeues(item interface{}) int
}
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.
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)
}
}
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
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)
}
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.
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()
}
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.
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.")
}
}
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.")
}
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.
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)
}
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!")
}
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.
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...")
}
Refer to object-oriented programming: interface composition for more details about k8s.io/client-go/util/workqueue
and RateLimitingInterface
implemenration.
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
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)
}
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).
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,
)