반응형

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 둘 다 가능하다.

반응형
Posted by seungkyua@gmail.com
,
반응형

Slice 와 Map 은 값을 여러개 자질 수 있는 타입이다. 그러나 선언한 타입과 같은 타입만 저장해야 하는 문제가 있다. 또한 class 와 같이 다양한 멤버 변수를 가질 수 없다.

 

그래서 Go 에서는 다양한 값을 가질 수 있도록 Struct 타입을 지원한다.

 

Struct

struct 의 를 만들려면 type 키워드, struct명, struct 키워드로 만든다. 그리고 그 안에 다양한 변수를 포함할 수 있으며 {} 로 묶어주면 된다.

 

type 으로 struct 를 선언했으면 그 자체가 하나의 type 이 된다. 그럼 var 로 새로운 만든 type을 활용하여 변수 선언을 할 수 있다. 아래와 같이 name 과 age 를 필드로 갖는 person 이라는 새로운 타입을 선언하고 jina 와 jamie 라는 변수를 만들 수 있다.

 

변수 선언만 한 struct 내의 필드들은 zero 값을 갖는다.

 

person 타입의 변수를 초기화 할 때는 {} 로 초기화 할 수 도 있다.

package main

import "fmt"

func main() {
	type person struct {
		name string
		age  int
	}

	var jina person
	fmt.Println(jina)

	jamie := person{}
	fmt.Println(jamie)
}

--- output ---
{ 0}
{ 0}

 

혹은 값을 지정하여 초기화 할 수 있다.

 

값을 지정하여 초기화할 때는 json 타입으로 변수:  으로 지정하거나  으로 지정할 수 있다. 변수 사이는 , 로 구분하며, 마지막 변수 끝에도 , 를 넣어야 한다.

 

Go 에서는 타입을 선언할 때는 { 앞에 공백을 넣지만, 값을 초기화할 때는 { 앞에 공백을 넣지 않는다. 또한 타입을 선언할 때는 필드 사이에 , 가 없지만, 값을 초기화 할 때는 필드값 사이에 , 를 넣는다.

jina := person{
	name: "Jina",
	age:  21,
}
fmt.Println(jina)

jamie := person{
	"Jamie",
	18,
}
fmt.Println(jamie)

--- output ---
{Jina 21}
{Jamie 18}

 

 

초기화는 아래와 같이 할 수 도 있다.

 

단, jina[”age”] 와 같이 인덱싱을 통한 접근은 지원하지 않는다.

var jina person
jina.name = "Jina"
jina.age = 21
// jina["age"] = 21  // 인덱싱은 지원하지 않음

fmt.Println(jina.name)

--- output ---
Jina

 

 

Anonymous struct

type 이 없는 struct 도 가능하다.

 

이 때는 type 키워드를 쓰지 않는다.

 

아래와 같이 바로 jina 라는 변수를 struct 형태로 지정하고 . 을 통해 값을 지정할 수 있다. 혹은 {} 를 사용하여 jamie 변수를 선언하면서 초기 값을 지정할 수 있다.

var jina struct {
		name string
		age  int
}

jina.name = "Jina"
jina.age = 21
fmt.Println(jina)

jamie := struct {
	name string
	age  int
}{
	name: "Jamie",
	age:  18,
}
fmt.Println(jamie)

--- output ---
{Jina 21}
{Jamie 18}

 

 

Struct 비교

go 에서는 type 이 같으면 대입(=), 비교(==) 를 할 수 있다. 하지만 type 이 다르면 비교나 대입을 할 수 없다.

 

하지만 type 을 convert 하여 맞춰 준다면 대입, 비교가 가능하다.

 

jina 와 jina2 는 type 이 같고 필드 값도 같다. 그래서 비교를 하면 true 값이 나온다.

 

하지만 jamie 는 jina 와 type 이 같지만 필드 값이 달라서 비교를 하면 false 값이 나온다.

jina := person{
	name: "Jina",
	age:  21,
}

jina2 := person{
	name: "Jina",
	age:  21,
}
fmt.Println(jina == jina2)

jamie := person{
	name: "Jamie",
	age:  18,
}
fmt.Println(jina == jamie)

--- output ---
true
false

 

 

 

새로운 anotherPerson type 을 만들어서 살펴보자.

 

아래와 같이 jina 는 person type 이고 jamie 는 anotherPerson type 이므로 서로 type 이 달라서 비교나 대입을 하면 에러가 난다.

type anotherPerson struct {
	name string
	age  int
}

jina := person{
	name: "Jina",
	age:  21,
}
fmt.Println(jina)

jamie := anotherPerson{
	name: "Jamie",
	age:  18,
}

fmt.Println(jina == jamie)

--- output ---
./main.go:88:19: invalid operation: jina == jamie (mismatched types person and anotherPerson)

 

 

 

이번에는 jamie 를 person type 으로 바꾸는 type converson 을 하여 비교하여 보자.

 

타입 변환도 잘 되고 값도 정상적으로 대입되는 것을 알 수 있다.

type anotherPerson struct {
	name string
	age  int
}

jina := person{
	name: "Jina",
	age:  21,
}
fmt.Println(jina)

jamie := anotherPerson{
	name: "Jamie",
	age:  18,
}

jina = person(jamie)
fmt.Println(jina)

--- output ---
{Jina 21}
{Jamie 18}

 

 

 

한가지 중요한 점은 struct 사이에 타입 변환은 타입, 필드명, 필드 순서, 필드 갯수 가 모두 동일해야 가능하다.

 

그럼 type 으로 선언된 struct 와 anonymous struct 는 타입을 제외한 필드명, 필드 순서, 필드 갯수 가 같다면 대입이나 비교, 타입 변환이 가능할까?

 

정답은 가능하다이다.

jina := person{
	name: "Jina",
	age:  21,
}
fmt.Println(jina)

var jamie struct {
	name string
	age  int
}

jamie = jina
fmt.Println(jina == jamie)

--- output ---
{Jina 21}
true

 

jamie := struct {
	name string
	age  int
}{
	name: "Jamie",
	age:  18,
}

var jina person

jina = person(jamie)
fmt.Println(jina)
fmt.Println(jina == jamie)

--- output --
{Jamie 18}
true

 

 

사실 person(jamie) 로 타입 변환을 하지 않아도 정상적으로 동작한다.

반응형
Posted by seungkyua@gmail.com
,