If

다른 언어와 다르게 Go에서는 if 문 안에 variable을 만들 수 있다.

 

테스트

func main() {
	fmt.Print(canIDrink(16))
}

func canIDrink(age int) bool { 
	if koreanAge := age+2; koreanAge < 18 {
		return false
	}

	return true
	
}

예를들어, 위와같이 koreanAge라는 variable을 만들어서 조건문에 적용할 수 있다. 그때, variable을 만든 후 ;(세미콜론) 이후 사용하면 된다. 

	koreanAge := age+2			//...1
	if koreanAge < 18 {
		return false
	}
    
    	if koreanAge := age+2; koreanAge < 18 {			//...2
		return false
	}

 

위 두개의 코드는 같은 기능을 수행하는 코드이다. 하지만, 다른 사람이 보기에 1번의 코드에서 koreanAge는 if문 외에도 아래에서 더 사용하기때문에 따로 선언을 한 것처럼 보인다. 이를 방지하고자 2번과 같은 기능을 만든 것 같다.

 

switch

switch도 if문과 같이 variable을 생성해줄 수 있다.

테스트

func canIDrink(age int) bool {
	switch koreanAge := age+2; koreanAge {
	case 10:
		return false
	case 18:
		return true
	}

	return true
	
}

'Go' 카테고리의 다른 글

Arrays/Slices  (0) 2023.12.29
Pointer  (0) 2023.12.28
for, range, args  (0) 2023.12.28
Functions  (0) 2023.12.28
Variables/Constants  (0) 2023.12.27

반복문

Go에서 반복문은 오로지 for만 사용할 수 있다. Java는 for, while, map 등 반복문에 여러 기능이 가능했지만 Go에서는 for로만 사용 가능하다. 오히려 더 간편한 느낌이다.

 

테스트

func main() {
	fmt.Print(superAdd(1,2,3,4,5,6))
}

func superAdd(numbers ...int) int {
	sum := 0
	for index, number := range numbers {	// index(0,1...,n-1)와 number라는 index에 해당하는 값
		fmt.Println(index, number)
	}
	return sum
}

출력

물론, index가 필요하지 않은 경우 이전에 배웠던 _를 통하여 사용하지 않음을 명시해줄 수 있다. 

	for i:= 0; i< len(numbers); i++ {
		fmt.Println(i, numbers[i])
	}

java에서와 비슷한 방식으로 위와 같이도 사용이 가능하다.

 

'Go' 카테고리의 다른 글

Pointer  (0) 2023.12.28
If/switch  (0) 2023.12.28
Functions  (0) 2023.12.28
Variables/Constants  (0) 2023.12.27
Import/Export  (0) 2023.12.24

Concept

Basic Types

다른언어와 비슷하다.

  • string type
    • string
  • boolean type
    • bool
  • numeric types
    • int8, uint8, int16 .... int64, uint64, int, uint, uintptr
    • float32, float64
    • complext64, complex128

 

 

functions

파라미터로 int형 숫자 2개를 보내고 곱한 값을 Return하는 함수를 만들어보자.

func multiply(a int, b int) int {
	return a * b
}

 

Go에서의 함수는 왼쪽이 파라미터 명, 오른쪽이 타입이다. 또한, return이 있다면 () 오른쪽에 return type을 지정해주어야 한다. 아래처럼 파라미터의 타입이 모두 동일한 경우 맨 오른쪽에만 타입을 지정해줘도 된다. 

func multiply(a, b int) int {
	return a * b
}

 

또한, 여러 타입들을 한 번에 return 해올 수 있다. (Java에는 없었던 기능)

func getNameAndPhoneNumber() (string, int) {
	return "qazyj", 123456789
}

위와 같이 타입이 다른 변수 두개를 return할 수 있다.

func main() {
	name, number := getNameAndPhoneNumber()
	fmt.Print(name, number)
}

받는 방법은 위와같은 방법으로 받을 수 있다. 또한, return한 값을 사용하고 싶지 않을 때는 _를 사용하면 된다. (에러를 Return하는 경우  사용되지 않는 경우가 있기도 함.)

func main() {
	name, _ := getNameAndPhoneNumber()
	fmt.Print(name)
}

_를 하는 이유는 _를 하지않고 name만 return 받으려고 위와 같이 작성하면 에러가 발생한다. _는 무시되는 value로 컴파일 시에 무시된다.

 

functions에서 argument를 무제한으로 받을 수 있는 방법도 있다. type앞에 ...을 붙이면 된다.

