Heroku, GiitHub Actions, Go로 명절 API 만들기
제작 API
Go를 배우며 어렵게 뭔가를 하려고 사내 채팅에서 명절 API에 대한 이야기를 나누다 보니 이참에 Go로 명절 API를 만들었다.
이번에 제작된 명절 앱의 데이터는 내각부명절 웹사이트 CSV를 활용해 2021~2023년까지 제작됐다.
'2021'과'2022'를 입력하고 연도를 검색한 뒤 연도를 입력한 명절 일람을 JSON으로 표시합니다.
시스템 구성
나는 가능한 한 돈을 쓰지 않고 DB를 사용하지 않고 운용하는 데도 시간이 걸리지 않을 것이라고 생각한다
방금 공개된 것이기 때문에 문제가 없지만 데이터의 수량과 처리 파라미터가 증가하면 시스템 구성에서 다시 한번 고려해 봅시다.
###완성품
다음은 이번 성과물.
요청은 두 가지 유형이 있습니다.
요구
https://go-holiday-api.herokuapp.com/holiday
나https://go-holiday-api.herokuapp.com/holiday/
하면 2021~2023년 모든 명절의 데이터를 얻을 수 있다.요청
https://go-holiday-api.herokuapp.com/holiday/year/yyyy
을 통해'yyy'년의 명절을 획득할 수 있습니다.사용 예
curl -s https://go-holiday-api.herokuapp.com/holiday/year/2022
반응은 다음과 같다.[
{
"Title": "元旦",
"Date": "2022-01-01"
},
{
"Title": "成人の日",
"Date": "2022-01-10"
},
{
"Title": "建国記念の日",
"Date": "2022-02-11"
},
{
"Title": "天皇誕生日",
"Date": "2022-02-23"
},
{
"Title": "春分の日",
"Date": "2022-03-21"
},
{
"Title": "昭和の日",
"Date": "2022-04-29"
},
{
"Title": "憲法記念日",
"Date": "2022-05-03"
},
{
"Title": "みどりの日",
"Date": "2022-05-04"
},
]
달력 데이터는 다음과 같은 구조이다.이름:
타입
설명
예제
Title
string
명절 명칭
"설날"
Date
string
날짜는 YYY-MM-DD 형식으로 표시
"2022-01-01"
다음은 이번에 만든 결과물이다.
절차.
이번에 고(Go)로 구조체를 만든 뒤 이를 제이슨(Json)으로 전환하는 방법으로 API를 만들었다.
제이슨 라이브러리의 Marshall Indent는 예쁘게 성형 데이터가 편리하기 때문이다.
sever.go
후술한 명절을 구조체로 쓴 Go 파일을 읽고 차례대로 dateHandler라는 구조체에 저장한다.
이후 요청에 따라 출력 방법을 변경했습니다.
package main
import (
"encoding/json"
"log"
"net/http"
"os"
"regexp"
"sync"
)
var (
listHolidayRe = regexp.MustCompile(`^\/holiday[\/]*$`)
getHolidayRe = regexp.MustCompile(`^\/holiday\/year\/(\d+)$`)
)
type Data []struct {
Title string `json:"Title"`
Date string `json:"Date"`
}
type datastore struct {
m map[string]Data
*sync.RWMutex
}
type dateHandler struct {
store *datastore
}
func (h *dateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && listHolidayRe.MatchString(r.URL.Path):
h.List(w, r)
return
case r.Method == http.MethodGet && getHolidayRe.MatchString(r.URL.Path):
h.Get(w, r)
return
default:
notFound(w, r)
return
}
}
func (h *dateHandler) List(w http.ResponseWriter, r *http.Request) {
h.store.RLock()
holiday := make([]Data, 0, len(h.store.m))
for _, v := range h.store.m {
holiday = append(holiday, v)
}
h.store.RUnlock()
jsonBytes, err := json.MarshalIndent(holiday, " ", " ")
if err != nil {
internalServerError(w, r)
return
}
w.WriteHeader(http.StatusOK)
_, err = w.Write(jsonBytes)
if err != nil {
log.Fatal(err)
return
}
}
func (h *dateHandler) Get(w http.ResponseWriter, r *http.Request) {
matches := getHolidayRe.FindStringSubmatch(r.URL.Path)
if len(matches) < 2 {
notFound(w, r)
return
}
h.store.RLock()
y, ok := h.store.m[matches[1]]
if !ok {
w.WriteHeader(http.StatusNotFound)
_, err := w.Write([]byte("year not found"))
if err != nil {
log.Fatal(err)
return
}
}
jsonBytes, err := json.MarshalIndent(y, " ", " ")
if err != nil {
internalServerError(w, r)
return
}
w.WriteHeader(http.StatusOK)
_, err = w.Write(jsonBytes)
if err != nil {
log.Fatal(err)
return
}
}
func internalServerError(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
_, err := w.Write([]byte("internal server error"))
if err != nil {
log.Fatal(err)
}
}
func notFound(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
_, err := w.Write([]byte("not found"))
if err != nil {
log.Fatal(err)
}
}
func main() {
port := os.Getenv("PORT")
h1 := holiday_2021()
h2 := holiday_2022()
h3 := holiday_2023()
mux := http.NewServeMux()
dHandler := &dateHandler{
store: &datastore{
m: map[string]Data{
"2021": Data(h1),
"2022": Data(h2),
"2023": Data(h3),
},
RWMutex: &sync.RWMutex{},
},
}
mux.Handle("/holiday", dHandler)
mux.Handle("/holiday/", dHandler)
if err := http.ListenAndServe(":"+port, mux); err != nil {
log.Fatal(err)
}
}
holiday_YYYY.go
각 해의 명절을 구조체로 보존하고 있는 문서다.
이번에는 2021, 2022, 2023 세 개의 서류를 준비했다.
package main
type Data_2021 []struct {
Title string `json:"Title"`
Date string `json:"Date"`
}
func holiday_2021() Data_2021 {
holidays_2021 := Data_2021{
{
Title: "元旦",
Date: "2021-01-01",
},
{
Title: "成人の日",
Date: "2021-01-11",
},
{
Title: "建国記念の日",
Date: "2021-02-11",
},
{
Title: "天皇誕生日",
Date: "2021-02-23",
},
{
Title: "春分の日",
Date: "2021-03-20",
},
{
Title: "昭和の日",
Date: "2021-04-29",
},
{
Title: "憲法記念日",
Date: "2021-05-03",
},
{
Title: "みどりの日",
Date: "2021-05-04",
},
{
Title: "こどもの日",
Date: "2021-05-05",
},
{
Title: "海の日",
Date: "2021-07-22",
},
{
Title: "スポーツの日",
Date: "2021-07-23",
},
{
Title: "山の日",
Date: "2021-08-08",
},
{
Title: "休日",
Date: "2021-08-09",
},
{
Title: "敬老の日",
Date: "2021-09-20",
},
{
Title: "秋分の日",
Date: "2021-09-23",
},
{
Title: "文化の日",
Date: "2021-11-03",
},
{
Title: "勤労感謝の日",
Date: "2021-11-23",
},
}
return holidays_2021
}
Test
sever_test.go
이번에도 일반에 공개된 것이 있는데 처음으로 고(Go)로 테스트를 썼다.
Http의 상태 코드만 확인했지만 앞으로도 데이터 확인이 이뤄질 것으로 생각한다.
(더 예쁜 글씨가 있을 것 같아서 앞으로 개선하고 싶어요.)
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"sync"
"testing"
)
func TestMain(t *testing.T) {
h1 := holiday_2021()
h2 := holiday_2022()
h3 := holiday_2023()
dHandler := &dateHandler{
store: &datastore{
m: map[string]Data{
"2021": Data(h1),
"2022": Data(h2),
"2023": Data(h3),
},
RWMutex: &sync.RWMutex{},
},
}
ts := httptest.NewServer(http.Handler(dHandler))
defer ts.Close()
// /holiday pass Test
resp, err := http.Get(fmt.Sprintf("%s/holiday", ts.URL))
if err != nil {
t.Fatalf("Excepted no error, got %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("Expected status code 200, got %v", resp.StatusCode)
}
// /holiday/ pass Test
resp2, err := http.Get(fmt.Sprintf("%s/holiday/", ts.URL))
if err != nil {
t.Fatalf("Excepted no error, got %v", err)
}
defer resp2.Body.Close()
if resp2.StatusCode != http.StatusOK {
t.Fatalf("Expected status code 200, got %v", resp2.StatusCode)
}
// /holiday/year/2022 pass Test
resp3, err := http.Get(fmt.Sprintf("%s/holiday/year/2022", ts.URL))
if err != nil {
t.Fatalf("Excepted no error, got %v", err)
}
defer resp3.Body.Close()
if resp3.StatusCode != http.StatusOK {
t.Fatalf("Expected status code 200, got %v", resp3.StatusCode)
}
// /holiday/year/2021 pass Test
resp4, err := http.Get(fmt.Sprintf("%s/holiday/year/2021", ts.URL))
if err != nil {
t.Fatalf("Excepted no error, got %v", err)
}
defer resp4.Body.Close()
if resp4.StatusCode != http.StatusOK {
t.Fatalf("Expected status code 200, got %v", resp4.StatusCode)
}
// /holiday/year/2023 pass Test
resp5, err := http.Get(fmt.Sprintf("%s/holiday/year/2023", ts.URL))
if err != nil {
t.Fatalf("Excepted no error, got %v", err)
}
defer resp5.Body.Close()
if resp5.StatusCode != http.StatusOK {
t.Fatalf("Expected status code 200, got %v", resp5.StatusCode)
}
// /holiday/year/2020 not pass Test
resp6, err := http.Get(fmt.Sprintf("%s/holiday/year/2020", ts.URL))
if err != nil {
t.Fatalf("Excepted no error, got %v", err)
}
defer resp6.Body.Close()
if resp6.StatusCode != http.StatusNotFound {
t.Fatalf("Expected status code 404, got %v", resp6.StatusCode)
}
// /holiday/year/2024 not pass Test
resp7, err := http.Get(fmt.Sprintf("%s/holiday/year/2024", ts.URL))
if err != nil {
t.Fatalf("Excepted no error, got %v", err)
}
defer resp7.Body.Close()
if resp7.StatusCode != http.StatusNotFound {
t.Fatalf("Expected status code 404, got %v", resp7.StatusCode)
}
}
GiHub Actions
GiitHub Action은 이번이 처음이어서 매우 편리합니다.
yml의 작법은 모르겠지만 누군가가 나에게 템플릿을 줄 수 있기 때문에 나는 그것을 복제하고 조금만 수정하면 시작한다.
또 제1회 지아이허브에서 push가 안 되는 현상이 발생했지만 설정검사워크플로우를 통해 해결했다.
go-sample-holiday-api.yml
name: Go
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
# setup
setup:
runs-on: ubuntu-latest
steps:
- name: set up
uses: actions/setup-go@v2
with:
go-version: 1.17
id: go
- name: check out
uses: actions/checkout@v2
- name: Cache
uses: actions/[email protected]
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
# build
build:
needs: setup
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: build
run: go build ./...
# test
test:
needs: setup
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: test
run: go test ./... -v
# lint
lint:
needs: setup
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: golangci-lint
uses: golangci/golangci-lint-action@v2
with:
version: v1.29
돌아보다
GiitHub Action은 이번이 처음이라 편리합니다!
Build/Test/Light는 자동이어서 어느 정도 품질을 보증할 수 있기 때문에 발표 전의 심리장애는 대폭 감소합니다!
앞으로 무엇을 할 때도 채택할 것이다.
오랜만에 Heroku를 적용했지만 간단한 앱을 개발할 때 편리하기 때문에 AWS와 다른 Iaas가 아직 쓸 정도는 아닌 것을 사용하려고 한다.
Reference
이 문제에 관하여(Heroku, GiitHub Actions, Go로 명절 API 만들기), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://zenn.dev/tanashon/articles/9c79d4b3b372aa텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)