반응형

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
,
반응형

언어를 배울 때 가장 기본적인 것 중에 하나가 비교문(if), 반복문(for), 함수(function) 이다.

 

 

If 문

if 는 비교 구문에서 사용되는 키워드 이다. 변수 선언과 동시에 비교를 할 수 있는데 이 때 선언된 변수는 해당 if 문 block 안에서만 유효하다. 그렇기 때문에 마지막 라인은 num 변수는 인식하지 못해 에러가 난다.

func main() {
	if num := 5; num == 0 {
		fmt.Println("False")
	} else if num < 10 {
		fmt.Println("True")
	} else {
		fmt.Println("Big number")
	}
	fmt.Println(num)
}

--- output ---
./main_05_for_function.go:17:14: undefined: num

 

 

 

if 문 밖에서 선언한 변수는 if 문 안과 밖 모두에서 사용할 수 있다. 하지만 if 문 안에서 같은 변수명을 재정의 하면 해당 변수 값은 오버라이드 된다.

func main() {
	num := 10
	if num > 0 {
		fmt.Println("outer num =", num)
		num := 0
		fmt.Println("inner num =", num)
	}
	fmt.Println("outer num =", num)
}

--- output ---
outer num = 10
inner num = 0
outer num = 10

 

 

For 문

for lloop 는 반복문이며 slice 나 map 타입에 대해서 range 를 사용하여 item 을 하나씩 가져온다. slice 를 range 로 가져오면 index, value 2개의 값으로 넘어온다. value 만을 사용하고 싶으면 index 부분은 _ 로 처리하여 버릴 수 있다. 또한 index 1개의 값만 가져올 수 있다.

func main() {
	nums := []int{10, 20, 30, 40, 50}
	for i, v := range nums {
		fmt.Println("index =", i, ",", "value =", v)
	}
	for _, v := range nums {
		fmt.Println("value =", v)
	}
	for i := range nums {
		fmt.Println("index =", i)
	}
}

--- output ---
index = 0 , value = 10
index = 1 , value = 20
index = 2 , value = 30
index = 3 , value = 40
index = 4 , value = 50
value = 10
value = 20
value = 30
value = 40
value = 50
index = 0
index = 1
index = 2
index = 3
index = 4

 

 

 

map 도 slice 와 마찬가지로 range 로 가져올 수 있다. 이 때 return 되는 값은 key, value 이다.

func main() {
  m := map[string]int{
		"a": 1,
		"b": 2,
		"c": 3,
	}
	for k, v := range m {
		fmt.Println("key =", k, ",", "value =", v)
	}
	for _, v := range m {
		fmt.Println("value =", v)
	}
	for k := range m {
		fmt.Println("key =", k)
	}
}

--- output ---
key = c , value = 3
key = a , value = 1
key = b , value = 2
value = 2
value = 3
value = 1
key = a
key = b
key = c

 

 

Switch

선택문은 switch 로 가능하다. 각 case 마다 break 문이 없어도 되며, case 에는 , 로 여러 개의 값을 지정할 수 있다.

func main() {
  alpha := []string{"a", "ab", "abc", "abcd", "abcde", "abcdef", "abedefg"}
	for _, a := range alpha {
		switch l := len(a); l {
		case 1, 2:
			fmt.Println(a, "length = 2 or 3")
		case 3:
			fmt.Println(a, "length = 3")
		case 4, 5, 6:
			fmt.Println(a, "length = 4, 5 or 6")
		default:
			fmt.Println(a, "length > 6")
		}
	}
}

--- output ---
a length = 2 or 3
ab length = 2 or 3
abc length = 3
abcd length = 4, 5 or 6
abcde length = 4, 5 or 6
abcdef length = 4, 5 or 6
abedefg length > 6

 

 

Function

함수는 func, function name, parameter, return value 로 만들 수 있다. 아래 2개의 숫자를 받아서 나누는 div 함수는 아래와 같이 정의 할 수 있다. parameter 가 같은 타입이면 앞의 타입은 생략할 수 있다.

func main() {
  result := div(4, 2)
	fmt.Println(result)
}

// func div(n int, d int) int { 는 n, d 파라미터의 타입이 같아 아래와 같이 바꿀 수 있음
func div(n, d int) int {
	if d == 0 {
		return 0
	}
	return n / d
}

--- output ---
2

 

 

 

, 로 구분하여 여러 개의 아규먼트를 넘길 수 있으며 이 때 받는 함수에서는 ... 를 활용하여 가변 파라미터로 받을 수 있다. 또한 slice... 와 같이 slice 를 가변인자로 보낼 수 있다.

func main() {
  fmt.Println(addTo(1))
	fmt.Println(addTo(1, 2))
	fmt.Println(addTo(1, 2, 3))
	i := []int{4, 5}
	fmt.Println(addTo(3, i...))   // slice를 가변인자로 보내기
	fmt.Println(addTo(6, []int{7, 8, 9}...))
}

// 가변인자 (variadic parameter)
func addTo(base int, vals ...int) []int {
	result := make([]int, 0, len(vals))
	for _, v := range vals {
		result = append(result, base+v)
	}
	return result
}

--- output ---
[]
[3]
[3 4]
[7 8]
[13 14 15]

 

 

 

Function 의 리턴 값을 여러 개로 할 수 있다. (multi return values)

func main() {
  result, remainder, err := multiReturnValues(10, 5)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(result, remainder)
}

