Go 가 벌써 버전 1.16 이 되었다. Go 1.11 부터 소개된 Go Module 이래로 많은 Go 개발 Workflow 가 바뀐 듯 하다.
많은 기능들이 있고, Release Note 를 살펴보면 추가된 기능을 볼 수 있다.
제일 마음에 드는 기능은 embed
. 사용법은 워낙 간단하다.
1.16 부터 Go Module 이 기본이라 go.mod
가 없으면 실행도 안된다.
한 개의 파일 임베딩 ¶
하나의 파일만 임베딩 시켜서 Go 내의 변수로 사용해보자. 어떤 경우에 사용하면 적합할까? 다양한 경우가 있겠지만 내 경우는 Windows 어플리케이션 제작 시 필요한 .ico
파일이나 웹 어플리케이션 서버 제작시 필요한 logo.png
등, 일부 글로벌하게 사용되는 경우에 주로 사용하게 되었다.
$ go mod init myproject
$ tree .
.
├── file.txt
├── go.mod
└── main.go
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
package main
import (
"fmt"
_ "embed"
)
//go:embed file.txt
var file string
func main() {
fmt.Printf("Contents of file.txt: %s\n", file)
}
|
file.txt
Contents of file.txt: Hello, World!
embed
모듈을 import 해서 사용했고, 사용하고자 하는 파일 위에 주석 같은 //go:embed 파일명
을 기재해주면 된다.
file.txt
파일이 없으면 실행이 되지 않는다.
9
10
|
//go:embed file.txt
var file string
|
string
을 사용하였는데, []byte
를 사용할 수도 있다.
3
4
5
6
7
|
import (
"fmt"
_ "embed"
)
|
하나의 파일만을 임베딩하였고, embed
모듈 내의 패키지를 사용하지 않았으므로, 초기화 시킨 기능만 사용하기 위하여 _ embed
으로 import 하였다.
여러 개의 파일 임베딩 ¶
여러 개의 파일을 임베딩 하는 경우는 대개 웹 어플리케이션 제작 시 필요한 템플릿 파일들의 임베딩, 웹 프론트엔드에서 사용되는 assets
디렉토리 임베딩, 혹은 public
디렉토리 임베딩이 일반적이었다. 정적 링크로 인해 단일 바이너리가 나오는 Go 의 빌드 특성상, 바이너리 내에 필요한 모든 웹 프론트엔드 자원들을 포함시켜서 빌드하는 경우 말이다.
혹시나 그렇게 사용하신다면 조금 주의하실 점이 있는데, 많은 용량을 바이너리에 담으면 프로그램을 실행할 때에 런타임에서의 메모리 사용량이 급격하게 늘어난다. 그래서 테스트를 충분히 거친 후 허용 가능한 만큼의 파일, 혹은 정말 필수불가결한 정도만큼을 담도록 하자. 혹은 컨테이너화 시켜서 임베딩을 제거할 수도 있다..!! 이에 대해선 밑에서 언급하는 조건적 임베딩을 참고하시면 된다.
$ go mod init myproject
$ tree .
.
├── go.mod
├── main.go
└── public
├── assets
│ ├── css
│ │ └── style.css
│ └── js
│ └── script.js
└── index.html
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
package main
import (
"embed"
"fmt"
"io/fs"
"log"
"net/http"
)
//go:embed public
var publicBox embed.FS
const PORT = 8080
func main() {
publicSubBox, _ := fs.Sub(publicBox, "public")
log.Printf("server is listening on :%d", PORT)
http.Handle("/", http.FileServer(http.FS(publicSubBox)))
if err := http.ListenAndServe(fmt.Sprintf(":%d", PORT), nil); nil != err {
log.Fatalln(err)
}
}
|
public/index.html
1
2
3
4
5
6
7
8
|
<!DOCTYPE html>
<html>
<head>
<link href="assets/css/style.css" rel="stylesheet">
<script src="assets/js/script.js"></script>
</head>
<body></body>
</html>
|
public/assets/css/style.css
public/assets/js/script.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
window.addEventListener('load', function () {
insertTitle();
insertBody();
});
function insertTitle() {
const title = document.createElement('title');
title.innerHTML = 'Hello, Title!';
document.head.appendChild(title);
}
function insertBody() {
const h1 = document.createElement('h1');
h1.innerHTML = 'Hello, World!'
document.body.appendChild(h1);
}
|
하나의 파일을 임베딩 할 때와는 다르게 embed
패키지를 직접 import 하였고, publicBox embed.FS
변수를 선언하여 해당 변수를 http 패키지의 FileServer 로 사용하였다. embed.FS
구조체는 Go 1.16 에 새롭게 추가된 io/fs 패키지의 fs.FS
인터페이스의 구현체이다.
빌드 태그를 이용한 조건적 임베딩 ¶
조건에 따라 파일을 임베딩 하고 싶지 않은 경우가 있다. 컨테이너 이미지로 빌드할 때나, assets 파일들은 별도의 CDN 에서 공급하려는 경우 등이 있을 것이다. 위에서 언급하였듯, 파일의 크기가 커지면 바이너리로 빌드 시 메모리 사용량이 많아진다. 파일 블롭을 전부 메모리에 적재해서 로드하니 당연한 일.
임베딩을 Go 프로그램 상에서 진행하면 별 문제가 없다. if else 로 나누면 되니까 말이다. 근데 얘는 주석 같은 태그를 사용하니, 이를 어떻게 해야 한단 말인가?
관련해서 이슈를 남겨보았더니, 빌드 태그를 이용하면 된다는 현답을 주셨다. 그래서 다음과 같이 임베딩을 하였다.
$ go mod init myproject
$ tree .
.
├── go.mod
├── main.go
└── public
├── assets
│ ├── css
│ │ └── style.css
│ └── js
│ └── script.js
├── box_embed.go
├── box.go
├── box_noembed.go
└── index.html
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
package main
import (
"fmt"
"log"
"net/http"
"myproject/public"
)
const PORT = 8080
var httpRoot http.FileSystem
func main() {
if public.IsEmbedded {
httpRoot = http.FS(public.Box)
} else {
httpRoot = http.Dir("public/")
}
log.Printf("server is listening on :%d", PORT)
http.Handle("/", http.FileServer(httpRoot))
if err := http.ListenAndServe(fmt.Sprintf(":%d", PORT), nil); nil != err {
log.Fatalln(err)
}
}
|
public/box.go
1
2
3
4
5
6
7
8
9
10
11
|
package public
import "embed"
var Box embed.FS
var IsEmbedded bool
func init() {
Box = ebox
}
|
public/box_embed.go
1
2
3
4
5
6
7
8
9
10
11
12
|
// +build embed
package public
import "embed"
//go:embed * index.html
var ebox embed.FS
func init() {
IsEmbedded = true
}
|
public/box_noembed.go
1
2
3
4
5
6
7
8
9
10
11
|
// +build !embed
package public
import "embed"
var ebox embed.FS
func init() {
IsEmbedded = false
}
|
위와 같이 3개의 go 파일을 두고, embed 와 noembed 가 아닐때를 구분하여서 사용하면, 빌드 시 embed 태그를 명시적으로 줄 때에만 파일을 임베딩 한다.
$ go build -tags embed main.go
내가 프로젝트를 할 때에 Go 를 이용한 웹 프로젝트의 경우 React 를 프론트엔드로 사용하기 때문에, Node.js 와 yarn 을 반드시 사용하므로 빌드 스크립트로 Node.js 를 사용하였다. 대략 다음과 같은 모양이다.
전체적인 빌드를 package.json
에 scripts 로 적당히 등록해놓는다.
package.json
1
2
3
4
5
6
7
8
9
10
11
|
{
"name": "project",
"private": true,
"dependencies": {},
"...": "...",
"scripts": {
"build": "node scripts/build/index.js",
"build:server": "node scripts/build/server.js",
"build:ui": "node scripts/build/ui.js"
}
}
|
scripts/build/server.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
// ...
const IS_ASSETS_EMBEDDING = process.env.NO_EMBED.toLowerCase() !== 'true';
// ...
try {
const serverBuildEnv = [
'GOOS=linux', 'GOARCH=amd64',
...buildEnvs,
];
const serverBuildArgv = [
'build', '-v', '-o', outputBinary,
'-ldflags', ldFlag,
...buildArgs,
];
if (IS_ASSETS_EMBEDDING) {
serverBuildArgv.push('-tags');
serverBuildArgv.push('embed');
}
// execute go build with serverBuildEnv, serverBuildArgv
}
// ...
|
그리고 컨테이너 이미지 빌드 시에는 embedding 이 필요 없으므로, Dockerfile 에서는 임베딩을 하지 않도록 설정하였다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
FROM <SOME IMAGE ONLY TO BUILD CONTAINER> AS builder
WORKDIR /workspace
COPY package.json .
# install dependencies
RUN yarn
# build
ENV NO_EMBED="true"
ENV GOPROXY="<PRIVATE GO PROXY>"
ENV GONOSUMDB="<PRIVATE GO REPOSITORY>"
COPY . .
RUN yarn build
FROM alpine:latest
RUN apk add --no-cache libc6-compat curl tzdata
# ...
|
더욱 자세한 스펙은 공식 홈페이지의 embed 관련 문서 을 참조하도록 하자.