본문 바로가기

카테고리 없음

39. 클라우드 네이티브 Go 실습 1편

모든 것을 이해하고 한번에 글을 써 나가는 게 아니라, 이해하고 경험하고 조금씩 글을 써 나가다보니, 글이 조각처럼 나누어지고 나중에 다시 읽어보면서 퍼즐처럼 붙여나가게 된다. 잘못된 부분은 수정하고, 글을 자르고 붙이다 보면 글의 인과관계가 안 맞는 경우가 자주 발생한다. 계속 읽어보면서 수정하는데, 시간이 많이 걸린다. 이해바란다.

 

과거에는 풀스택 자바스크립트, 데이터분석 파이썬이 인기가 많은 프로그래밍 언어였다. 클라우브 네이티브에서 가장 인기있는 유용한 언어는 바로 Go이다.

요즘은 예전에는 백엔드 서버(자바 스프링) 프로그래밍을 할 기회가 없는 대신에, 데이터 엔지니어링과 프론트엔드에서 API 개발을 많이 하므로 파이썬과 자바스크립트에 집중했다. 클라우드 네이티브와 오픈소스 커스터마이징이 많은 요즘에는 Go에 대한 필요성을 많이 느낀다.

예전에는 빅쿼리 파이썬을 많이 개발하였는데 근래는 Go와 몽고 기반으로 개발한다. 개인적으로 구글 오픈소스를 선호하지만 회사 업무와 관련이 없다 보니, 구글을 사용하기는 어렵고 업무 생산성을 고려해서 솔루션을 선택하는 경우가 많다.

클라우드 네이티브 문서 포맷은 JSON이므로 JSON을 저장하고 분석하는데 많은 시간을 사용한다. JSON의 문제점은 복잡한 다계층 구조를 지니며 API 문서화가 부족한 경우가 많다 보니, 이를 활용하고 분석하는데 많은 어려움이 따른다. 몽고는 해결책을 제공한다. 몽고로 데이터를 저장하고 집계하는 로직을 개발할 경우에, 어떤 다른 솔루션보다 쉽고 빠르게 개발할 수 있었다.

클라우드 네이티브 어플리케이션은 대부분 Go로 개발되었다. 쿠버네티스 도커 테라폼 아르고 등이 좋은 예이다. 물론 다양한 언어를 사용해서 API를 제공하므로 Go가 아닌 다른 언어를 사용하는 것도 가능하지만, 클라우드 네이티브 환경에서 보다 좋은 최적화와 성능을 원한다면 Go를 추천한다

 

다양한 클라우드 네이티브 오픈소스가 Go로 개발되었다. 예를 들어보면

  1. 쿠버네티스, 인프라 운영 자동화
  2. 헬름, 쿠버네티스 패키지 관리
  3. 도커, 컨테이너
  4. 프로메테우스, 메트릭 및 알림 관리
  5. 아르고 CD, 쿠버네티스 배포
  6. 테라폼, 코드 기반 인프라 자동화
  7. 그라파나, 클라우드 모니터링
  8. 예거, 분산 추적
  9. OPA, 어플리케이션 접근제어 및 정책
  10. 키알리, 마이크로서비스 모니터링
  11. 오퍼레이터, 쿠버네티스 플러그인
  12. 하이퍼레저, 패브릭 블록체인 플랫폼
  13. 아파치 빔 파이프라인

사실 클라우드에서 구현된 중요 소프트웨어는 GO로 개발이 되었다. 기본적으로 이해해야 되는 것은 아래와 같다.

  • gRPC는 구글이 최초로 개발한 오픈 소스 원격 프로시저 호출 시스템이다. 전송을 위해 HTTP/2를, 인터페이스 정의 언어로 프로토콜 버퍼를 사용하며 인증, 양방향 스트리밍 및 흐름 제어, 차단 및 비차단 바인딩, 취소 및 타임아웃 등의 기능을 제공
  • Apache Beam 배치와 실시간 데이터 파이프라인을 개발하고, 다양한 런타임 환경을 지원
  • Go-kit 마이크로서비스 프레임워크이고 다양한 마이크로서비스 디자인 패턴을 지원
  • Mongo 가장 유명한 NoSQL이고 JSON의 관리 및 집계에 유용
  • Redis 가장 유명한 캐시 저장소
  • Tracing 관찰가능성 중 분산추적을 위한 프레임워크를 제공
  • Echo 웹 어플리케이션 프레임워크이고 Air와 함께 auto reload를 지원
  • MySQL은 오픈소스 RDBMS
  • GORM은 Go언어에서 사용 가능한 ORM(Object Relation Mapping) 라이브러리
  • Operator 사용자 정의 리소스의 컨트롤러 역할을 수행하며, 쿠버네티스 자원에 대한 멱등성을 구현
  • Exporter는 다양한 시스템으로부터 메트릭을 수집해서 프로메테우스에게 제공
  • RealWorld은 Echo, GORM 등을 사용한 레퍼런스 데모를 제공

