반응형

OPA 실행 파일 설치

$ curl -L -o opa <https://github.com/open-policy-agent/opa/releases/download/v0.62.1/opa_darwin_arm64_static>
$ chmod +x opa
$ mv opa ~/bin/

참고로 홈 디렉토리 아래의 bin 디렉토리에 실행 패스가 잡혀있기 때문에 해당 디렉토리로 이동시킨 것이다.

Intellij 와 Open Policy Agent Plugin 설치

JetBrain 의 IntelliJ 가 있다면 Open Policy Agent 플러그인을 설치하여 rego 프로그램을 작성할 수 있다.

hello 프로그램을 작성하여 실행하여 보자.

먼저 번들용 chap1 디렉토리를 만든다.

$ mkdir -p chap1

chap1 번들 아래에 hello.rego 파일을 만든다.

package hello

default allow_hello = false
default allow_world = false

allow_hello {
    "hello" != ""
}

allow_world {
    "world" != "world"
}

패키지 hello 는 디렉토리와 상관없으며 chap1 디렉토리를 만들었기 때문에 bundle 은 chap1 이다.

allow_hello 와 allow_world 는 rule 을 나타낸다. OPA 1.0 미만은 if 문이 없기 때문에 비교문 만으로 표현한다. OPA 1.0 부터는 if 문을 사용한다.

opa 실행은 input json 에 대한 output json 이 결과로 나오는데 input 소스를 보면 input 이 필요없기 때문에 빈 input.json 파일을 만든다.

$ touch input.json 

현재까지의 디렉토리 구조를 보면 다음과 같다.

rego-example.iml 은 IntelliJ 의 OPA 플러그인에서 사용하는 파일이므로 신경쓸 필요가 없다.

$ tree .
.
├── chap1
│   ├── hello.rego
│   └── input.json
└── rego-example.iml

이를 cli 로 실행하면 다음과 같다.

$ opa eval -f pretty -b chap1 data.hello    

--- output ---
{
  "allow_hello": true,
  "allow_world": false
}

opa eval 명령어로 rule 을 평가할 수 있다.

-f pretty 결과를 보기 쉽게 출력하라는 의미이다.

-b cha1 은 Bundle 을 입력해야 하는데 chap1 디렉토리 의 아래 rego 파일을 실행하다.

data.hello 는 Query 를 의미하며, 여기에는 package 나 rule 을 넣으면 된다. data 는 명시적으로 붙혀서 data.패키지 로 입력하면 된다.

allow_hello rule 을 실행하기 위해서는 query 부분에 rule 까지 넣어주면 된다.

$ opa eval -f pretty -b chap1 data.hello.allow_hello

--- output ---
true

결과 값으로 json 형태가 디퐅트로 출력되는데 -f pretty 를 제거하면 json 으로 결과가 출력된다.

$ opa eval -b chap1 data.hello

--- output ---
{
  "result": [
    {
      "expressions": [
        {
          "value": {
            "allow_hello": true,
            "allow_world": false
          },
          "text": "data.hello",
          "location": {
            "row": 1,
            "col": 1
          }
        }
      ]
    }
  ]
}

input 을 위해서 빈 input.json 파일을 만들었는데 이를 활용하는 명령을 추가할 수 있다.

input 값을 실제로 사용하지는 않기 때문에 결과 값은 동일하다.

$ opa eval -b chap1 -i chap1/input.json data.hello

--- output ---
{
  "result": [
    {
      "expressions": [
        {
          "value": {
            "allow_hello": true,
            "allow_world": false
          },
          "text": "data.hello",
          "location": {
            "row": 1,
            "col": 1
          }
        }
      ]
    }
  ]
}

실행을 cli 로 하지 말고 IntelliJ 에서 실행하는 방법은 아래와 같이 입력하면 된다.

메뉴에서 Run >> Edit Configurations... 을 실행한다.

앞에서 설명한 cli 에서 입력한 내용을 그대로 넣으면 된다.

Run 버튼을 클릭하면 다음과 같이 결과가 나온다.

소스는 아래 사이트에서 다운받을 수 있다. (계속 업데이트 될 예정)

https://github.com/seungkyua/rego-example.git

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