// multiple retrun values
func multiReturnValues(n, d int) (int, int, error) {
	if d == 0 {
		return 0, 0, errors.New("cannot divid by zero")
	}
	return (n / d), (n % d), nil
}

--- output ---
2 0

 

 

 

Function 은 변수에 담을 수 있다. cal 함수 내부에 보면 map 의 value 로 Function 타입을 선언하여 할당한 것을 볼 수 있다. Function 을 변수로 선언할 수 있고, 파라미터로 받을 수 있고 return value 로도 사용할 수 있다.

func main() {
  exp := []string{"3", "+", "4"}
	result, err := cal(exp)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(result)
}

func cal(exp []string) (int, error) {
	// Function Type
	type opFuncType func(int, int) int
	var opMap = map[string]opFuncType{
		"+": add,
		"-": sub,
	}

	a, err := strconv.Atoi(exp[0])
	if err != nil {
		fmt.Println(err)
		return 0, err
	}

	b, err := strconv.Atoi(exp[2])
	if err != nil {
		fmt.Println(err)
		return 0, err
	}

	op := exp[1]
	opFunc, ok := opMap[op]
	if !ok {
		fmt.Println("unsupported operator: ", op)
		return 0, err
	}
	return opFunc(a, b), nil
}

func add(a, b int) int {
	return a + b
}

func sub(a, b int) int {
	return a - b
}

--- output ---
7

 

 

 

struct 와 마찬가지로 Function 도 anonymous 로 만들 수 있다.

func main() {
  for i := 0; i < 5; i++ {
		func(j int) {
			fmt.Println(j)
		}(i)
	}
}

--- output ---
0
1
2
3
4

 

 

 

Function 안에는 Function 을 중첩해서 선언할 수 있는데 이 것을 clousure (클로저)라 한다. 클로저는 앞에서 설명한 Function 을 파라미터와 리턴 값으로 사용할 수 있는 성질을 많이 이용한다.

 

클로저는 변수 값이 사라지지 않고 계속 유지되는 특성을 갖는다. 아래 multiple(2) 을 호출 할 때 생성된 파라미터 base 변수 에는 2가 할당 되고 funcA(3) 을 호출 할 때도 계속 값이 남아 있어 base * factor 의 값은 2 * 3 이 되어 6 이 출려된다.

func main() {
  funcA := multiple(2)
	fmt.Println(funcA(3))

	funcA = multiple(4)
	fmt.Println(funcA(5))
}

// high-ooder fuctnion
func multiple(base int) func(int) int {
	return func(factor int) int {
		return base * factor
	}
}

--- output ---
6
20

 

 

 

함수를 호출할 때 모든 것은 Call by Value 이다. 아래과 같이 기본 타입이나 Struct 타입은 함수를 통해 넘겨받은 변수의 값을 변경하더라도 원래의 값은 변경되지 않는다. 즉, 아래와 같이 person 을 modify 함수를 통해 넘긴 다음, modify 함수에서 값을 수정해도 원래의 person 의 값은 수정되지 않는다.

func main() {
  i := 1
	s := "Hello"
	p := person{}
	modify(i, s, p)
	fmt.Println(i, s, p)
}

type person struct {
	name string
	age  int
}

// Call by Value (여기서 수정해도 원래 변수 값은 수정되지 않는다)
func modify(i int, s string, p person) {
	i = i + 1
	s = "modified"
	p.name = "ask"
	p.age = 100
}

--- output ---
1 Hello { 0}

 

 

 

그러나 포인터로 넘기면 원래의 값도 수정할 수 있다. 포인터는 다음에 설명하기로 하고 여기서는 slice 와 map 이 포인터와 같이 작동한다고 이해하면 된다.

 

한가지 중요한 것이 있다. modSlice(s) 에서의 s 변수 가 가리키는 주소와 modSlice(s []int) 함수에서 s 변수 가 가리키는 주소는 동일하다. (마치 포인터 처럼 동작한다) 하지만 s 변수 의 경우 modSlice 함수 통해 넘기는데 처음 생성할 때 길이:2, 크기:2 로 생성했다. 즉, 길이와 크기가 같아 modSlice 함수에서 append 로 item 을 추가할 경우 크기를 늘린 새로운 slice 를 만들어서 리턴 된다.

 

s = append(s, 3) 에서 s 가 가리키는 주소는 처음 modSlice(s []int) 일 때 가리키는 주소가 아니라 크기가 늘어나 새롭게 생성된 slice 를 가리키게 된다. 그래서 그 이전에 수정된 s 의 값은 변화가 있고, 그 이후에 수정된 s 의 값은 변화가 없다.

func main() {
  m := map[string]int{
		"one": 1,
		"two": 2,
	}
	modMap(m)
	fmt.Println(m)

	s := []int{1, 2}
	modSlice(s)
	fmt.Println(s)
}

// Call by Value but slice and map like pointer
func modMap(m map[string]int) {
	m["two"] = 20
	m["three"] = 3
	delete(m, "one")
}

func modSlice(s []int) {
	s[0] = s[0] * 10
	s = append(s, 3)  // 길이 == 크기 이므로 새로운 크기의 slice 가 만들어져서 리턴된다
	s[1] = s[1] * 10
}

--- output ---
map[three:3 two:20]
[10 2]

 

반응형
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
,
반응형

 

Map 은 key, value 로 저장할 수 있는 타입이다. HashMap 을 사용하기 때문에 key 는 유일한 값으로 key 를 사용하면 value 를 바로 찾아서 쓸 수 있다.