func main() {
	repeatMe("q", "a", "z", "y", "j")
}

func repeatMe(words ...string) {
	fmt.Println(words)
}

출력

 

 

Naked Return

return 오른쪽에 return되는 값을 넣어주는 것이 아닌 Return Type을 적어주는 곳에 return 값을 넣음. 

 

사용 방법

func main() {
	fmt.Print(lenAndUpper("qazyj"))
}

func lenAndUpper(name string) (length int, uppercase string) {
	length = len(name)		// return 값에 선언해준 변수명이랑 같아야함
	uppercase = strings.ToUpper(name)     // return 값에 선언해준 변수명이랑 같아야함
	return // 필수
}

 

 

defer

function이 끝날 때, 추가적으로 무엇인가 동작할 수 있도록 하는 기능
Java에서 못봤던 기능인 것 같은데, 유용하게 쓰일 수 있는 기능 같다.
ex) 이미지를 열거나 파일을 생성하고나서 defer로 이미지, 파일을 닫던가 삭제하는, API로 요청을 보낸다던가 등

 

테스트

func lenAndUpper(name string) (length int, uppercase string) {
	defer fmt.Println("I am done.")			// ..1 
	fmt.Println("lengAndUpper Start")			// ..2
	length = len(name)
	uppercase = strings.ToUpper(name)
	fmt.Println("lengAndUpper Finish")			// ..3
	return
}

출력

defer라는 기능을 통해 1 -> 2 -> 3 번 순서로 출력이 되어야 할 출력물이, 2 -> 3 -> 1 번 순서로 출력되는 것을 볼 수 있다.

 

 

 

 

 

 

 

 

'Go' 카테고리의 다른 글

If/switch  (0) 2023.12.28
for, range, args  (0) 2023.12.28
Variables/Constants  (0) 2023.12.27
Import/Export  (0) 2023.12.24
package와 main  (0) 2023.12.24

Constants

상수를 만드는 방법은 간단하다.

const name = "qazyj"

위처럼 작성만 하면된다. 하지만, 아래처럼 아직 타입이 정해지지 않은 상수이다. 

Go는 type 언어이기 때문에 Java나 C처럼 type이 무엇인지 알려주어야 한다. 

const name string = "qazyj"

타입은 위와같이 작성해주면 된다. 당연하게도 string 대신 bool, int와 같은 타입은 작성해준다면 아래와 같은 에러를 발생해주며 코드 작성을 도와준다.

 

또한, 당연하게도 상수는 재할당을 할 수 없기때문에 할당을 하려는 코드를 작성하면 아래와 같은 에러 문구로 코드 작성을 도와준다.

 

 

Variables

contstants와 비슷하다. const를 var로만 바꾸면 된다. constants 때 사용한 코드에서 const -> var로만 변경해주었다.

constants와 다른 점은 재할당이 가능하다는 점에서 다르다.

 

하지만, 변수를 선언해줄 때의 코드가 너무 길다는 단점이 존재한다. 이러한 불편한 점을 Go 개발자들도 인지하였는지 아래와 같은 코드로도 작성이 가능하다.

name := "qazyj"

 

이게 뭐지 싶겠지만 name에 마우스 커서를 위치시켜 보면 아래와 같은 문구를 볼 수 있다. var와 type을 생략할 수 있도록 도와준다. :=를 사용하면 Go에서 자동으로 적절한 type을 설정해준다. 이를 부르는 용어는 Go의 짧은 변수 선언이라고 한다. 

그렇다면, 의문점이 생긴다. Go에서 선언된 값을 통하여 변수를 설정해주기 때문에 시간의 차이가 있지 않을까? 라는 생각이 들지만 알아본 바에 의하면 성능 차이는 거의 존재하지 않는다고 한다. var name string = "qazyj"는 지양하고, name := "qazyj"를 지향하면 보다 코드를 작성하는 시간을 단축시킬 수 있겠다.

 

 

 

 

'Go' 카테고리의 다른 글

for, range, args  (0) 2023.12.28
Functions  (0) 2023.12.28
Import/Export  (0) 2023.12.24
package와 main  (0) 2023.12.24
Go에서 의존성 주입 (wire, fx 사용방법)  (0) 2023.05.14

fmt와 같이 import해서 사용할 수 있는 func은 모두 대문자로 시작한다. 그냥 지나칠 수 있지만, 대문자로 시작하는 이유가 있다.

 

Java, python, node.js, JavaScript에서는 import 할 때 import XXX from XXX과 같은 방식으로 import 한다. Go는 function을 export하고 싶다면 function을 대문자로 시작해주면 된다.

 