인터페이스가 다른 인터페이서를 가지는 임베딩 방식을 사용하여 인터페이스를 선언할 수 있다. 예를 들어 io.ReadCloser 인터페이스는 io.Reader 와 io.Closer 인터페이스를 가지고 있다.

type Reader interface {
        Read(p []byte) (n int, err error)
}

type Closer interface {
        Close() error
}

type ReadCloser interface {
        Reader
        Closer
}

이 경우 ReadCloser 인터페이스는 아래와 동일 효과를 갖는다.

type ReadCloser interface {
        Read(p []byte) (n int, err error)
        Close() error
}

그런데 인터페이스를 struct 타입 안에 임베딩할 수 도 있다.

이렇게 struct 타입 안에 인터페이스를 넣는 이유는 보통 Stub 으로 유닛 테스트 코드를 만들기 쉽기 때문이다.

아래와 같이 Calculator 라는 스트럭트가 있다고 하자.

type Calculator struct {
    Resolver MathResolver
}

여기에는 MathResolver 인터페이스 타입의 필드를 가지고 있다.

type MathResolver interface {
    Resolve(expression string) (float64, error)
}

이렇게 인터페이스를 만들어 놓으면 MathResolver 를 Stub 으로 구현하여 테스트 코드를 만들 수 있다.

Calculator 는 계산 표현식을 가지고 실제 계산하여 결과 값을 리턴하는 Process 라는 메소스도 가진다.

func (c Calculator) Process(r io.Reader) (float64, error) {
    expression, err := readOneLine(r)
    if err != nil {
        return 0, err
    }
    if len(expression) == 0 {
        return 0, errors.New("no expression to read")
    }
    answer, err := c.Resolver.Resolve(expression)
    return answer, err
}

readOneLine 함수는 계산 표현식을 한 줄만 읽어들이는 함수이며, 이렇게 읽어들인 함수를 MathResolver 타입의 Resolve 함수에 아규먼트로 넘겨서 결과를 받아오는 구조이다.

이제 Proecess 메소드에 대한 테스트 코드를 만들어 보자.

첫번째로 테스트 코드로 MathResolverStub 스트럭트와 Resolve 메소드를 간단히 구현한다.

type MathResolverStub struct{}

func (mr MathResolverStub) Resolve(expr string) (float64, error) {
    switch expr {
    case "2 + 4 * 10":
        return 42, nil
    case "( 2 + 4 ) * 10":
        return 60, nil
    case "( 2 + 4 * 10":
        return 0, fmt.Errorf("invalid expression: %s", expr)
    }
    return 0, nil
}

다음으로 이 스텁을 사용한 테스트 코드를 작성한다.

func TestCalculatorProcess(t *testing.T) {
    c := embed.Calculator{Resolver: MathResolverStub{}}
    in := strings.NewReader(`2 + 4 * 10
( 2 + 4 ) * 10
( 2 + 4 * 10`)

    data := []float64{42, 60, 0}
    expectedErr := errors.New("invalid expression: ( 2 + 4 * 10")
    for _, d := range data {
        result, err := c.Process(in)
        if err != nil {
            if err.Error() != expectedErr.Error() {
                t.Errorf("want (%v) got (%v)", expectedErr, err)
            }
        }
        if result != d {
            t.Errorf("Expected result %f, got %f", d, result)
        }
    }
}

MathResolverStub 를 가지는 Calculator 를 생성하여 Calculator 의 Process 메소드를 테스트할 수 있는 코드를 쉽게 작성할 수 있다.

테스트 코드의 전체 작성은 다음과 같다.

$ mkdir -p interface/embed
$ vi interface/embed/calculate.go

package embed

import (
    "errors"
    "io"
)

type Calculator struct {
    Resolver MathResolver
}

type MathResolver interface {
    Resolve(expression string) (float64, error)
}

func (c Calculator) Process(r io.Reader) (float64, error) {
    expression, err := readOneLine(r)
    if err != nil {
        return 0, err
    }
    if len(expression) == 0 {
        return 0, errors.New("no expression to read")
    }
    answer, err := c.Resolver.Resolve(expression)
    return answer, err
}

func readOneLine(r io.Reader) (string, error) {
    var out []byte
    b := make([]byte, 1)
    for {
        _, err := r.Read(b)
        if err != nil {
            if err == io.EOF {
                return string(out), nil
            }
        }
        if b[0] == '\n' {
            break
        }
        out = append(out, b[0])
    }
    return string(out), nil
}
$ vi interface/embed/calculate_test.go