앞서 slice 와 동일하게 zero 값은 nil 이다. 그러므로 map 변수를 선언만 하면 nil 값으로 비교할 수 있다. slice 는 nil 값이라도 append 를 사용하면 값을 추가하면서 새로운 slice 가 생성되어 return 되었다. 하지만 map 은 nil 값에는 어떠한 key, value 도 추가할 수 있다.

 

Map

map 은 key, value 쌍으로 이루어진 데이터 구조이다. 파이썬에서는 dictionary 와 비슷하다고 생각하면 된다. 단지 다른 것은 하나의 map 에는 사전에 지정된 타입의 key, value 만 저장될 수 있다.

다음은 string 타입을 key 로하고 int 를 value 로 저장하는 map 을 선언한 것이다. map[key타입]value타입 으로 선언할 수 있다.

이렇게 선언만 한 map 변수의 zero 값은 nil 이다.

var m map[string]int
fmt.Println(m == nil)

--- output ---
true

 

이렇게 nil 값이 상태의 map 에는 key, value 를 추가할 수 없다(slice 와는 다르다).

var m map[string]int
m["one"] = 0

--- output ---
panic: assignment to entry in nil map

 

아래와 같이 empty map 을 생성할 수 있으며, 여기에는 key, value 를 추가할 수 있다.

var m1 = map[string]int{}
fmt.Println(m1 == nil)
m1["one"] = 0
fmt.Println(m1)

--- output ---
false
map[one:0]

 

초기 값을 아래와 같이 생성할 수 도 있다. 초기화 할 때 key, 와 value 는 : 로 구분하고 연속적으로 key, value 를 입력하기 위해서는 , 구분자를 사용한다.

m2 := map[string]int{
    "one":  1,
    "two": 2,
}
fmt.Println(m2)

--- output ---
map[one:1 two:2]

 

make 함수

slice 와 동일하게 make 내장 함수를 사용하여 empty map 을 생성할 수 있다. 이렇게 생성된 map 은 길이는 0, 사이즈는 5 가 되고, 길이가 사이즈 보다 커질 때에도 사이즈가 확장되어 key, value 를 추가할 수 있다. (사이즈 확장은 때에 따라 성능 감소의 요인이 될 수 있다)

출력 결과와 같이 map 에 추가된 key, value 는 순서가 없다.

m := make(map[string]int, 5)
fmt.Println(len(m))

m["one"] = 1
m["two"] = 2
m["three"] = 3
m["four"] = 4
m["five"] = 5
fmt.Println(m, len(m))

m["six"] = 6
fmt.Println(m, len(m))

--- output ---
0
map[five:5 four:4 one:1 three:3 two:2] 5
map[five:5 four:4 one:1 six:6 three:3 two:2] 6

 

comma ok idiom

map 에서 없는 key 값으로 value 를 요청하면 value 타입의 zero 값이 리턴된다. 즉 에러가 발생하지 않는다.

m := map[string]int{
    "one": 1,
    "two": 2,
}
fmt.Println(m["three"])

--- output ---
0

 

그렇기 때문에 해당 key 값의 value 가 제대로 넘어왔는지 알 수 있게 , 로 구분되어 value 와 boolen 값이 리턴된다.

m := map[string]int{
    "one": 1,
    "two": 2,
}

v, ok := m["one"]
fmt.Println(v, ok)

v, ok = m["three"]
fmt.Println(v, ok)

--- output ---
1 true
0 false

 

map 의 item 을 삭제하려면 delete 내장 함수 를 사용한다. delete 함수에 첫번째 아규먼트에는 map 을, 두번째 아규먼트에는 key 를 넣으면 된다. 해당 key 가 map 에 없다고 해서 에러를 만들지는 않는다.

m := map[string]int{
    "one": 1,
    "two": 2,
}

delete(m, "one")
fmt.Println(m)
delete(m, "three")

--- output ---
map[two:2]

 

map 의 key 는 중복될 수 없다. key, value 를 추가하는데 기존의 map 에 key 가 존재한다면, key 에 해당 하는 value 값이 업데이트 된다.

Go 에는 Set 타입이 없는데 참고로 key 가 중복될 수 없다는 이 특징을 잘 활용하면 Set 을 구현할 수 있다.

m := map[string]int{
    "one": 1,
    "two": 2,
}
m["two"] = 3
fmt.Println(m)

--- output ---
map[one:1 two:3]

 

 

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

 

 

기본 내장 타입에 추가하여 Array, Slice, Map 타입에 대해서 살펴본다.

Go 에서는 Array 는 거의 사용하지 않는다. 그러므로 이번에는 Slice 와 Map 타입에 대해서만 살펴 본다.

참고로 Go 에서는 nil 이라는 값이 있다. 이는 값이 없다라는 뜻인데, 기본 타입에는 이 nil 과의 비교문 (==)을 쓸 수 없다. (이전에 기본 타입은 전부 zero 값이 nil 이 아니다.)

**nil 과 비교문이 가능한 타입은 앞으로 설명할 slice, map, pointer 이며, 이 3개의 zero 값이 nil 이다.**

 

Slice

slice 는 값을 순차적으로 가질 수 있는 데이터 구조이다. 파이썬에서는 list 와 비슷하다고 생각하면 된다. 단지 다른 것은 하나의 slice 에는 사전에 지정된 타입만 저장될 수 있다.