Test
something이라는 폴더를 만들고 폴더안에 something.go 라는 go 파일을 하나 만들어 준다.

 

something.go 코드로는 아래와 같이 sayBye는 소문자로, SayHello는 대문자로 시작해주도록 했다.

package something

import "fmt"

func sayBye() {
	fmt.Println("Bye")
}

func SayHello() {
	fmt.Println("Hello")
}

 

그 후 

main에 위와 같이 import하여 func을 사용하면 대문자로 시작하는 func만 사용가능한 것을 볼 수 있다.

소문자로 시작하는 func의 경우 정의되어 있지않다는 문구를 볼 수 있다.

 

 

 

'Go' 카테고리의 다른 글

Functions  (0) 2023.12.28
Variables/Constants  (0) 2023.12.27
package와 main  (0) 2023.12.24
Go에서 의존성 주입 (wire, fx 사용방법)  (0) 2023.05.14
go로 gRPC Unary 찍먹  (0) 2023.05.01

Package

Go의 코드를 묶는 단위로 모든 .go 파일은 package를 설정해야 합니다.

가장 기본적으로 Go 파일을 컴파일하기 위해서는 프로그램의 시작점 Package를 main으로 설정해야 합니다.

 

 

main

  • packge main은 프로그램의 시작점입니다.
  • 그렇기때문에, package main으로 작성된 .go 파일은 해당 파일을 컴파일 할 것이고 시작 패키지는 packge main입니다.
  • package 명이 main이 아닌 패키지는 기능을 패키징화 한다는 등의 이유가 될 수 있습니다.

그렇다면, 위와같이 package만 작성하고 컴파일을 하면 컴파일러가 정상적으로 컴파일 할 수 있을까요?

그렇지 않습니다. 에러를 읽어보면 아래와 같은 runtime 에러 로그를 보여줍니다.

function main is undeclared in the main packge

 

main package 안에 function main이 선언되어있지 않다는 친절하고 명확한 에러로 개발을 편리하게 도와줍니다.

package main

func main() {
}

에러의 안내대로 위와 같이 func main() {}을 생성해주고 돌려보면 정상적으로 돌아갑니다. 이러한 경험대로 Go라는 언어는 컴파일 하기 위해서는 main package가 패키지의 시작점이고, main package안에서 func main() {}이 프로그램을 읽는 출발점이라고 볼 수 있습니다.

 

 

Go에서 제공하는 여러 package들은 아래에서 확인할 수 있다.

 

Standard library - Go Packages

Discover Packages Standard library Version: go1.21.5 Opens a new window with list of versions in this module. Published: Dec 5, 2023 License: BSD-3-Clause Opens a new window with license information. Jump to ... Directories Directories ¶ Show internal Exp

pkg.go.dev

 

 

 

 

 

 

'Go' 카테고리의 다른 글

Variables/Constants  (0) 2023.12.27
Import/Export  (0) 2023.12.24
Go에서 의존성 주입 (wire, fx 사용방법)  (0) 2023.05.14
go로 gRPC Unary 찍먹  (0) 2023.05.01
gRPC란?  (0) 2023.05.01

서론

Spring Framework에서 Spring Bean을 사용하여 객체지향 설계원칙을 지키는 방법에 대해서 공부해본 경험이 있었기때문에 궁금증이 생겼습니다. go에서는 Spring Bean과 같은 IoC(제어의 역전)을 지원해주는 도구는 없는가에 대한 궁금증이.


본론

go에서의 의존성 도구는 아래와 같은 도구들이 있습니다. 

  1. wire
  2. fx(built on top of dig)

wire는 google에서 만들었으며 code generator 방식으로 build-time에 코드를 만들어내는 도구입니다. fx는 uber에서 만들었으며 reflect를 이용해서 runtime에 의존성을 주입하는 도구입니다. 각각의 도구들을 사용해봅시다.


wire

wire는 providers와 injectors라는 핵심 개념이 있다.

아래의 명령어를 실행합니다.

go get github.com/google/wire/cmd/wire

 

Defining Providers

Wire의 기본 메커니즘은 값을 생성할 수 있는 함수인 provide입니다. 
package foobarbaz

type Foo struct {
    X int
}

// ProvideFoo returns a Foo.
func ProvideFoo() Foo {
    return Foo{X: 42}
}

provider 함수는 일반 함수와 마찬가지로 다른 패키지에서 사용하기 위해 내보내야 합니다. provider는 매개변수로 종속성을 지정할 수 있습니다.