위의 예제 외에도 Go는 블록체인, 머신러닝 등에도 활용되고 있다. 특히 블록체인에서는 Go를 많이 사용한다.

1편에서는 gRPC, Apache Beam, Go-kit을 데모하고 설명한다.
1. gRPC

 

C:\Users\germany>go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26
C:\Users\germany>go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1

 

Protocol Buffers v3.19.2 다운로드

https://github.com/protocolbuffers/protobuf/releases 에서 protoc-3.19.2-win64.zip 다운로드

 

C:\Users\germany\grpc-go\examples\helloworld>protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative helloworld/helloworld.proto

 

syntax = "proto3";

option go_package = "google.golang.org/grpc/examples/helloworld/helloworld";
option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";

package helloworld;

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

소스 다운로드

C:\Users\germany>git clone -b v1.41.0 https://github.com/grpc/grpc-go
Cloning into 'grpc-go'...
remote: Enumerating objects: 29525, done.
remote: Counting objects: 100% (35/35), done.
remote: Compressing objects: 100% (32/32), done.
remote: Total 29525 (delta 8), reused 22 (delta 3), pack-reused 29490
Receiving objects: 100% (29525/29525), 15.57 MiB | 11.42 MiB/s, done.
Resolving deltas: 100% (18598/18598), done.
Note: switching to 'a671967dfbaab779d37fd7e597d9248f13806087'.

서버 실행

package main

import (
	"context"
	"flag"
	"fmt"
	"log"
	"net"

	"google.golang.org/grpc"
	pb "google.golang.org/grpc/examples/helloworld/helloworld"
)

var (
	port = flag.Int("port", 50051, "The server port")
)

// server is used to implement helloworld.GreeterServer.
type server struct {
	pb.UnimplementedGreeterServer
}

// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
	log.Printf("Received: %v", in.GetName())
	return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}

func main() {
	flag.Parse()
	lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port))
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}
	s := grpc.NewServer()
	pb.RegisterGreeterServer(s, &server{})
	log.Printf("server listening at %v", lis.Addr())
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

 

C:\Users\germany>cd grpc-go/examples/helloworld
C:\Users\germany\grpc-go\examples\helloworld>go run greeter_server/main.go
go: downloading github.com/golang/protobuf v1.4.3
go: downloading google.golang.org/protobuf v1.25.0
go: downloading google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98
go: downloading golang.org/x/net v0.0.0-20200822124328-c89045814202
go: downloading golang.org/x/text v0.3.0
2022/01/10 09:40:11 server listening at [::]:50051
2022/01/10 09:42:24 Received: world

클라이언트 실행

package main

import (
	"context"
	"flag"
	"log"
	"time"

	"google.golang.org/grpc"
	pb "google.golang.org/grpc/examples/helloworld/helloworld"
)

const (
	defaultName = "world"
)

var (
	addr = flag.String("addr", "localhost:50051", "the address to connect to")
	name = flag.String("name", defaultName, "Name to greet")
)

func main() {
	flag.Parse()
	// Set up a connection to the server.
	conn, err := grpc.Dial(*addr, grpc.WithInsecure())
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()
	c := pb.NewGreeterClient(conn)

	// Contact the server and print out its response.
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	r, err := c.SayHello(ctx, &pb.HelloRequest{Name: *name})
	if err != nil {
		log.Fatalf("could not greet: %v", err)
	}
	log.Printf("Greeting: %s", r.GetMessage())
}
C:\Users\germany\grpc-go\examples\helloworld>go run greeter_client/main.go
2022/01/10 09:42:24 Greeting: Hello world

2. Apache Beam

Apache Beam Go 개발은 윈도우에서도 가능하다. 편의상 리눅스에서 개발을 진행한다.