package embed_test

import (
    "errors"
    "fmt"
    "strings"
    "testing"

    "github.com/seungkyua/go-test/interface/embed"
)

type MathResolverStub struct{}

func (mr MathResolverStub) Resolve(expr string) (float64, error) {
    switch expr {
    case "2 + 4 * 10":
        return 42, nil
    case "( 2 + 4 ) * 10":
        return 60, nil
    case "( 2 + 4 * 10":
        return 0, fmt.Errorf("invalid expression: %s", expr)
    }
    return 0, nil
}

func TestCalculatorProcess(t *testing.T) {
    c := embed.Calculator{Resolver: MathResolverStub{}}
    in := strings.NewReader(`2 + 4 * 10
( 2 + 4 ) * 10
( 2 + 4 * 10`)

    data := []float64{42, 60, 0}
    expectedErr := errors.New("invalid expression: ( 2 + 4 * 10")
    for _, d := range data {
        result, err := c.Process(in)
        if err != nil {
            if err.Error() != expectedErr.Error() {
                t.Errorf("want (%v) got (%v)", expectedErr, err)
            }
        }
        if result != d {
            t.Errorf("Expected result %f, got %f", d, result)
        }
    }
}

실행을 위한 세팅 명령어는 다음과 같다.

$ go mod init github.com/seungkyua/go-test
$ go mod tidy
$ go mod vendor

$ go work init
$ go work use .

go work 는 비지니스 모듈 패키지를 아직 github 에 커밋하지 않은 상태에서 로컬의 최신 패키지 참조를 위해서 필요하다.

전체 소스 트리는 다음과 같다.

$ tree .                   
.
├── README.md
├── go.mod
├── go.work
└── interface
    └── embed
        ├── calculate.go
        └── calculate_test.go

다음은 테스트 실행 결과이다.

$ go test interface/embed/calculate_test.go
ok      command-line-arguments  0.390s

소스는 아래의 링크에서 다운 받을 수 있다.

https://github.com/seungkyua/go-test.git

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

MSA 패턴(CQRS, SAGA 등)을 코드로 구현하기 위해서 SpringBoot 으로 REST API 어플리케이션을 만드는 것을 정리하고 있는데 자료 구조외에 자주쓰게 되는 코드들이 있어서 이를 정리해 봤다.

어플리케이션 개발에 Java 를 사용하는 이유는 아직 우리나라에서는 Java 가 많이 쓰이기 때문이다. SpringBoot 으로 어느 정도 정리되면 이후에는 Gin (Go 언어 기반 웹 프레임워크)으로 정리할 예정이다.

1. null check

Primitive Type 보다는 Object 를 많이 사용하기 때문에 null check 를 항상 해야 된다.

String nullString = null;
String emptyString = "";

if (Objects.isNull(nullString))
    System.out.println("nullString is null");

if (Objects.nonNull(emptyString))
    System.out.println("emptyString is not null");

--------- output -----------
nullString is null
emptyString is not null

Objects 클래스를 사용하여 가독성있게 null 체크를 할 수 있다.

2. null check & default value 세팅

null check 와 기본 값 세팅을 메소드 체이닝으로 바로 할 수 있다.

BigDecimal bd = null;

BigDecimal defaultValue = Optional.ofNullable(bd).orElse(BigDecimal.ZERO);
System.out.println(defaultValue);

--------- output -----------
0

Optional.ofNullable 은 static 메소드로 null 혹은 객체 를 담고있는 Optional 을 리턴한다.

Optional.orElse 는 메소드로 객체의 값이 null 일 경우 넘겨주는 아큐먼트로 그 값을 채운다.

여기서는 BigDecimal.ZERO 로 채워진 것을 알 수 있다.

3. null check & throw Exception

null check 와 null 일 때 예외를 던질 수 있다.

BigDecimal bd = null;

Optional.ofNullable(bd).orElseThrow(() -> new IllegalArgumentException("db is null"));
System.out.println(defaultValue);

--------- output -----------
Exception in thread "main" java.lang.IllegalArgumentException: db is null
    at com.ask.example.Main.lambda$setDefaultValue$0(Main.java:45)
    at java.base/java.util.Optional.orElseThrow(Optional.java:403)
    at com.ask.example.Main.setDefaultValue(Main.java:45)
    at com.ask.example.Main.main(Main.java:24)

