반응형

인터페이스가 다른 인터페이서를 가지는 임베딩 방식을 사용하여 인터페이스를 선언할 수 있다. 예를 들어 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
,
반응형

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
,
반응형
I. require 와 include
    1. 다른 파일을 불러온다. HTML 태그, php 함수, php 클래스 등이다.
    2. require() 는 실패했을 경우 치명적인 오류를 내지만 include() 는 가벼운 경고만 낸다.
    3. require_once()와 include_once() 는 라이브러리 파일을 두 번 이상 포함시키는 일을 막아준다.
        그러나 require()나 include() 보다 속도가 느리다. 
    4. php 문은 파일 확장자가 .html 이거나 .php 등일 때에만 스크립트 언어로 처리되어 실행.
    5. require로 불러들일 파일의 확장자 이름에 .inc와 같은 특별한 규칙을 두어 작성하는 것도 좋은 방법
        단, 이런 문서를 웹 문서 트리 안에 저장할 때에는 사용자들이 직접 접근 하지 못하도록 주위 (패스워드 등) 
        php 환경설정에서 아래와 같이 디렉토리를 웹 문서 디렉토리로 설정하고 추가로 config 설정파일은
        php 웹 문서가 아닌 디렉토리로 지정할 수 있다.(/Library/WebServer/php/includes)
        예) include_path = ".:/Library/WebServer/php/includes:/Library/WebServer/Documents"

II. 참조 전달 방법
    1. & 를 이용하면 참조로 전달 할 수 있다.
function increment(&$value, $amount = 1) {
    $value = $value + $amount;

III. 클래스 만들기
    1. 생성자와 소멸자
        - 생성자는 __construct() 라는 특별한 이름으로 파라미터를 가지며 생성할 수 있다.
        - 소멸자는 __destruct() 라는 이름으로 만들며 파라미터를 가지지 못한다.
 
    2. 클래스 속성
        - 접근자가 생략되면 모두 public 이다.
        - 클래스 속성의 접근자는 private 으로 하고 __set(), __get() 으로 속성에 접근한다.
        - 자기 자신의 인스턴스는 $this 로 접근한다.

class Point {

    private $x;

    private $y;

    public function __construct($x, $y) {

        $this->x = $x;

        $this->y = $y;

    }

    public function __get($name) {

        return $this->$name;

    }

    public function __set($name, $value) {

        $this->$name = $value;

    }

    public function display() {

        print("\$this->x = " . $this->x . "<br/>");

        print("\$this->y = " . $this->y . "<br/>");

    } 

}

$point = new Point(3, 5);

print($point->x);         // 3

print($point->y);         // 5

$point->x = 7;

print($point->x);         // 7

$point->display(); 


    3. 클래스 상속
        - 부모의 클래스는  parent 키워드를 이용한다.

class Rectangle extends Point {

    private $width;

    private $height;


    public function __construct($x, $y, $width, $height) {

        parent::__construct($x, $y);

        $this->x = $x;

        $this->y = $y;

    }

}

$rectangle = new Rectangle(10, 20, 100, 200);

print($rectangle->x);   // 10

 
    4. 인터페이스
        - 인터페이스는 interface 키워드를 사용한다.
        - 구현은 implements 로 한다.

interface Displayable {

    public function display();

}

class WebPage implements Displayable {

    public function display() {

        print("Hello World!! <br/>");

    }

}

$page = new WebPage();

$page->display();


    5. 클래스의 상수 및 정적 메소드 
        - 상수는 const 키워드를 사용하고 :: 연산자로 접근한다.
        - 정적 메소드는 static 키워드를 사용하고 :: 연산자로 접근한다.

class Math {

    const pi = 3.14159;

    public static function squared($input) {

        return $input * $input;

    }

}

print("Math::pi = " . Math::pi . "<br/>");

print("Math::squared(8) = " . Math::squared(8) . "<br/>");


    6. 클래스 타입 hinting
        - 클래스의 특정 타입을 파라미터 전달 시에 전달할 수 있다.
function check_hint(Point $object) {
    // ...

    7.  추상 클래스
        - abstract 키워드를 사용한다.

abstract class A {

    public abstract function operationX($param1, $param2);

}

class ConcreteA extends A {

    public function operationX($param1, $param2) {

        return $param1 + $param2;

    }

}

$a = new ConcreteA();

print($a->operationX(3, 4) . "<br/>");


    8. 메소드 오버로딩
        - __call() 메소드를 이용하여 오버로딩을 구현한다.
        - 첫번째 전달인자는 호출되는 메소드이고, 두번째 전달인자는 파라미터로 넘길 파라미터 배열이다.
public function __call($method, $param) {
    if ($method == "display") {
        if (is_object($p[0])) {
            $this->displayObject($p[0]);
        } else if (is_array($p[0])) {
            $this->displayArray($p[0]);
        } else {
            $this->displayScalar($p[0]);
        }
    }
}

$object = new Overload();
$object->display(array(1, 2, 3));
$object->display('cat'); 

    9. 클래스 자동 import
        - __autoload() 함수를 이용하여 자동으로 include 할 수 있다.
        - 이 함수는 클래스 메소드가 아니라 독립된 함수이다.
        - 아래는 클래스 파일과 같은 파일명을 읽어 들이는 예제이다.
function __autoload($name) {
    include_once $name . ".php";

    10. Reflection API
require_once("config.inc");

$class = new ReflectionClass("Point");
print("<pre>" . $class . "</pre>"); 


반응형
Posted by seungkyua@gmail.com
,