[root@localhost shipping]# go get -u github.com/apache/beam/sdks/v2/go/pkg/beam
go: downloading github.com/apache/beam v1.9.1
go: downloading github.com/apache/beam/sdks/v2 v2.35.0
go: downloading github.com/apache/beam v2.35.0+incompatible
go: downloading google.golang.org/protobuf v1.27.1
go: downloading github.com/google/uuid v1.3.0
go: downloading google.golang.org/grpc v1.39.0
go: downloading google.golang.org/grpc v1.43.0
go: downloading golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6
go: downloading golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d
go: downloading google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f
go: downloading golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c
go: downloading golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e
go: downloading golang.org/x/text v0.3.6
go: downloading google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368
go: downloading golang.org/x/text v0.3.7
go get: added github.com/apache/beam/sdks/v2 v2.35.0
go get: upgraded golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 => v0.0.0-20220107192237-5cfca573fb4d
go get: upgraded golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 => v0.0.0-20211216021012-1d35b9e2eb4e
go get: upgraded golang.org/x/text v0.3.3 => v0.3.7
go get: upgraded google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 => v0.0.0-20220107163113-42d7afdf6368
go get: upgraded google.golang.org/grpc v1.38.0 => v1.43.0

소스 다운로드

[root@localhost shipping]# go install github.com/apache/beam/sdks/go/examples/wordcount
no required module provides package github.com/apache/beam/sdks/go/examples/wordcount; to add it:
	go get github.com/apache/beam/sdks/go/examples/wordcount
[root@localhost shipping]# go get github.com/apache/beam/sdks/go/examples/wordcount
go get: module github.com/apache/beam@upgrade found (v2.35.0+incompatible), but does not contain package github.com/apache/beam/sdks/go/examples/wordcount
[root@localhost shipping]#

 

package main

// beam-playground:
//   name: WordCount
//   description: An example that counts words in Shakespeare's works.
//   multifile: false
//   pipeline_options: --output output.txt
//   categories:
//     - Combiners
//     - Options

import (
	"context"
	"flag"
	"fmt"
	"log"
	"reflect"
	"regexp"
	"strings"

	"github.com/apache/beam/sdks/v2/go/pkg/beam"
	"github.com/apache/beam/sdks/v2/go/pkg/beam/io/textio"
	"github.com/apache/beam/sdks/v2/go/pkg/beam/transforms/stats"
	"github.com/apache/beam/sdks/v2/go/pkg/beam/x/beamx"
)

// Concept #2: Defining your own configuration options. Pipeline options can
// just be standard Go flags (or be obtained any other way). Defining and
// configuring the pipeline is normal Go code.
var (
	// By default, this example reads from a public dataset containing the text of
	// King Lear. Set this option to choose a different input file or glob.
	input = flag.String("input", "gs://apache-beam-samples/shakespeare/kinglear.txt", "File(s) to read.")

	// Set this required option to specify where to write the output.
	output = flag.String("output", "", "Output file (required).")
)

// Concept #3: You can make your pipeline assembly code less verbose and by
// defining your DoFns statically out-of-line. A DoFn can be defined as a Go
// function and is conventionally suffixed "Fn". The argument and return types
// dictate the pipeline shape when used in a ParDo: for example,
//
//      formatFn: string x int -> string
//
// indicate that it operates on a PCollection of type KV<string,int>, representing
// key value pairs of strings and ints, and outputs a PCollection of type string.
// Beam typechecks the pipeline before running it.
//
// DoFns that potentially output zero or multiple elements can also be Go functions,
// but have a different signature. For example,
//
//       extractFn : string x func(string) -> ()
//
// uses an "emit" function argument instead of string return type to allow it to
// output any number of elements. It operates on a PCollection of type string and
// returns a PCollection of type string. Also, using named function transforms allows
// for easy reuse, modular testing, and an improved monitoring experience.
//
// DoFns must be registered with Beam in order to be executed in ParDos. This is
// done automatically by the starcgen code generator, or it can be done manually
// by calling beam.RegisterFunction in an init() call.
func init() {
	beam.RegisterFunction(formatFn)
	beam.RegisterType(reflect.TypeOf((*extractFn)(nil)))
}