다음은 int 타입을 slice 저장 한 후에 출력한 결과이다. [] 는 slice 를 만들겠다는 의미이고, 바로 이어서 type 이 오며, 초기값을 지정하기 위해서 { } 를 사용한다.

var s = []int{1, 2, 3, 4, 5}
fmt.Println(s)

--- output ---
[1 2 3 4 5]

 

slice 는 초기화 값을 지정하지 않으면 nil 을 나타낸다. 즉, 변수 선언만 하면 항상 nil 값을 갖게 된다. lencap 을 사용하면 slice 가 가지가 있는 길이와 용량을 알 수 있다.

Slice 는 연속적인 값을 가지고 있는 메모리 주소인 포인터, 길이, 용량 으로 구성되어 있다. (이 부분은 매우 중요하다.)

길이는 실제 들어있는 값이고 용량은 해당 값을 가질 수 있는 크기라고 할 수 있다.

아래 s1 은 nil 값이기 때문에 len 과 cap 이 모두 0 이다.

var s1 []int
fmt.Println(s1 == nil)
fmt.Println(len(s1), cap(s1))

--- output ---
true
0 0

 

슬라이스는 배열처럼 사용하여 s1[0] 첫번째 값을 가져올 수 있다.

하지만 선언만 하여 nil 인 상태에서는 해당 index 를 사용하게 되면 런타임 에러가 난다.

var s1 []int
fmt.Println(s1[0])

--- output ---
panic: runtime error: index out of range [0] with length 0

 

s1 slice 가 nil 인 상태에서도 append 내장 함수를 사용하면 값을 추가할 수 있다(그렇기 때문에 특별한 경우를 제외하고는 nil 값인 변수 선언만 하여 사용하면 된다). append 함수를 쓰면 nil 이라 하더라도 slice 에 값을 추가 할 수 있다. 물론 값이 추가된 결과 return 값은 변수에 할당해야 한다. (변수가 달라도 됨)

var s1 []int
s1 = append(s1, 1)
fmt.Println(s1)

--- output ---
[1]

 

slice 에 slice 를 합쳐서 추가할 수 있다. python 의 extend 와 비슷하다고 보면된다. 두번째 slice 에는 여러 아규먼트가 들어간다는 것을 표현해 주기 위해서 ... 을 붙혀야 한다.

var s1 []int
s1 = append(s1, 1)

s2 := []int{2, 3, 4, 5}
s1 = append(s1, s2...)  // append(s1, 2, 3, 4, 5) 와 동일함
fmt.Println(s1)

 

slice 는 값이 추가되어 길이가 커져서 용량에 도달하면 용량이 자동으로 확장된다.

var s []int
fmt.Println(s, len(s), cap(s))
s = append(s, 1)
fmt.Println(s, len(s), cap(s))
s = append(s, 2)
fmt.Println(s, len(s), cap(s))
s = append(s, 3)
fmt.Println(s, len(s), cap(s))
s = append(s, 4)
fmt.Println(s, len(s), cap(s))
s3 := append(s, 5)
fmt.Println("s:", s, len(s), cap(s))
fmt.Println("s3", s3, len(s3), cap(s3))

-- output ---
[] 0 0
[1] 1 1
[1 2] 2 2
[1 2 3] 3 4
[1 2 3 4] 4 4
s: [1 2 3 4] 4 4
s3 [1 2 3 4 5] 5 8

 

 

make 내장 함수

slice 와 map 에서 make 내장 함수를 사용하면 nil 이 아닌 empty slice 를 만들 수 있다. make 함수의 첫번째 아큐먼트는 slice type, 두번째는 길이, 세번째는 용량이다. 세번째 용량은 생략이 가능하다.

아래 make 로 생성한 s 는 nil 과의 비교가 false 임을 알 수 있다.

s := make([]int, 0, 5)
fmt.Println(s == nil)
fmt.Println(s, len(s), cap(s))

--- output ---
false
[] 0 5

 

make 로 길이를 지정하면 그 길이 만큼은 zero 값이 채워진 slice 가 만들어 진다. 그러니 필요없이 길이를 1 이상으로 지정하면 초기 메모리만 차지할 수 있다. 아래는 make 로 만든 s1 slice 에 0번째와 1번째 index 까지 0 값이 채워진 것을 볼 수 있다.

s1 := make([]int, 2)
fmt.Println(s1)

s1 = append(s1, 1)
fmt.Println(s1)

--- output ---
[0 0]
[0 0 1]

 

길이가 0 인 slice 는 아래와 같이 만들 수 도 있다. nil 이 아니면서 길이가 0 인 slice 는 JSON 으로 변환할 때 사용된다.

var s = []int{}   // make([]int, 0) 와 동일하다.

 

 

Slice 자르기

Slice 를 자르는 것은 : 을 이용해서 자를 수 있다. 하지만 자른다고 해도 최종적으로 보는 Reference 는 동일하다. 아래를 보면 이를 이해할 수 있다.

첫번째 단락에서 slice 를 자르면, b 는 0 index 이상, 3 index 미만 까지의 slice 를 갖고, c 는 2 index 이상 끝까지의 slice 값을 갖는다. 중요한 것은 c 처럼 중간에서 마지막까지 자른 slice 는 용량이 그 길이만큼으로 줄어든다는 것이다. (앞의 자른 부분이 없어졌기 때문이다)