orElseThrow 메소드는 function 을 파라미터로 받으며 여기서는 람다함수를 사용하여 간단히 IllegalArgumentException 을 생성하여 던진다.

4. Array 와 List 를 Stream 으로 만들기

Stream은 Java 8부터 추가된 기능으로, Collection(Array, List, Map 등)과 같은 데이터 요소들을 처리하는데 도움을 주는 기능이다. Stream을 이용하면 다양한 작업을 수행할 수 있다.

  1. 필터링(Filtering): filter() 메서드를 사용하여 특정 조건을 만족하는 요소들만 걸러낼 수 있다.
  2. 변환(Mapping): map() 메서드를 사용하여 요소들을 다른 형태로 변환할 수 있다.
  3. 소팅(Sorting): sorted() 메서드를 사용하여 요소들을 정렬할 수 있다.
  4. 그룹화(Grouping): collect() 메서드를 사용하여 요소들을 그룹화할 수 있다.
  5. 집계(Reduction): reduce() 메서드를 사용하여 요소들을 축소하여 하나의 값으로 만들 수 있다.
  6. 제한(Limiting): limit() 메서드를 사용하여 스트림의 크기를 제한할 수 있다.
  7. 건너뛰기(Skipping): skip() 메서드를 사용하여 스트림의 앞부분 요소들을 건너뛸 수 있다.
  8. 병렬 처리(Parallel Processing): parallelStream() 메서드를 사용하여 스트림 요소들을 병렬로 처리할 수 있다.
  9. 중복 제거(Distinct): distinct() 메서드를 사용하여 스트림의 중복 요소들을 제거할 수 있다.
  10. 조인(Joining): joining() 메서드를 사용하여 문자열 요소들을 합쳐서 하나의 문자열로 만들 수 있다.

먼저 Stream 을 만들어 보자.

배열을 Stream 으로 만들기

String[] fruits = { "apple", "banana", "orange", "grape", "banana" };
Stream stream = Arrays.stream(fruits);
System.out.println(stream);

--------- output -----------
java.util.stream.ReferencePipeline$Head@36baf30c

List 를 Stream 으로 만들기

List<String> fruits = new ArrayList<String>(
        Arrays.asList("apple", "banana", "orange", "grape", "banana"));
Stream stream = fruits.stream();
System.out.println(stream);

--------- output -----------
java.util.stream.ReferencePipeline$Head@36baf30c

5. Stream 을 다른 Collection 으로 바꾸기

Stream 은 Collectors 클래스를 사용하여 다른 Collection 으로 쉽게 변경이 가능하다.

Stream → Set 으로 변경 (중복 제거)

참고로 distinct 로도 중복 제거가 가능하다.

List<String> fruits = new ArrayList<String>(
        Arrays.asList("apple", "banana", "orange", "grape", "banana"));
Set<String> newSet = fruits.stream()
        .collect(Collectors.toSet());
newSet.forEach(item -> System.out.println(item));

--------- output -----------
banana
orange
apple
grape

중복된 banana 는 하나만 출력된다.

Stream → LinkedList 로 변경

List<String> fruits = new ArrayList<String>(
        Arrays.asList("apple", "banana", "orange", "grape", "banana"));
List<String> newList = fruits.stream()
        .collect(Collectors.toCollection(
                LinkedList::new
        ));
newList.forEach(item -> System.out.println(item));

--------- output -----------
apple
banana
orange
grape
banana

Stream → Map 으로 변경

List<String> fruits = new ArrayList<String>(
        Arrays.asList("apple", "banana", "orange", "grape", "banana"));
Map<String, Integer> newMap = fruits.stream()
        .collect(Collectors.toSet()).stream()
        .collect(Collectors.toMap(
                Function.identity(),
                String::length
        ));
newMap.forEach((key, value) -> System.out.println(key + " " + value));

--------- output -----------
banana 6
orange 6
apple 5
grape 5

Map 은 중복 키를 가질 수 없기 때문에 Set 으로 변환한 다음에 Map 으로 변환하였다.

