Kubernetes Custom Controller 개발에 필요한 Go 언어 기초 문법
Kubernetes custom controller 개발에 가장 잘 맞는 프로그래밍 언어는 Go 이다. Kubernetes 가 Go 로 개발된 S/W 이다 보니 Custom controller 도 Go 로 만드는 것이 좋을 것 같다는 생각이다.
그래서 겸사겸사 Custom Controller 개발에 필요한 Go 문법만 정리해 보기로 했다.
- 변수 선언 : var, Short Variable Declaration Operator(:=)
- Package 선언 및 활용
- Struct (json 으로 변환)
- Receiver function
- Interface 선언, 활용
변수 선언
변수는 var 키워드로 쉽게 선언할 수 있다.
// var 변수명 변수타입
var message string
변수를 선언하면서 값을 대입하면 마지막의 변수 타입은 생략 가능하다.
var message = "Hi, Welcome!"
:= operator 를 사용하면 shortcut 으로 선언하여 var 도 생략할 수 있다.
message := "Hi, Welcome!"
Package 선언 및 활용
모듈로 만들어서 import 하여 사용할 수 있다.
아래와 같이 디렉토리를 만들어보자. greetings 는 모듈로 선언하고 basic 에서 greetings 모듈을 import 하여 사용할 예정이다.
$ mkdir -p go-sample
$ cd go-sample
$ mkdir -p {basic,greetings}
$ cd greetings
go-sample 이라는 프로젝트 아래에 greetings 라는 모듈을 만든다.
모듈을 만들기 위해서 go.mod 파일을 만든다.
$ go mod init github.com/seungkyua/go-sample/greetings
go.mod 파일이 생성되며 파일 내용은 아래와 같다.
module github.com/seungkyua/go-sample/greetings
go 1.19
greetings.go 파일을 만들어서 아래와 같이 입력한다.
여기도 error 핸들링과 string format 을 위해서 모듈을 import 하고 있는데 이를 이해할려고 하지 말고 그냥 Hello function 이 있다는 것만 이해하자.
package greetings
import (
"errors"
"fmt"
)
func Hello(name string) (string, error) {
if name == "" {
return "", errors.New("empty name")
}
message := fmt.Sprintf("Hi, %v. Welcome!", name)
return message, nil
}
go-sample 홈디렉토리에서 보는 greetings 디렉토리 구조는 아래와 같다.
greetings
├── go.mod
└── greetings.go
golang 1.19 버전 부터는 로컬 하위 디렉토리를 인식하기 위해서 [go.work](<http://go.work>) 를 사용한다. 그리니 아래와 같이 하여 파일을 만들어 보자.
$ go work use ./basic
$ go work use ./greetins
go-sample 홈디렉토리 아래에 [go.work](<http://go.work>) 파일이 아래와 같이 만들어 진다.
go 1.19
use (
./greetings
./basic
)
이제 basic 디렉토리로 가서 똑같이 go.mod 파일을 만들고 main.go 파일도 만들어 본다.
$ cd basic
$ go mod init github.com/seungkyua/go-sample/basic
go.mod 파일이 아래와 같이 생성되었음을 알 수 있다.
module github.com/seungkyua/go-sample/basic
go 1.19
greetings 모듈을 로컬로 인식하게 변경한다.
$ go mod edit -replace github.com/seungkyua/go-sample/greetings=../greetings
그리고 로컬 버전을 사용하기 위해서 pseudo 버전을 사용하게 tidy 명령을 활용한다.
$ go mod tidy
그럼 최종 go.mod 는 다음과 같다.
module github.com/seungkyua/go-sample/basic
go 1.19
// go mod edit 으로 로컬 경로를 보도록 수정
replace github.com/seungkyua/go-sample/greetings => ../greetings
// 로컬이므로 pseudo 버전을 만든다
require github.com/seungkyua/go-sample/greetings v0.0.0-00010101000000-000000000000
main.go 파일을 만들어서 greetings 모듈을 import 하여 활용해 본다.
package main
import (
"fmt"
"log"
"github.com/seungkyua/go-sample/greetings"
)
func main() {
message, err := greetings.Hello("Seungkyu")
if err != nil {
log.Fatal(err)
}
fmt.Println(message)
}
go-sample 디렉토리 구조는 아래와 같다.
basic
├── go.mod
└── main.go
greetings
├── go.mod
└── greetings.go
go.work
Struct 활용 (json 변환)
struct 를 json data 로 변환하는 것을 marshal (encoding) 이라고 하고 json data를 struct 로 변환하는 것을 unmarshal (decoding) 이라고 한다.
json 패키지의 Marshal function 은 다음과 같다.
func Marshal(v interface{}) ([]byte, error)
Struct 를 만들어서 json data (byte slice) 로 변환해 보자.
type album struct {
ID string `json:"id"`
Title string `json:"title"`
Artist string `json:"artist"`
Price float64 `json:"price"`
}
a := album{ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99}
b, err := json.Marshal(a)
if err != nil {
log.Fatal(err)
}
b2 := []byte(`{"id":"1","title":"Blue Train","artist":"John Coltrane","price":56.99}`)
fmt.Println(bytes.Equal(b, b2))
Unmarshal function 은 다음과 같다.
func Unmarshal(data []byte, v interface{}) error
json data (byte slice) 를 struct 로 다시 변환한다.
var a2 album
err = json.Unmarshal(b, &a2)
if err != nil {
log.Fatal(err)
}
fmt.Println(a2)
Receiver Function
Receiver function 은 struct 의 멤버 변수를 활용할 수 있는 function 이다.
Vertex struct 를 만들고 그에 속한 멤버 변수를 활용하는 function 을 만들면 된다.
func 와 함수명 사이에 Struct 를 변수와 함께 넣으면 Receiver function 이 된다.
type Vertex struct {
X, Y float64
}
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
아래와 같이 main 함수를 실행하면 값은 50 이 나온다.
func main() {
v := Vertex{3, 4}
v.Scale(10)
fmt.Println(v.Abs())
}
그런데 만약 위의 func (v *Vertex) Scale(f float64) **에서 Vertex 를 포인터가 아닌 value 로 만들면 어떤 결과가 나올까? 위의 함수에서 * 를 지우고 다시 실행해 보자.
결과는 5 가 된다.
즉, Struct 의 멤버 변수의 값을 바꾸고 싶으면 Pointer Receiver 를 사용해야 한다.
Interface 선언, 활용
Interface 는 method 를 모아둔 것이라 할 수 있다. interface 역시 type 키워드로 선언하기 때문에 interface 타입이라고도 말한다.
아래와 같이 Abs() 메소드를 선언하고 나서 Abs 를 Receiver function 으로 구현했다면 Abs 를 구현한 타입은 Abser 타입이라고 할 수 있다.
type Abser interface {
Abs() float64
}
아래는 MyFloat 타입도 Abser 타입이라고 할 수 있다. 하지만 Abs 의 Receiver 는 value Receiver 이다.
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
Vertex 타입도 역시 Abser 타입이며 pointer Receiver 이다.
type Vertex struct {
X, Y float64
}
func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
main 함수에서 interface 를 활용해 본다.
a = v 에서 에러가 발생한다.
func main() {
var a Abser
f := MyFloat(-math.Sqrt2)
v := Vertex{3, 4}
a = f // MyFloat 은 Abser 인터페이스를 구현했으니 가능함
a = &v // *Vertex 는 Abser 인터페이스를 구현했으니 가능함
a = v // v 는 Vertext 타입임 (*Vertex 타입이 아님), value receiver 는 구현안했으니 에러
fmt.Println(a.Abs())
}
Interface 선언, 활용 (2)
interface 는 interface 를 포함할 수 있다.
아래와 같이 Client interface 가 Reader interface 를 가지면 Reader interface 에 선언된 함수가 그 대로 Client 에도 속하게 된다.
package interfaces
import (
"fmt"
)
type Reader interface {
Get(obj string) error
List(list []string) error
}
type Client interface {
Reader
}
var ReconcileClient Client = &client{}
type client struct {
name string
}
func (c *client) Get(obj string) error {
fmt.Println(obj)
return nil
}
func (c *client) List(list []string) error {
fmt.Println(list)
return nil
}
그런 다음 struct 에서 interface 를 또 다시 포함할 수 있다.
아래와 같이 GuestbookReconciler struct 에 Client interface 를 포함하면 GuestbookReconciler 는 마치 Client interface 가 가지는 함수를 자신의 메소드 처럼 사용할 수 있다.
package main
import "github.com/seungkyua/go-sample/interfaces"
type GuestbookReconciler struct {
interfaces.Client
}
func main() {
g := GuestbookReconciler{interfaces.ReconcileClient}
g.Get("Seungkyu")
g.Client.Get("Seungkyu")
}
이 때 메소드 활용은 g.Get 혹은 g.Client.Get 둘 다 가능하다.