두번째 단락에서 c[0] 에 30 을 대입하면 slice 자른 것은 같은 reference 를 바라보기 때문에 a 와 b 에 모두 영향을 준다.

세번째 단락도 마찬가지 이다.

a := []int{1, 2, 3, 4, 5}
b := a[:3]
c := a[2:]
fmt.Println("cap(a):", cap(a), "cap(b):", cap(b), "cap(c):", cap(c))
fmt.Println("a:", a)
fmt.Println("b:", b)
fmt.Println("c:", c)
fmt.Println("================")

c[0] = 30
fmt.Println("a:", a)
fmt.Println("b:", b)
fmt.Println("c:", c)
fmt.Println("================")

b = append(b, 40)
fmt.Println("a:", a)
fmt.Println("b:", b)
fmt.Println("c:", c)

--- output ---
cap(a): 5 cap(b): 5 cap(c): 3
a: [1 2 3 4 5]
b: [1 2 3]
c: [3 4 5]
================
a: [1 2 30 4 5]
b: [1 2 30]
c: [30 4 5]
================
a: [1 2 30 40 5]
b: [1 2 30 40]
c: [30 40 5]

 

Slice 를 자른 것이 append 를 사용해서 추가할 때 같은 레퍼런스를 처다보지 않게 하려면, 용량을 길이와 동일하게 하면 된다. 그러면 append 했을 경우 용량이 초과되어 새로운 slice 가 생겨서 return 되므로 서로 영향이 가지 않는다.

아래 처럼 a 를 잘라서 b 를 만들 때 :3:3 과 같이 마지막에 용량 3을 추가하여 b slice 의 길이와 용량을 똑같이 맞췄다. 그렇게 되면 c[0] 처럼 값을 바꾸는 것은 영향을 받지만, append(b, 40) 과 같이 append 한 것은 새로운 slice 가 만들어져서 return 되므로 a 와 c 에 영향을 주지 않는다.

a := []int{1, 2, 3, 4, 5}
b := a[:3:3]
c := a[2:]
fmt.Println("cap(a):", cap(a), "cap(b):", cap(b), "cap(c):", cap(c))
fmt.Println("a:", a)
fmt.Println("b:", b)
fmt.Println("c:", c)

fmt.Println("===== (c[0] = 30) =====")
c[0] = 30
fmt.Println("cap(a):", cap(a), "cap(b):", cap(b), "cap(c):", cap(c))
fmt.Println("a:", a)
fmt.Println("b:", b)
fmt.Println("c:", c)

fmt.Println("===== append(b, 40) ======")
b = append(b, 40)
b[0] = 10
fmt.Println("cap(a):", cap(a), "cap(b):", cap(b), "cap(c):", cap(c))
fmt.Println("a:", a)
fmt.Println("b:", b)
fmt.Println("c:", c)


--- output ---
cap(a): 5 cap(b): 3 cap(c): 3
a: [1 2 3 4 5]
b: [1 2 3]
c: [3 4 5]
===== (c[0] = 30) =====
cap(a): 5 cap(b): 3 cap(c): 3
a: [1 2 30 4 5]
b: [1 2 30]
c: [30 4 5]
===== append(b, 40) ======
cap(a): 5 cap(b): 6 cap(c): 3
a: [1 2 30 4 5]
b: [10 2 30 40]
c: [30 4 5]
c: [30 4 5]
===== append(b, 40) ======
a: [1 2 30 4 5]
b: [1 2 30 40]
c: [30 4 5]

 

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

 

 

C 와 Java 를 안다면 Go 를 공부하면 헷갈리는 부분들이 다소 있다. 일반적으로 기본 내장 타입은 비슷해서 이해하기 쉽지만 pointer 와 interface 등을 공부하다 보면 헷갈리기 마련이다.

일단 먼저 기본 타입에 대해서 설명한다.

 

Built-in Type

기본 내장 타입에는 boolean, integer, rune, float, byte, string 이 있다. 물론 complex type 도 있지만 이는 생략하겠다. 기본 내장 타입은 zero 값을 갖고 있다.

이 중에서 숫자 타입은 integer, rune, float 이다.

 

boolean

boolean 의 zero 값은 false 이다.

var isDev bool
var isTrue = true

fmt.Println(isDev)
fmt.Println(isTrue)

--- output ---
false
true

변수 선언은 var 변수명 타입 으로 선언할 수 있다. 이렇게 설명하면 zero 값으로 false 가 자동 대입된다.

다른 방법은 var 변수명 = 값 과 같이 변수를 선언하면서 값을 대입하는 것이다. 그럼 값을 보고 알아서 타입을 결정해 준다.

 

Integer (숫자 타입 중 하나)

Integer type 은 int8, int16, int32, int64, uint8, uint16, uint32, uint64 이다. 그리고 특별한 integer 타입으로 byterune 타입이 있다.

byteuint8 의 별칭으로 unit8 보다는 byte 를 주로 쓰게 된다.

runeint32 의 별칭이다.

보통 Integer Type 은 int 로 많이 사용하는데, 32bit cpu 이면 int32, 64bit cpu 이면 int64 와 동일하다.

int 의 zero 값은 0 이다.

var x int
var y int = 1
var z = 2
fmt.Println(x)
fmt.Println(y)
fmt.Println(z)

--- output ---
0
1
2

 

Float (숫자 타입 중 하나)

Float type 은 float32, float64 이 있다.

float type 의 zero 값은 int 와 동일하게 0 이다.