var (
	wordRE          = regexp.MustCompile(`[a-zA-Z]+('[a-z])?`)
	empty           = beam.NewCounter("extract", "emptyLines")
	smallWordLength = flag.Int("small_word_length", 9, "length of small words (default: 9)")
	smallWords      = beam.NewCounter("extract", "smallWords")
	lineLen         = beam.NewDistribution("extract", "lineLenDistro")
)

// extractFn is a DoFn that emits the words in a given line and keeps a count for small words.
type extractFn struct {
	SmallWordLength int `json:"smallWordLength"`
}

func (f *extractFn) ProcessElement(ctx context.Context, line string, emit func(string)) {
	lineLen.Update(ctx, int64(len(line)))
	if len(strings.TrimSpace(line)) == 0 {
		empty.Inc(ctx, 1)
	}
	for _, word := range wordRE.FindAllString(line, -1) {
		// increment the counter for small words if length of words is
		// less than small_word_length
		if len(word) < f.SmallWordLength {
			smallWords.Inc(ctx, 1)
		}
		emit(word)
	}
}

// formatFn is a DoFn that formats a word and its count as a string.
func formatFn(w string, c int) string {
	return fmt.Sprintf("%s: %v", w, c)
}

// Concept #4: A composite PTransform is a Go function that adds
// transformations to a given pipeline. It is run at construction time and
// works on PCollections as values. For monitoring purposes, the pipeline
// allows scoped naming for composite transforms. The difference between a
// composite transform and a construction helper function is solely in whether
// a scoped name is used.
//
// For example, the CountWords function is a custom composite transform that
// bundles two transforms (ParDo and Count) as a reusable function.

// CountWords is a composite transform that counts the words of a PCollection
// of lines. It expects a PCollection of type string and returns a PCollection
// of type KV<string,int>. The Beam type checker enforces these constraints
// during pipeline construction.
func CountWords(s beam.Scope, lines beam.PCollection) beam.PCollection {
	s = s.Scope("CountWords")

	// Convert lines of text into individual words.
	col := beam.ParDo(s, &extractFn{SmallWordLength: *smallWordLength}, lines)

	// Count the number of times each word occurs.
	return stats.Count(s, col)
}

func main() {
	// If beamx or Go flags are used, flags must be parsed first.
	flag.Parse()
	// beam.Init() is an initialization hook that must be called on startup. On
	// distributed runners, it is used to intercept control.
	beam.Init()

	// Input validation is done as usual. Note that it must be after Init().
	if *output == "" {
		log.Fatal("No output provided")
	}

	// Concepts #3 and #4: The pipeline uses the named transform and DoFn.
	p := beam.NewPipeline()
	s := p.Root()

	lines := textio.Read(s, *input)
	counted := CountWords(s, lines)
	formatted := beam.ParDo(s, formatFn, counted)
	textio.Write(s, *output, formatted)

	// Concept #1: The beamx.Run convenience wrapper allows a number of
	// pre-defined runners to be used via the --runner flag.
	if err := beamx.Run(context.Background(), p); err != nil {
		log.Fatalf("Failed to execute job: %v", err)
	}
}

빌드

[root@localhost wordcount]# pwd
/root/go/pkg/mod/github.com/apache/beam/sdks/v2@v2.35.0/go/examples/wordcount
[root@localhost wordcount]# go build
go: downloading cloud.google.com/go v0.81.0
go: downloading google.golang.org/api v0.45.0
go: downloading cloud.google.com/go/storage v1.15.0
go: downloading github.com/googleapis/gax-go/v2 v2.0.5
go: downloading golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914
go: downloading go.opencensus.io v0.23.0
go: downloading github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da
[root@localhost wordcount]#

실행

