Heroku, GiitHub Actions, Go로 명절 API 만들기

60679 단어 GoAPItech

제작 API


Go를 배우며 어렵게 뭔가를 하려고 사내 채팅에서 명절 API에 대한 이야기를 나누다 보니 이참에 Go로 명절 API를 만들었다.
이번에 제작된 명절 앱의 데이터는 내각부명절 웹사이트 CSV를 활용해 2021~2023년까지 제작됐다.
'2021'과'2022'를 입력하고 연도를 검색한 뒤 연도를 입력한 명절 일람을 JSON으로 표시합니다.

시스템 구성


システム構成図
나는 가능한 한 돈을 쓰지 않고 DB를 사용하지 않고 운용하는 데도 시간이 걸리지 않을 것이라고 생각한다
  • 명절의 데이터는 Go로 구성되어 처리 후 Json 형식으로 전환된다.
  • GiitHub Actions를 사용하여 Build/Test/Lin의 공정을 자동으로 실시하여 어느 정도의 품질을 보증합니다.
  • 지티허브의 마스터 창고에 통합하면 자동으로 히로쿠 창고의 예처리를 해 즉시 공개할 수 있다.
  • 이런 식으로 만들어 봤어요.
    방금 공개된 것이기 때문에 문제가 없지만 데이터의 수량과 처리 파라미터가 증가하면 시스템 구성에서 다시 한번 고려해 봅시다.

    ###완성품


    다음은 이번 성과물.
    https://github.com/jacoloves/go-sample-holiday-api
    요청은 두 가지 유형이 있습니다.

  • 요구https://go-holiday-api.herokuapp.com/holidayhttps://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"
    다음은 이번에 만든 결과물이다.
    https://github.com/jacoloves/go-sample-holiday-api

    절차.


    이번에 고(Go)로 구조체를 만든 뒤 이를 제이슨(Json)으로 전환하는 방법으로 API를 만들었다.
    제이슨 라이브러리의 Marshall Indent는 예쁘게 성형 데이터가 편리하기 때문이다.
    https://pkg.go.dev/encoding/json#MarshalIndent

    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의 작법은 모르겠지만 누군가가 나에게 템플릿을 줄 수 있기 때문에 나는 그것을 복제하고 조금만 수정하면 시작한다.
    https://qiita.com/takehanKosuke/items/4c5f5a28825eb460ae7f
    또 제1회 지아이허브에서 push가 안 되는 현상이 발생했지만 설정검사워크플로우를 통해 해결했다.
    https://zenn.dev/urasaku77/articles/dd94110dd1041a

    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가 아직 쓸 정도는 아닌 것을 사용하려고 한다.

    좋은 웹페이지 즐겨찾기