var p float64
var q = 0.0
fmt.Println(p)
fmt.Println(q)

--- output ---
0
0

숫자 타입 간에는 편하게 해당 타입으로 변환이 가능하다.

var x int = 10
var p float64 = 20.2
fmt.Println(x + int(p))
fmt.Println(float64(x) + p)

--- output ---
30
30.2

 

String

string 의 zero 값은 empty string 이다. Java 에서는 null string 이 있지만 Go 에서는 null 이 아니다.

string 과 자주 사용되는 타입은 runebyte 이다.

앞에서 숫자 타입은 서로 타입 변환이 해당 타입으로 감싸주면 된다고 설명했는데 string, rune, byte 도 서로 타입 변환이 가능하다.

Go 최신 버전에서는 숫자 타입을 단순히 string 으로 감싸는 것은 체크 시에 에러가 발생한다.

var r rune = 'r'
var s string = string(r)
var b byte = 'b'
var s2 string = string(b)
fmt.Println(r)
fmt.Println(b)
fmt.Println(s)
fmt.Println(s2)

--- output ---
114
98
r
b

runebyteint 타입의 별칭이라 숫자 값이 출력된 것을 이해하자.

나중에 설명하겠지만 [] 를 사용하여 slice (배열과 비슷한) 타입을 활용하면 string[]byte 혹은 []rune 으로 변환할 수 있다.

var s string = "Hello, 😀"
var bs []byte = []byte(s)
var rs []rune = []rune(s)
fmt.Println(bs)
fmt.Println(rs)

--- output ---
[72 101 108 108 111 44 32 240 159 152 128]
[72 101 108 108 111 44 32 128512]

😀 이모지 값은 utf-8 으로 표시해야 되기 때문에 byte 는 [240 159 152 128] 와 같이 4 바이트로 표시되고 rune 은 [128512] 로 표시된다. byte 는 int8 이고, rune 은 int32 임을 기억하자.

이렇게 보면 string 은 rune 으로 변환하는 것이 편리하다.

마지막으로 Short Variable Delcaration Operator(:=) 를 사용하여 vartype 키워드 없이 변수를 선언하고 바로 할당할 수 있다.

var a, b int = 1, 2
var c, d = 3, "a"

i := 10
x, s := 20.5, "Hello World"

--- output ---
1 2 3 a
10 20.5 Hello World

 

 

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

 

 

Kubernetes source code 를 디버깅하면서 분석하기 위해 golang 환경 설정과 사용을 계속해서 설명하고자 한다.

 

오늘은 첫번째로 무료 IDE 인 (thx MS) VSCode 를 설치하고 세팅하는 법을 알아보자.

golang 설치

mac 에서 golang 은 home brew 로 간단히 설치할 수 있다. 혹시 라도 이전에 낮은 버전이 설치되어 있는지 확인한다.

$ brew list | grep go
$ go@1.12

$ go version
go version go1.12.9 darwin/amd64

새로운 버전을 install 한다.

$ brew install go@1.16

현재 1.12 버전을 unlink 한다.

$ brew unlink go

새로운 버전을 link 한다.

$ brew link --force go@1.16

새로운 버전을 check 하면 업그레이드 되어 있는 것을 알 수 있다.

$ go version
go version go1.16.14 darwin/amd64

Go Workspace

go 는 install 명령어로 다른 go tool 들을 설치할 수 있다. 설치 위치는 workspace 라고 부르는 곳인데 특별히 지정하지 않으면 디폴트로 $HOME/go 디렉토리에 설치 된다.

 

go install 로 설치된 툴들은 소스는 $HOME/go/src 에, 바이너리 실행파일은 $HOME/go/bin 에 설치된다.

 

workspace 는 변경할 수 있는데 아래와 같이 .zshrc (zsh 일 경우) 이나 .bash_profile (bash 일 경우) 파일에 환경 변수를 추가하면 된다. 설정하기 전에 해당 workspace 디렉토리는 미리 만들어 줘야 한다.

## 디렉토리 만들기
$ mkdir -p $HOME/Documents/go_workspace

## .zshrc 파일에 환경변수 세팅
export GOPATH=$HOME/Documents/go_workspace
export PATH=$PATH:$GOPATH/bin

Go Tool 설치

코드를 체크해 주는 golint 툴이 있다. 이를 설치해 보자

$ go install golang.org/x/lint/golint@latest

$ which golint
/Users/ahnsk/Documents/go_workspace/bin/lint

특정 디렉토리에서 lint 체크를 하고 싶으면 아래 명령어를 쓰면 된다. 코드를 만든 프로젝트 디렉토리에서 수행하면 모든 파일을 lint 한다.

$ golint ./...

그리고 go 를 설치하면 일부 tool 들은 자동으로 설치되는데 그 중 vet 이라는 유용한 tool 있다. vet 은 error 를 잡아주는 tool 이다.

$ go vet ./...

VS Code 설치

VS Code 는 아래 url 에 접속해서 OS 별로 설치하면 된다. 설치 화면은 생략한다.

 

https://code.visualstudio.com/Download

 

VS Code 를 띄우고 나서 아무 화면에서나 Cmd+Shift+P 키를 입력하면 Command Palette 가 화면 위에 생성된다. 거기에서 shell command 로 검색하여 install ‘code’ command in PATH 를 선택한다.

 

 

이렇게 설치한 후 터미널을 새롭게 띄워서 특정 디렉토리에서 code 명령어를 이력하면 VS Code 가 실행된다.

 