[root@localhost wordcount]# ./wordcount --input wordcount.go --output counts
2022/01/09 20:47:17 Executing pipeline with the direct runner.
2022/01/09 20:47:17 Pipeline:
2022/01/09 20:47:17 Nodes: {1: []uint8/bytes GLO}
{2: string/string GLO}
{3: string/string GLO}
{4: string/string GLO}
{5: string/string GLO}
{6: KV<string,int>/KV<string,int[varintz]> GLO}
{7: CoGBK<string,int>/CoGBK<string,int[varintz]> GLO}
{8: KV<string,int>/KV<string,int[varintz]> GLO}
{9: string/string GLO}
{10: KV<int,string>/KV<int[varintz],string> GLO}
{11: CoGBK<int,string>/CoGBK<int[varintz],string> GLO}
Edges: 1: Impulse [] -> [Out: []uint8 -> {1: []uint8/bytes GLO}]
2: ParDo [In(Main): []uint8 <- {1: []uint8/bytes GLO}] -> [Out: T -> {2: string/string GLO}]
3: ParDo [In(Main): string <- {2: string/string GLO}] -> [Out: string -> {3: string/string GLO}]
4: ParDo [In(Main): string <- {3: string/string GLO}] -> [Out: string -> {4: string/string GLO}]
5: ParDo [In(Main): string <- {4: string/string GLO}] -> [Out: string -> {5: string/string GLO}]
6: ParDo [In(Main): T <- {5: string/string GLO}] -> [Out: KV<T,int> -> {6: KV<string,int>/KV<string,int[varintz]> GLO}]
7: CoGBK [In(Main): KV<string,int> <- {6: KV<string,int>/KV<string,int[varintz]> GLO}] -> [Out: CoGBK<string,int> -> {7: CoGBK<string,int>/CoGBK<string,int[varintz]> GLO}]
8: Combine [In(Main): int <- {7: CoGBK<string,int>/CoGBK<string,int[varintz]> GLO}] -> [Out: KV<string,int> -> {8: KV<string,int>/KV<string,int[varintz]> GLO}]
9: ParDo [In(Main): KV<string,int> <- {8: KV<string,int>/KV<string,int[varintz]> GLO}] -> [Out: string -> {9: string/string GLO}]
10: ParDo [In(Main): T <- {9: string/string GLO}] -> [Out: KV<int,T> -> {10: KV<int,string>/KV<int[varintz],string> GLO}]
11: CoGBK [In(Main): KV<int,string> <- {10: KV<int,string>/KV<int[varintz],string> GLO}] -> [Out: CoGBK<int,string> -> {11: CoGBK<int,string>/CoGBK<int[varintz],string> GLO}]
12: ParDo [In(Main): CoGBK<int,string> <- {11: CoGBK<int,string>/CoGBK<int[varintz],string> GLO}] -> []
2022/01/09 20:47:17 Plan[plan]:
14: Impulse[0]
1: ParDo[textio.writeFileFn] Out:[]
2: CoGBK. Out:1
3: Inject[0]. Out:2
4: ParDo[beam.addFixedKeyFn] Out:[3]
5: ParDo[main.formatFn] Out:[4]
6: Combine[stats.sumIntFn] Keyed:false Out:5
7: CoGBK. Out:6
8: Inject[0]. Out:7
9: ParDo[stats.keyedCountFn] Out:[8]
10: ParDo[main.extractFn] Out:[9]
11: ParDo[textio.readFn] Out:[10]
12: ParDo[textio.expandFn] Out:[11]
13: ParDo[beam.createFn] Out:[12]
2022/01/09 20:47:17 Reading from wordcount.go
2022/01/09 20:47:17 Writing to counts
[root@localhost wordcount]#

3. Go-kit

소스 다운로드

[root@localhost helloworld]# git clone https://github.com/go-kit/examples.git
Cloning into 'examples'...
remote: Enumerating objects: 1601, done.
remote: Counting objects: 100% (1601/1601), done.
remote: Compressing objects: 100% (718/718), done.
remote: Total 1601 (delta 744), reused 1591 (delta 736), pack-reused 0
Receiving objects: 100% (1601/1601), 4.22 MiB | 3.90 MiB/s, done.
Resolving deltas: 100% (744/744), done.
[root@localhost helloworld]# cd examples

실행

[root@localhost examples]# cd shipping
[root@localhost shipping]# go run main.go
go: downloading github.com/go-kit/kit v0.10.0
go: downloading github.com/prometheus/client_golang v1.10.0
go: downloading github.com/gorilla/mux v1.8.0
go: downloading github.com/pborman/uuid v1.2.1
go: downloading github.com/go-logfmt/logfmt v0.5.0
go: downloading github.com/prometheus/client_model v0.2.0
go: downloading github.com/prometheus/common v0.18.0
go: downloading github.com/beorn7/perks v1.0.1
go: downloading github.com/cespare/xxhash/v2 v2.1.1
go: downloading github.com/golang/protobuf v1.5.2
go: downloading github.com/prometheus/procfs v0.6.0
go: downloading github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5
go: downloading github.com/sony/gobreaker v0.4.1
go: downloading github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a
go: downloading github.com/google/uuid v1.1.2
go: downloading google.golang.org/protobuf v1.26.0
go: downloading github.com/matttproud/golang_protobuf_extensions v1.0.1
go: downloading golang.org/x/sys v0.0.0-20210309074719-68d13333faf2

 