package foobarbaz

// ...

type Bar struct {
    X int
}

// ProvideBar returns a Bar: a negative Foo.
func ProvideBar(foo Foo) Bar {
    return Bar{X: -foo.X}
}

providers는 error를 return 할 수도 있습니다.

package foobarbaz

import (
    "context"
    "errors"
)

// ...

type Baz struct {
    X int
}

// ProvideBaz returns a value if Bar is not zero.
func ProvideBaz(ctx context.Context, bar Bar) (Baz, error) {
    if bar.X == 0 {
        return Baz{}, errors.New("cannot provide baz when bar is zero")
    }
    return Baz{X: bar.X}, nil
}

공급자는 공급자 집합으로 그룹화할 수 있습니다. 이는 여러 공급자가 자주 함께 사용되는 경우에 유용합니다. 이러한 공급자들을 SuperSet이라는 새 세트에 추가하려면 wire.NewSet function을 사용하면됩니다.

package foobarbaz

import (
    // ...
    "github.com/google/wire"
)

// ...

var SuperSet = wire.NewSet(ProvideFoo, ProvideBar, ProvideBaz)

다른 공급자 집합을 공급자 집합에 추가할 수도 있습니다.

package foobarbaz

import (
    // ...
    "example.com/some/other/pkg"
)

// ...

var MegaSet = wire.NewSet(SuperSet, pkg.OtherSet)

 

Injectors

애플리케이션은 종속성 순서대로 provider를 호출하는 함수인 injector를 사용하여 wire로 연결합니다. Wire를 사용하면 injector의 서명을 작성하면 Wire가 함수의 본문을 생성합니다.

인젝터는 wire.Build에 대한 호출을 본문으로 하는 함수 선언을 작성하여 선언됩니다. 반환 값은 올바른 유형인 한 중요하지 않습니다. 생성된 코드에서 값 자체는 무시됩니다. 위의 provider가 go/providers/foobarbaz라는 패키지에 정의되어 있다고 가정해 보겠습니다. 다음은 Baz를 얻기 위한 injector를 선언합니다:

// +build wireinject
// The build tag makes sure the stub is not built in the final build.

package main

import (
    "context"

    "github.com/google/wire"
    "go/providers/foobarbaz"
)

func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
    wire.Build(foobarbaz.SuperSet)
    return foobarbaz.Baz{}, nil
}

이후 위 코드가 담긴 디렉토리 안에서 아래의 명령어를 실행합니다.

wire

저는 해당 과정에서 zsh: command not found: wire 에러가 발생했고, export PATH=$PATH:$GOPATH/bin 명령어로 path를 수정해주니 정상적으로 돌아갔습니다. 해결과정에서 참고한 링크입니다. 

 

wire 명령어를 치는순간 아래와 같은 코드로 wire_gen.go라는 파일이 자동으로 생성됩니다.

// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package main

import (
	"context"
	"go/providers/foobarbaz"
)

// Injectors from wire.go:

func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
	foo := foobarbaz.ProvideFoo()
	bar := foobarbaz.ProvideBar(foo)
	baz, err := foobarbaz.ProvideBaz(ctx, bar)
	if err != nil {
		return foobarbaz.Baz{}, err
	}
	return baz, nil
}

순서대로 잘 생성된 모습을 볼 수 있고 wire를 통해 의존성을 주입하고 최종적으로 의존성을 주입받은 객체를 반환받습니다. 더 자세한 기능들은 링크에서 확인할 수 있습니다.

 

총 코드

foo, bar, baz 코드 

package foobarbaz

import (
    "context"
    "errors"
)

type Foo struct {
    X int
}

type Bar struct {
    X int
}

type Baz struct {
    X int
}

// ProvideFoo returns a Foo.
func ProvideFoo() Foo {
    return Foo{X: 42}
}

// ProvideBar returns a Bar: a negative Foo.
func ProvideBar(foo Foo) Bar {
    return Bar{X: -foo.X}
}

// ProvideBaz returns a value if Bar is not zero.
func ProvideBaz(ctx context.Context, bar Bar) (Baz, error) {
    if bar.X == 0 {
        return Baz{}, errors.New("cannot provide baz when bar is zero")
    }
    return Baz{X: bar.X}, nil
}

wire build 코드

// +build wireinject
// The build tag makes sure the stub is not built in the final build.

package wire

import (
    "context"

    "github.com/google/wire"
    "go/providers/foobarbaz"
)

func InitializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
    wire.Build(foobarbaz.ProvideFoo, foobarbaz.ProvideBar, foobarbaz.ProvideBaz)
    return foobarbaz.Baz{}, nil
}

생성된 wire_gen.go 코드

// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package wire

import (
	"context"
	"go/providers/foobarbaz"
)

// Injectors from wire.go:

func InitializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
	foo := foobarbaz.ProvideFoo()
	bar := foobarbaz.ProvideBar(foo)
	baz, err := foobarbaz.ProvideBaz(ctx, bar)
	if err != nil {
		return foobarbaz.Baz{}, err
	}
	return baz, nil
}

테스트

package main

import (
    "fmt"
    "context"
    
	"go/providers/foobarbaz"
	"go/providers/foobarbaz/wire"
)

func main() {
	foo := foobarbaz.ProvideFoo()
	bar := foobarbaz.ProvideBar(foo)
	baz1, _ := foobarbaz.ProvideBaz(context.Background(), bar)
    fmt.Println(baz1.X)

	baz2, _ := wire.InitializeBaz(context.Background())
	fmt.Println(baz2.X)
}

출력

기존에 생성코드로 만든 baz와 wire로 생성된 baz 모두 같은 값을 출력하는 것을 확인할 수 있다. Foo의 X값을 변경해도 변경된 값으로 출력된다. 의존성과 파라미터, return 형태에 변경만 없다면 내부 로직을 바꾸어도 정상적으로 돌아가는 것을 확인할 수 있다.

하지만 의존성/파라미터/return 형태에 변경이 있다면 코드를 변경해준 뒤 wire 명령어를 다시 입력해야 한다.

 


fx

fx는 서버 애플리케이션의 구성 요소를 생성하고 연결하는 데 도움을 주면서 DI를 지원하는 프레임워크입니다.

사용하면서 느낀건데 go에서 사용되는 다른 web framework들도 DI를 제공합니다. (예를들어, Fiber / Gin / Echo 등이 있다.)

 

사용방법

아래의 명령어를 실행합니다.

go get go.uber.org/fx@v1

main.go

package main

import "go.uber.org/fx"

func main() {
  fx.New().Run()
}

run

go run .

아래와 같은 출력을 확인할 수 있습니다.

[Fx] PROVIDE	fx.Lifecycle <= go.uber.org/fx.New.func1()
[Fx] PROVIDE	fx.Shutdowner <= go.uber.org/fx.(*App).shutdowner-fm()
[Fx] PROVIDE	fx.DotGraph <= go.uber.org/fx.(*App).dotGraph-fm()
[Fx] RUNNING

인자 없이 fx.New를 호출하여 빈 Fx 애플리케이션을 빌드했습니다. 애플리케이션은 일반적으로 fx.New에 인수를 전달해 컴포넌트를 설정하고 App.Run 메서드로 이 애플리케이션을 실행합니다. ctrl+c를 누르면 종료가 됩니다.

Fx는 주로 장기 실행 서버 애플리케이션을 위한 것으로 종료할 때가 되면 배포 시스템으로부터 신호를 받습니다.

 

Add an HTTP server

HTTP 서버를 구축하는 함수를 작성

// NewHTTPServer builds an HTTP server that will begin serving requests
// when the Fx application starts.
func NewHTTPServer(lc fx.Lifecycle) *http.Server {
  srv := &http.Server{Addr: ":8080"}
  return srv
}

하지만 이것만으로는 충분하지 않습니다. Fx에게 HTTP 서버를 시작하는 방법을 알려줘야 합니다. 이것이 바로 추가 fx.Lifecycle 인수의 목적입니다. fx.Lifecycle 객체를 사용하여 애플리케이션에 수명 주기 후크를 추가합니다. 이렇게 하면 Fx가 HTTP 서버를 시작하고 중지하는 방법을 알려줍니다.

func NewHTTPServer(lc fx.Lifecycle) *http.Server {
  srv := &http.Server{Addr: ":8080"}
  lc.Append(fx.Hook{
    OnStart: func(ctx context.Context) error {
      ln, err := net.Listen("tcp", srv.Addr)
      if err != nil {
        return err
      }
      fmt.Println("Starting HTTP server at", srv.Addr)
      go srv.Serve(ln)
      return nil
    },
    OnStop: func(ctx context.Context) error {
      return srv.Shutdown(ctx)
    },
  })
  return srv
}

fx.Provide 추가

func main() {
  fx.New(
    fx.Provide(NewHTTPServer),
  ).Run()
}