여기서 Function.identity() 는 item 그 자체의 값으로 apple 과 같은 리스트의 요소를 가리키며, Map의 키로 사용되었다. String::Length 는 스트링 객체의 길이를 구하는 메소드를 호출한 리턴 값으로 Map 의 값으로 사용되었다.

6. Stream 과 데이터 변경 메소드들

Stream.map()

map() 은 Item 각각에 대해서 다른 형태로 변경할 수 있다.

List<String> fruits = new ArrayList<String>(
        Arrays.asList("apple", "banana", "orange", "grape", "banana"));
List<String> newList = fruits.stream()
        .map(item -> item.toUpperCase())
        .collect(Collectors.toList());
newList.forEach(item -> System.out.println(item));

--------- output -----------
APPLE
BANANA
ORANGE
GRAPE
BANANA

map() 은 아규먼트로 function 을 받으며 결과로 Stream 을 반환한다. 아규먼트로 들어가는 함수는 각 요소를 변경하여 반환하는 함수이어야 한다.

Stream.flatMap()

flatMap() 은 중첩된 Collection 을 한꺼풀 벗긴다고 생각하면 된다.

List<List<String>> listInList = Arrays.asList(
        Arrays.asList("apple", "banana"),
        Arrays.asList("orange", "grape", "banana"));

List<String> newList = listInList.stream()
        .flatMap(Collection::stream)
        .distinct()
        .collect(Collectors.toList());
newList.forEach(item -> System.out.println(item));

--------- output -----------
apple
banana
orange
grape

flatMap()function 를 아규먼트로 받고 결과를 Stream 으로 변환한다. 단 아규먼트로 들어가는 함수는 각 요소에 영향을 주면서 Stream 을 반환하는 함수이어야 한다.

6. enum 활용

enum 은 enum 타입이 반환된다.

enum Gender {
    MALE("male"),
    FEMALE("female");

    private final String param;

    Gender(String param) {
        this.param = param;
    }

    String getParam() {
        return this.param;
    }
}

System.out.println(Gender.MALE.getParam());

--------- output -----------
male

Gender.MALE 값은 enum Gender 타입이다.

하지만 아래와 같이 문자열을 enum 값으로 변환할 수 있다. 이 방법은 Json 을 marshal , unmarshal 할 때 많이 사용된다.

enum Gender {
    MALE("male"),
    FEMALE("female");

    private final String param;

    private static final Map<String, Gender> paramMap =
            Arrays.stream(Gender.values())
                    .collect(Collectors.toMap(
                            Gender::getParam,
                            Function.identity()
                    ));

    Gender(String param) {
        this.param = param;
    }

    static Gender fromParam(String param) {
        return Optional.ofNullable(param)
                .map(paramMap::get)
                .orElseThrow(() -> new IllegalArgumentException("param is not valid"));
    }

    String getParam() {
        return this.param;
    }
}

System.out.println(Gender.fromParam("male"));

--------- output -----------
MALE

static 으로 Map<String, Gender> 변수를 선언하여 거기에 값을 채워넣고, static fromParam 메소드를 사용하여 String 값으로 Gender 값을 가져온다.

7. Map 의 종류

Map 에는 다음과 같은 종류가 있다.

  1. HashMap : key - value 로 저장되며 key 의 중복을 허용하지 않는다
  2. TreeMap : HashMap 의 기능에 key 가 정렬되어 있다.
  3. LinkedHashMap: HashMap 의 기능에 key 입력 순서대로 출력된다.
  4. MultiValueMap: HashMap 가 다른 점이 key 의 중복을 허용한다.

다만, MultiValueMap 을 사용하기 위해서는 spring-core 패키지를 import 해야 한다. (apache commons 의 commons-collections4 에도 MultiValueMap 은 있지만 spring-core 가 더 사용하기 편하다)

pom.xml 에 아래와 같은 dependcy 를 추가한다.

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>6.0.9</version>
    </dependency>
</dependencies>
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("Accept-Encoding", "compress;q=0.5");
map.add("Accept-Encoding", "gzip;q=1.0");

map.forEach((key, value) -> System.out.println(key + " " + value));

--------- output -----------
Accept-Encoding [compress;q=0.5, gzip;q=1.0]

REST API 를 만들 때 Header 에 값을 넣기 위해서 위와 같이 MultiValueMap 을 사용한다.

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

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
,