소스 다운로드

[root@localhost Downloads]# git clone https://github.com/go-kit/examples.git
Cloning into 'examples'...
remote: Enumerating objects: 1615, done.
remote: Counting objects: 100% (1615/1615), done.
remote: Compressing objects: 100% (726/726), done.
remote: Total 1615 (delta 749), reused 1601 (delta 738), pack-reused 0
Receiving objects: 100% (1615/1615), 4.22 MiB | 0 bytes/s, done.
Resolving deltas: 100% (749/749), done.
[root@localhost Downloads]# ll
total 0
drwxr-xr-x. 12 root root 249 Jan  9 20:55 examples
[root@localhost Downloads]# cd examples

빌드

[root@localhost stringsvc1]# go build
[root@localhost stringsvc1]# ll
total 6576
-rw-r--r--. 1 root root    2818 Jan  9 20:55 main.go
-rwxr-xr-x. 1 root root 6727823 Jan  9 20:56 stringsvc1

서버 실행

package main

import (
	"context"
	"encoding/json"
	"errors"
	"log"
	"net/http"
	"strings"

	"github.com/go-kit/kit/endpoint"
	httptransport "github.com/go-kit/kit/transport/http"
)

// StringService provides operations on strings.
type StringService interface {
	Uppercase(string) (string, error)
	Count(string) int
}

// stringService is a concrete implementation of StringService
type stringService struct{}

func (stringService) Uppercase(s string) (string, error) {
	if s == "" {
		return "", ErrEmpty
	}
	return strings.ToUpper(s), nil
}

func (stringService) Count(s string) int {
	return len(s)
}

// ErrEmpty is returned when an input string is empty.
var ErrEmpty = errors.New("empty string")

// For each method, we define request and response structs
type uppercaseRequest struct {
	S string `json:"s"`
}

type uppercaseResponse struct {
	V   string `json:"v"`
	Err string `json:"err,omitempty"` // errors don't define JSON marshaling
}

type countRequest struct {
	S string `json:"s"`
}

type countResponse struct {
	V int `json:"v"`
}

// Endpoints are a primary abstraction in go-kit. An endpoint represents a single RPC (method in our service interface)
func makeUppercaseEndpoint(svc StringService) endpoint.Endpoint {
	return func(_ context.Context, request interface{}) (interface{}, error) {
		req := request.(uppercaseRequest)
		v, err := svc.Uppercase(req.S)
		if err != nil {
			return uppercaseResponse{v, err.Error()}, nil
		}
		return uppercaseResponse{v, ""}, nil
	}
}

func makeCountEndpoint(svc StringService) endpoint.Endpoint {
	return func(_ context.Context, request interface{}) (interface{}, error) {
		req := request.(countRequest)
		v := svc.Count(req.S)
		return countResponse{v}, nil
	}
}

// Transports expose the service to the network. In this first example we utilize JSON over HTTP.
func main() {
	svc := stringService{}

	uppercaseHandler := httptransport.NewServer(
		makeUppercaseEndpoint(svc),
		decodeUppercaseRequest,
		encodeResponse,
	)

	countHandler := httptransport.NewServer(
		makeCountEndpoint(svc),
		decodeCountRequest,
		encodeResponse,
	)

	http.Handle("/uppercase", uppercaseHandler)
	http.Handle("/count", countHandler)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

func decodeUppercaseRequest(_ context.Context, r *http.Request) (interface{}, error) {
	var request uppercaseRequest
	if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
		return nil, err
	}
	return request, nil
}

func decodeCountRequest(_ context.Context, r *http.Request) (interface{}, error) {
	var request countRequest
	if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
		return nil, err
	}
	return request, nil
}

func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
	return json.NewEncoder(w).Encode(response)
}
[root@localhost stringsvc1]# ./stringsvc1

클라이언트 실행

[root@localhost ~]# curl -XPOST -d'{"s":"hello, world"}' localhost:8080/uppercase
{"v":"HELLO, WORLD"}
[root@localhost ~]#