돌려보면 정상적으로 돌아가는 것 같지만 위에 적어놓은 fmt.Println("Starting HTTP server at", srv.Addr)부분이 출력되지 않는 것을 확인할 수 있습니다. 서버가 시작되지 않았다는 것인데요. 실제로 curl로 요청을 보내보면 아래와 같은 에러로그가 출력됩니다.

curl: (7) Failed to connect to localhost port 8080 after 14 ms: Couldn't connect to server

이 문제를 해결하려면 생성된 서버를 요청하는 fx.Invoke를 추가해야합니다.

 fx.New(
    fx.Provide(NewHTTPServer),
    fx.Invoke(func(*http.Server) {}),
  ).Run()

이제 돌려보면 정상적으로 돌아갑니다. curl로 요청을 보내면 404가 뜨지만, 적절한 응답이 없어서 뜨는 에러입니다.

404 page not found

그렇다면 적절한 응답을 보내기 위해 Handler를 추가해봅시다.

// EchoHandler is an http.Handler that copies its request body
// back to the response.
type EchoHandler struct{}

// NewEchoHandler builds a new EchoHandler.
func NewEchoHandler() *EchoHandler {
  return &EchoHandler{}
}

// ServeHTTP handles an HTTP request to the /echo endpoint.
func (*EchoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  if _, err := io.Copy(w, r.Body); err != nil {
    fmt.Fprintln(os.Stderr, "Failed to handle request:", err)
  }
}

main 메소드 코드도 변경해야합니다.

    fx.Provide(
      NewHTTPServer,
      NewEchoHandler,
    ),
    fx.Invoke(func(*http.Server) {}),

다음으로 *http.ServeMux를 빌드하는 함수를 작성합니다. http.ServeMux는 서버가 수신한 요청을 다른 핸들러로 라우팅합니다. 우선, 이 함수는 /echo로 전송된 요청을 *EchoHandler로 라우팅하므로 생성자는 *EchoHandler를 인수로 받아들여야 합니다.

// NewServeMux builds a ServeMux that will route requests
// to the given EchoHandler.
func NewServeMux(echo *EchoHandler) *http.ServeMux {
  mux := http.NewServeMux()
  mux.Handle("/echo", echo)
  return mux
}

Provide에도 추가해줘야합니다.

    fx.Provide(
      NewHTTPServer,
      NewServeMux,
      NewEchoHandler,
    ),

NewHTTPServer 메소드에도 ServeMux를 추가합니다.