아래와 같이 go-sample 디렉토리에서 실행하면 VS Code 가 뜨는 것을 볼 수 있다.

$ cd go-sample
$ code

VS Code Extension 설치

VS Code 는 다양한 Extension 을 설치할 수 있다. 그 중에 go 를 써야 하니 Go extension 을 설치해 보자.

 

VS Code 화면에 보면 맨 좌측에 세로로 아이콘들이 여러개 있다. 그 중 위에서 5번째 혹은 6번째에 있는 extension 아이콘을 선택하고 go 를 검색해서 go Team at Google 에서 만든 Go 를 설치해 준다.

 

아래 그림에서 맨 처음 줄의 Go 의 install 파란 버튼을 클릭해서 설치한다.

 

 

이제 기본 설치는 끝났다.

 

hello world 를 출력하는 간단한 소스를 만들어서 실행해 보자.

 

VS Code 에서 새로운 파일을 생성해서 main.go 파일을 만들어서 저장한다.

package main

import "fmt"

func main() {
	fmt.Println("Hello world!!!")
}

VS Code 상단 메뉴에 Terminal >> New Terminal 을 클릭하면 터미널을 VS Code 에서 사용할 수 있다.

$ go run main.go
Hello world!!!

VS Code 화면을 보면 다음과 같다.

 

 

이제 세팅은 모두 끝났다. 디버깅 세팅하는 방법이 있는데 go 를 설명하다가 어느 정도 지나면 디버깅 세팅도 추가로 올릴 예정이다.

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

Golang 으로 개발된 애플리케이션을 Docker image 로 만드는 Dockerfile 이다.

이미지 사이즈를 줄이기 위해서 빌드 이미지와 실행 이미지를 구분하여 작성하였다.

 

실행 이미지를 더 작게 만들고 싶으면 base를 golang 이미지 대신 alphine 이미지로 만들면 된다.

 

한가지 tip 이라 하면 RUN go mod vendor 를 먼저 하고 나중에 COPY . . 를 하여 소스를 복사한 부분이다.

만약 소스 복사를 먼저하면 변경되 소스로 인해서 (하위 layer 의 변경) go mod vendor 로 다운로드 하는 부분을 수행하게 된다. 하지만 go mod vendor 를 먼저한 후 소스를 복사하면 go mod vendor 에서 수정된 부분이 없으면 해당 layer 를 재사용하기 때문에 매번 다운로드를 하지 않는다.  

 

# vi Dockerfile

FROM golang:1.16.3-stretch AS builder
LABEL AUTHOR Seungkyu Ahn (seungkyua@gmail.com)

RUN mkdir -p /build
WORKDIR /build

COPY go.mod .
COPY go.sum .
RUN go mod vendor
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/tksinfo ./cmd/server.go

RUN mkdir -p /dist
WORKDIR /dist
RUN cp /build/bin/tksinfo ./tksinfo




FROM golang:alpine3.13

RUN mkdir -p /app
WORKDIR /app

COPY --chown=0:0 --from=builder /dist /app/
EXPOSE 9111

ENTRYPOINT ["/app/tksinfo"]
CMD ["--port", "9111"]
반응형
Posted by seungkyua@gmail.com
,
반응형

2021년 4월 Golang 의 최신 버전은 1.16.x 입니다.

 

go 프로그램을 실행파일로 컴파일 하면 default 는 dynamic library 로 만들어 집니다.

 

아무리 OS 와 architecture 에 맞춰서 cross compile 을 했다고 해도 링크된 모듈이 존재하지 않으면 에러가 납니다.

특히 docker 로 실행할 경우 이런 문제를 자주 겪게 됩니다.

 

1.15 버전 이상에서는 CGO_ENABLED=0 환경 변수로 static library 로 만들 수 있습니다. 참고로 1.14 버전 이하에서는 -tags 옵션을 사용할 수 있습니다.

 

아래 Makefile 은 이 부분을 고려해서 만들었습니다.

GOOS는 OS 의 종류를 GOARCH 는 Architecture 에 맞춰 실행파일을 만듭니다.

 

$ vi Makefile

.PHONY: build clean docker

default: build

all: build docker

build: build-darwin build-linux

build-darwin:
	CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o bin/tksinfo-darwin-amd64 ./cmd/server.go
	CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o bin/tksinfo-appclient-darwin-amd64 ./examples/application/client.go

build-linux:
	CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/tksinfo-linux-amd64 ./cmd/server.go
	CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/tksinfo-appclient-linux-amd64 ./examples/application/client.go

clean:
	rm -rf ./bin

docker:
	docker build --no-cache -t seungkyua/tksinfo -f Dockerfile .
반응형
Posted by seungkyua@gmail.com
,
반응형

Kubernetes 는 GoLang 으로 만들어진 대표적인 s/w 입니다. Kubernetes  구조와 비슷하게 GoLang 으로 프로젝트를 만들 때 사용되는 일반적인 디렉토리 구조를 설명하겠습니다.

 

제일 먼저 GOPATH 를 지정하고, bin 디렉토리를 PATH에 추가로 지정합니다.

$ GOPATH=/Users/ahnsk/Documents/go_workspace
$ PATH=/Users/ahnsk/Documents/go_workspace/bin:$PATH

 

다음은 go get 으로 govendor 를 설치합니다. govendor 는 dependency module 을 쉽게 다운받고 관리할 수 있습니다.

