Go 패키지 생성에서 버전관리 까지

GitHub 에서의 go 프로젝트 생성과 versioning 과정을 코드로 정리해 보고자 한다.

유의적 버전 2.0.0 | Semantic Versioning
https://github.com/golang-standards/project-layout

Semantic 버전관리

Go에서는 패키지 버저닝을 위해 Semantic Versioning을 사용하길 제안하고 있으며, 간단하게 정리를 하자면 다음과 같다.
  • ~기존 버전과 비호환~ 되며 API가 바뀌면 “MAJOR 버전”을 올리고,
  • ~기존 버전과 호환~ 되면서 ~새로운 기능을 추가~ 할 때는 “MINOR 버전”을 올리고,
  • ~기존 버전과 호환~ 되면서 ~버그를 수정~ 한 것이라면 “PATCH 버전”을 올린다.

프로젝트 레이아웃 생성

go 프로젝트에 공식적으로 잡힌 표준은 없으나 커뮤니티에서 잡힌 레이아웃은 존재하는 모양이다.  별 다른 제안이 없어서 GitHub에 공유된  GitHub - Standard Go Project Layout 프로젝트 레이아웃을 참고해서 사용하고 있고, 몇 가지는 내 입맛에 맞게 사용하고 있다.
아래 링크는 GopherCon 2018에서 발표된 Go 애플리케이션 구조에 대한 발표이고 간단하고 쉽게 참고할 만해서 적어둔다.
* [YouTube - "How Do You Structure Your Go Apps"](https://www.youtube.com/watch?v=oL6JBUk6tj0)
*  [PDF - "How Do You Structure Your Go Apps"](https://github.com/katzien/talks/blob/master/how-do-you-structure-your-go-apps/londongophers-2018-09-19/slides.pdf)
Go 애플리케이션 설계에 대한 번역글
번역 Go에서 애플리케이션 설계하기 · mingrammer's note

Github 에서 Go 패키지 버전관리 하기

vgo modules 가 1.11.1 에 go mod 내부에 도입 된 이후에,  modules 가 패키지 버전을 인식하는 방식에 대해 알아보자.
아래는 go modules 버전관리 방법을 가장 간단하게 나타낸 이미지이며,  Defining Go Modules  문서를 참고했다.
간단하게 설명하면 Major 버전 2.x 대가 되기전까지는 master 에서 tag 를 새로 따서 배포를 하고, 2.x 대가 넘어가게 되는 경우에는 v2 라는 이름으로 새로운 브랜치를 생성한 후에 go.mod 의 모듈패스에 /v2 와 같이 새로운 버전을 표기 하고 태그로 배포를 하는 형식이다.
다시 정리하면 버전 2.x 전까지는 master 에서 tag 로 배포, 2.x 이후 부터는 Major 버전 별로 브랜치를 생성해서 관리 하도록 한다.

hello 패키지 생성

이해를 돕기 위해 hello라는 패키지를 Github 에 생성하고 배포하는 것을 가정하여 작성해봤다.
/* hello 패키지 초기화 */

$ go mod init github.com/breezymind/hello
go: creating new go.mod: module github.com/breezymind/hello

첫 hello v0.1.0 버전 릴리즈

/* 개발이 완료 된 이후에 첫 릴리즈 버전을 v0.1.0 이라고 가정하고 배포 */

/* 로컬 태그 생성과 메시지 */
$ git tag -a v0.1.0 -m'v0.1.0 release~!!'

/* 로컬 태그 삭제는 git tag -d v0.1.0 */

/* 태그 목록 확인 */
$ git tag -l
v0.1.0

/* 원격 서버에 태그 생성 */
$ git push origin v0.1.0
/* 원격 서버에서 태그 삭제는 git push origin :v0.1.0 */

v2 대 이하에서는 go 소스코드에서 import 도 아래와 같이 그대로 작성하면 된다.

import github.com/breezymind/hello

v2.x 릴리즈를 위한 브랜치 생성

v2.x 이상은 브랜치를 새로 생성하여 관리하도록 한다
/* 어느덧 v2.x 를 릴리즈해야 할 시점 */

/* 로컬 v2 브랜치 생성 */
$ git checkout -b v2
/* 로컬 브랜치 삭제는 git branch -d v2 */

v2.x 릴리즈를 위한 go.mod 수정

go.mod 상단을 아래와 같이 끝에 브랜치 버전 수정
module github.com/breezymind/hello/v2

v2.x 릴리즈 반영

아래와 같이 변경점 반영 후 v2.0.0 릴리즈
$ git add .
$ git commit -m'v2 브랜치 추가'

/* 원격 서버에 v2 브랜치 추가 */
$ git push origin v2
/* 원격 브랜치 삭제는 git push origin :v2 */

$ git tag -a v2.0.0 -m'v2.0.0 release~!!'
$ git push origin v2.0.0

위와 같이 v2 대 이상에서는 go 소스코드에서 아래와 같이 브랜치 버전을 같이 표기하여 import 하도록 한다.

import github.com/breezymind/hello/v2

버전 표기하여 실행파일 빌드하기

-ldflags 빌드옵션으로 바이너리 파일에 Version이나 빌드일시, Git Revision등을 전역 변수로 넘겨서, 실행 파일이 어떤 버전을 컴파일 한 것인지 표기하도록 해보자

아래는 실행시 간단한 예시이며, 실행 파일의 버전과 빌드일시, 리비전을 확인 할 수 있다.

$ ./bin/sg-cli-api.o --help

[STRAIT-GATE],
CLI-API server (ver.1.0.0, bld.201811131059, rev.8e59e02)

Usage of ./bin/sg-cli-api.o:
  -mode string
    	running mode (default "docker")

다음은 Go 실행파일 내부에서 -ldflags 변수 받는 부분

package main

import (
	"flag"
	"fmt"
)

var (
	Version, Build, Revision string
)

func init() {	
	fmt.Printf(
		"\n[%s],\n%s (ver.%s, bld.%s, rev.%s)\n", 
		"STRAIT-GATE","CLI-API server", 
		Version, Build, Revision,  // -ldflags -X 옵션으로 넘긴 값들
	)

func main() {
...
}

다음은 shell script 또는 Makefile 에서 빌드할때 예시이다.

GOBASE=$(shell pwd)
GOPATH=$(GOBASE)/vendor
GOBIN=$(GOBASE)/bin

// 현재 년월일시분
BUILD=`date +%Y%m%d%H%M`
// 커밋 리비젼
REVISION=$(shell git rev-parse --short HEAD)
// LAST_VER 라는 변수는 현재 브랜치의 최신 태그 버전을 받음
LAST_VER=$(shell git describe --tags $(git rev-list --tags --max-count=1))
// 현재 브랜치 명을 같이 추가하고 싶을때는 아래 ldflags 에 추가하여 넘김
BRANCH=$(shell git rev-parse --abbrev-ref --verify HEAD)

...

// -ldflags -X로 전역변수 값을 넘김
@GOPATH=$(GOPATH) GOBIN=$(GOBIN) go build -mod vendor -ldflags "-X main.Version=$(LAST_VER) -X main.Build=$(BUILD) -X main.Revision=$(REVISION)" -o $(GOBIN)/$(app) $(GOBASE)/cmd/$(app)/$(app).go