func NewHTTPServer(lc fx.Lifecycle, mux *http.ServeMux) *http.Server {
  srv := &http.Server{Addr: ":8080", Handler: mux}
  lc.Append(fx.Hook{

그 후 돌려보면 요청에 따라 다른 응답을 보냅니다.

$ curl -X POST -d 'hello kyj' http://localhost:8080/echo
hello kyj

현재는 프로그램을 돌리면 아래와 같은 출력이 발생합니다. 이를 로거로 바꿔주겠습니다. 

[Fx] PROVIDE	*http.Server <= main.NewHTTPServer()
[Fx] PROVIDE	*http.ServeMux <= main.NewServeMux()
[Fx] PROVIDE	*main.EchoHandler <= main.NewEchoHandler()
[Fx] PROVIDE	fx.Lifecycle <= go.uber.org/fx.New.func1()
[Fx] PROVIDE	fx.Shutdowner <= go.uber.org/fx.(*App).shutdowner-fm()
[Fx] PROVIDE	fx.DotGraph <= go.uber.org/fx.(*App).dotGraph-fm()
[Fx] INVOKE		main.main.func1()
[Fx] HOOK OnStart		main.NewHTTPServer.func1() executing (caller: main.NewHTTPServer)
Starting HTTP server at :8080
[Fx] HOOK OnStart		main.NewHTTPServer.func1() called by main.NewHTTPServer ran successfully in 320.824µs
[Fx] RUNNING
^C[Fx] INTERRUPT
[Fx] HOOK OnStop		main.NewHTTPServer.func2() executing (caller: main.NewHTTPServer)
[Fx] HOOK OnStop		main.NewHTTPServer.func2() called by main.NewHTTPServer ran successfully in 241.698µs

여기는 Zap을 사용했습니다. 사용하기 위해서는 아래 명령어를 실행해주고 "go.uber.org/zap"을 import 해야합니다.

go get -u go.uber.org/zap

애플리케이션에 Zap 로거를 제공합니다.

    fx.Provide(
      NewHTTPServer,
      NewServeMux,
      NewEchoHandler,
      zap.NewExample,
    ),

에코핸들러에 로거를 보관할 필드를 추가하고, 새 에코핸들러에서 이 필드를 설정하는 새 로거 인수를 추가합니다.

type EchoHandler struct {
  log *zap.Logger
}

func NewEchoHandler(log *zap.Logger) *EchoHandler {
  return &EchoHandler{log: log}
}

출력대신 로그로 바꿉니다. 

func (h *EchoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  if _, err := io.Copy(w, r.Body); err != nil {
    h.log.Warn("Failed to handle request", zap.Error(err))
  }
}
func NewHTTPServer(lc fx.Lifecycle, mux *http.ServeMux, log *zap.Logger) *http.Server {
  srv := &http.Server{Addr: ":8080", Handler: mux}
  lc.Append(fx.Hook{
    OnStart: func(ctx context.Context) error {
      ln, err := net.Listen("tcp", srv.Addr)
      if err != nil {
        return err
      }
      log.Info("Starting HTTP server", zap.String("addr", srv.Addr))
      go srv.Serve(ln)

Fx의 자체 로그에도 동일한 Zap 로거를 사용할 수 있습니다.

func main() {
  fx.New(
    fx.WithLogger(func(log *zap.Logger) fxevent.Logger {
      return &fxevent.ZapLogger{Logger: log}
    }),

로그로 바뀐 모습

{"level":"info","msg":"provided","constructor":"main.NewHTTPServer()","type":"*http.Server"}
{"level":"info","msg":"provided","constructor":"main.NewServeMux()","type":"*http.ServeMux"}
{"level":"info","msg":"provided","constructor":"main.NewEchoHandler()","type":"*main.EchoHandler"}
{"level":"info","msg":"provided","constructor":"go.uber.org/zap.NewExample()","type":"*zap.Logger"}
{"level":"info","msg":"provided","constructor":"go.uber.org/fx.New.func1()","type":"fx.Lifecycle"}
{"level":"info","msg":"provided","constructor":"go.uber.org/fx.(*App).shutdowner-fm()","type":"fx.Shutdowner"}
{"level":"info","msg":"provided","constructor":"go.uber.org/fx.(*App).dotGraph-fm()","type":"fx.DotGraph"}
{"level":"info","msg":"initialized custom fxevent.Logger","function":"main.main.func1()"}
{"level":"info","msg":"invoking","function":"main.main.func2()"}
{"level":"info","msg":"OnStart hook executing","callee":"main.NewHTTPServer.func1()","caller":"main.NewHTTPServer"}
{"level":"info","msg":"Starting HTTP server","addr":":8080"}
{"level":"info","msg":"OnStart hook executed","callee":"main.NewHTTPServer.func1()","caller":"main.NewHTTPServer","runtime":"378.452µs"}
{"level":"info","msg":"started"}

위의 방식대로 fx를 사용하면 됩니다. 

 

 

 

 

 

Reference

'Go' 카테고리의 다른 글

Import/Export  (0) 2023.12.24
package와 main  (0) 2023.12.24
go로 gRPC Unary 찍먹  (0) 2023.05.01
gRPC란?  (0) 2023.05.01
Golang 설정 및 실행  (0) 2023.04.30

protobuf install

brew install protobuf

 

mod init

go mod init go

 

Install protocol compiler

go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2

 

update PATH

export PATH="$PATH:$(go env GOPATH)/bin"

 

protocol buffer

syntax = "proto3";

package grpc;
option go_package = "./grpc/hello";

// 테스트
service Test {
    rpc Show(TestRequest) returns (TestResponse);
}

// 요청
message TestRequest {
    string requestString = 1;
}

// 응답
message TestResponse {
    string ResponseString = 1;
}

 

grpc 의존성 추가

go get -u google.golang.org/grpc
go get -u github.com/golang/protobuf/protoc-gen-go
@저는 GOPATH를 바꿨다가 go get 명령어가 permission 문제로 돌아가지 않았는데 아래 명령어로 GOPATH를 정의해주니 잘 돌아갑니다.
export GOPATH="$HOME/go"

이유는 GOPATH는 GO의 workspace 위치를 정의하는 환경변수인데, Unix 시스템은 workspace 기본디렉터로 $HOME/go를 제공한다는 글을 읽고 위와 같이 바꾸어주니까 잘 작동하네요.

// 아마도 맨 처음 "brew install go"를 하면  GOPATH가 $HOME/go로 되어있을 것이라 추정

 

 

protoc로 컴파일

protoc --go_out=. --go_opt=paths=source_relative \
    --go-grpc_out=. --go-grpc_opt=paths=source_relative \
    grpc/grpc.proto

위 명령어로 컴파일을 하면 grpc.pb.go 파일이 생성된다. 후에 이 파일의 인터페이스로 코드를 작성하면 된다.

생성된 파일

생성된 코드를 확인해보면 다음과 같다.

// 테스트
service Test {
    rpc Show(TestRequest) returns (TestResponse);
}

 .proto 파일에서 작성한 service가 client와 server 각각 interface로 생성되어있다.

// TestClient is the client API for Test service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type TestClient interface {
	Show(ctx context.Context, in *TestRequest, opts ...grpc.CallOption) (*TestResponse, error)
}

// TestServer is the server API for Test service.
// All implementations must embed UnimplementedTestServer
// for forward compatibility
type TestServer interface {
	Show(context.Context, *TestRequest) (*TestResponse, error)
	mustEmbedUnimplementedTestServer()
}

그리고 아래와 같이 rpc로 생성한 기능이 생성된 것을 볼 수 있다. gRPC Client와 Server로 함수처럼 이용이 가능하며 내부 로직을 몰라도 사용이 가능하다. 매우 Simple하다.

// Client
type testClient struct {
	cc grpc.ClientConnInterface
}

func NewTestClient(cc grpc.ClientConnInterface) TestClient {
	return &testClient{cc}
}

func (c *testClient) Show(ctx context.Context, in *TestRequest, opts ...grpc.CallOption) (*TestResponse, error) {
	out := new(TestResponse)
	err := c.cc.Invoke(ctx, "/grpc.Test/Show", in, out, opts...)
	if err != nil {
		return nil, err
	}
	return out, nil
}

// Server
// UnimplementedTestServer must be embedded to have forward compatible implementations.
type UnimplementedTestServer struct {
}

func (UnimplementedTestServer) Show(context.Context, *TestRequest) (*TestResponse, error) {
	return nil, status.Errorf(codes.Unimplemented, "method Show not implemented")
}
func (UnimplementedTestServer) mustEmbedUnimplementedTestServer() {}

그렇다면, gRPC Client와 Server의 코드를 작성해보자.

파일 구조

파일의 구조는 위와 같다. 

 

server.go

package main

import (
	"context"
	"log"
	"net"

	pb "go/grpc"
	"google.golang.org/grpc"
)

type server struct {
	pb.TestServer
}

func (s *server) Show(ctx context.Context, in *pb.TestRequest) (*pb.TestResponse, error) {
	log.Printf("Received: %v", in.GetRequestString())
	return &pb.TestResponse{ResponseString: "Hello " + in.GetRequestString()}, nil
}

func main() {
	lis, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}
	s := grpc.NewServer()
	pb.RegisterTestServer(s, &server{})
	log.Printf("server listening at %v", lis.Addr())
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

server 측에선 특정 port로 연결을 대기해놓는다. 참고로 REST API의 경우 요청마다 연결을 성립하지만 gRPC의 경우 한번 연결을 해놓으면 해당 연결을 재사용하며 API를 주고 받는다.

그렇다면? gRPC는 하나의 server당 하나의 client만 연결할 수 있는가?? (후에 정리)

 

client.go

package main

import (
	"context"
	"log"
	"time"

	pb "go/grpc"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

func main() {
	// Set up a connection to the server.
	conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()
	c := pb.NewTestClient(conn)
	
	// Contact the server and print out its response.
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	r, err := c.Show(ctx, &pb.TestRequest{RequestString: "KYJ"})
	if err != nil {
		log.Fatalf("could not greet: %v", err)
	}
	log.Printf("Test: %s", r.GetResponseString())
}

Client에서는 주소:port로 connection을 만들고 connection으로 Client를 생성합니다. 그리고 해당 Client의 interface에 정의된 함수로 요청을 Server측으로 보낼 수 있습니다.

 

 

테스트

server 실행

go run ./server/server.go

client 실행

go run ./client/client.go

server 출력

client 출력

 

 

참고자료

'Go' 카테고리의 다른 글

Import/Export  (0) 2023.12.24
package와 main  (0) 2023.12.24
Go에서 의존성 주입 (wire, fx 사용방법)  (0) 2023.05.14
gRPC란?  (0) 2023.05.01
Golang 설정 및 실행  (0) 2023.04.30

+ Recent posts