$ go get -u github.com/kardianos/govendor
$ govendor -version
v1.0.9

 

이제 본인이 생성할 프로젝트를 만들어 보겠습니다. github.com 에 있는 go 프로젝트를 다운로드 받습니다.

$ mkdir -p /Users/ahnsk/Documents/go_workspace/cookiemonster2
$ GOPATH=/Users/ahnsk/Documents/go_workspace/cookiemonster2
$ go get -u github.com/seungkyua/cookiemonster2

GOPATH 는 각 프로젝트마다 자신만의 용도로 사용하기 위해 각각 지정하는 것이 편리합니다.

 

cookiemonster2 라는 프로젝트는 GOPATH 아래의 src 디렉토리에 패키지 경로(github.com/seungkyua/cookiemonster2)로 다운로드 됩니다.

/Users/ahnsk/Documents/go_workspace/cookiemonster2/src/
github.com/seungkyua/cookiemonster2

 

프로젝트 디렉토리로 이동해서 보면 다음과 같습니다.

$ tree -L 2 .
.
├── Dockerfile.cookiemonster
├── LICENSE
├── Makefile
├── README.md
├── cmd
│   ├── cluster_client.go
│   ├── cluster_client2.go
│   └── server.go
├── config
│   └── config.yaml
├── manifest
│   ├── cookiemonster-cm-config.yaml
│   ├── cookiemonster-deployment.yaml
│   ├── cookiemonster-rbac.yaml
│   └── cookiemonster-service.yaml
├── pkg
│   ├── domain
│   └── handler
└── vendor
    └── ...

 

일반적으로 entrypoint 가 되는 go 파일은 cmd 디렉토리 아래에 위치하고 나머지 go 파일들은 pkg 디렉토리에 패키지 구조로 위치합니다.

vendor 디렉토리는 dependency module 을 다운받은 곳이므로 이 디렉토리는 삭제를 하고, 처음부터 새롭게 구성해 보겠습니다.

$ rm -rf vendor

 

govendor init 으로 vendor 디렉토리를 생성합니다.  새롭게 생성된 vendor 디렉토리에 vendor.json 파일이 보이는데 이것은 향후 dependency module 을 다운 받을 때 활용됩니다.

$ govendor init

$ tree -L 2 .
.
├── Dockerfile.cookiemonster
├── LICENSE
├── Makefile
├── README.md
├── cmd
│   ├── cluster_client.go
│   ├── cluster_client2.go
│   └── server.go
├── config
│   └── config.yaml
├── manifest
│   ├── cookiemonster-cm-config.yaml
│   ├── cookiemonster-deployment.yaml
│   ├── cookiemonster-rbac.yaml
│   └── cookiemonster-service.yaml
├── pkg
│   ├── domain
│   └── handler
└── vendor
    └── vendor.json

 

vendor 디렉토리 밑으로 디펜던시 module 을 다운로드 받습니다.

$ govendor get github.com/seungkyua/cookiemonster2

$ tree -L 2 .
.
├── Dockerfile.cookiemonster
├── LICENSE
├── Makefile
├── README.md
├── cmd
│   ├── cluster_client.go
│   ├── cluster_client2.go
│   └── server.go
├── config
│   └── config.yaml
├── manifest
│   ├── cookiemonster-cm-config.yaml
│   ├── cookiemonster-deployment.yaml
│   ├── cookiemonster-rbac.yaml
│   └── cookiemonster-service.yaml
├── pkg
│   ├── domain
│   └── handler
└── vendor
    ├── appengine
    ├── appengine_internal
    ├── github.com
    ├── golang.org
    ├── google.golang.org
    ├── gopkg.in
    ├── k8s.io
    ├── sigs.k8s.io
    └── vendor.json

 

vendor 디렉토리에 dependency module 의 특정버전까지 다운 받을 수 있으니 관리가 편리해 집니다. 

현재 버전에서 labstack 의 echo v4 모듈에 버그가 있어 v3 로 다시 다운받기 위해 해당 디렉토리를 지우고 다시 다운 받을 수 있습니다. @v3 는 v3  이상의 최신 버전에 해당하는 git 브랜치나 태그를 다운받으라는 의미입니다.

$ rm -rf vendor/github.com/labstack/echo

$ govendor get github.com/labstack/echo@v3

 

여기서 govendor 의 버그로 echo 는 다운받았지만 하위 디렉토리인 echo/middleware 는 다운이 안되었으므로 추가로 vendor.json 을 수정하여 다운로드 받습니다.

$ vi vendor/vendor.json
{
   "checksumSHA1": "ynPXfBgVKquHSKkdFWk3oqSYf+g=",
   "path": "github.com/labstack/echo",
   "revision": "38772c686c76b501f94bd6cd5b77f5842e93b559",
   "revisionTime": "2019-01-28T14:12:53Z",
   "version": "v3.3.10",
   "versionExact": "v3.3.10"
},
{
   "checksumSHA1": "Rp/k+BJKpaeB9vyjEPFBW4LeFP8=",
   "path": "github.com/labstack/echo/middleware",
   "revision": "38772c686c76b501f94bd6cd5b77f5842e93b559",
   "revisionTime": "2019-01-28T14:12:53Z",
   "version": "v3.3.10",
   "versionExact": "v3.3.10"
}

$ govendor fetch github.com/labstack/echo/middleware

 

 

 

 

 

 

 

 

 

 

 

반응형
Posted by seungkyua@gmail.com
,