Go 언어로 Tail 명령을 만들어 보세요.

Go 언어로 Tail 명령을 만들어 보세요.


최근 Go 언어를 배우기 시작했고, 학습의 일환으로 Go 언어로 Tail 지령을 만들어 보았다.
실제 설치부터 테스트용 유닛 테스트, GiitHub Actions의 CI/CD를 사용해 보고 싶습니다.
버전 업그레이드 등으로 인해 프로그램이 다를 수 있으니 주의하십시오

물줄기

  • Docker를 통해 Go의 개발 환경 구축
  • 다단계 구축의 실현
  • 대략적인 규격을 고려해 보세요
  • 설치
  • 단원 테스트
  • 테스트 덮어쓰기 확인
  • GiitHub Action 기반 CI/CD(Go, Docker)
  • 개발 환경

  • macOS BigSur 11.2.3(20D91)
  • Docker Desktop for Mac 20.10.5, build 55c4c88
  • Docker를 통해 Go의 개발 환경 구축


    우선 지금까지 Go를 사용한 적이 없기 때문에 앞으로 Docker의 Go를 구축하는 개발 환경도 고려해야 한다.

    Docker 설치



    Mac 용 Docker 설치
    공식 사이트에서 Docker 계정을 만들어 로그인하고 Docker Hub에서 다운로드하여 설치합니다.
    https://hub.docker.com/editions/community/docker-ce-desktop-mac
    설치 후 CLI를 사용하여 확인합니다.
    $ docker -v
    Docker version 20.10.5, build 55c4c88
    
    버전이 이렇게 표시되면 완료됩니다.
    ubuntu를 사용해 보세요.
    먼저 테스트 폴더를 만드는 등 Docker file을 만듭니다.
    $ mkdir test
    $ cd test
    $ echo "From ubuntu" > Dockerfile
    
    다음에 구축하고 셸에 들어갑니다.
    $ docker build -t test .
    $ docker run -it test bash
    $ cat /etc/os-release
    
    잘 작동하면 이런 정보가 표시됩니다.
    NAME="Ubuntu"
    VERSION="20.04.2 LTS (Focal Fossa)"
    
    이렇게 하면 Docker의 동작을 확인할 수 있다.
    이 컨테이너는 필요 없으니 우선 캐시 삭제를 포함해라.
    $ docker system prune -a
    

    다단계 구축


    이름만 물어보면 어려운 인상을 줄 수 있으니 간단하게 요약해 보면
  • 여러 개의 Docker file이 필요한 경우 한 개의 Docker file에서 여러 개의 이미지 Build
  • 를 생성할 수 있습니다.
  • From을 트리거로 하여 여러 레벨의 산물을 계승할 수 있음
  • 따라서 하나의 Docker file로 여러 컨테이너를 관리할 수 있습니다.
    다중 레벨 구축을 설정하기 전에 파일의 구성을 결정하십시오.
    local
     gotail
     ├── .github
     │    └── workflows
     │        └── go.yml
     ├── README.md
     ├── Dockerfile
     ├── Makefile
     ├── main.go
     ├── main_test.go
     ├── test.txt
     ├── cmd.sh
     └── covercheck.sh
    
    다음은 다단계 구축 중인 컨테이너 안의 파일 구조를 고려한다.

    용기1:고


    stage1
     go
     └── src
         ├── main.go
         ├── main_test.go
         ├── test.txt
         └── cmd.sh
    

    컨테이너 2:alpine linux


    stage2
     root
     ├── main
     ├── test.txt
     └── cmd.sh
    
    상기 파일 구조의 다단계 구축을 실현하려면 다음과 같다.
    /local/Dockerfile
    FROM golang:latest
    WORKDIR /go/src
    COPY main.go .
    COPY main_test.go .
    COPY test.txt .
    COPY cmd.sh .
    RUN go test main_test.go main.go -v
    RUN go build main.go
    
    FROM alpine:latest
    RUN apk --no-cache add ca-certificates && \
        apk add bash
    WORKDIR /root
    COPY --from=0 /go/src/main .
    COPY --from=0 /go/src/test.txt .
    COPY --from=0 /go/src/cmd.sh .
    CMD ["./cmd.sh"]
    
    다단계 구문은 여기서 끝냅니다.
    자세한 내용은 공식 홈페이지를 보세요.
    https://docs.docker.com/develop/develop-images/multistage-build/

    대략적인 스타일을 결정하다


    이번에tail 명령의 기본 동작과 n 옵션의 기능을 실현하려고 합니다.

    이른바 tail


    파일의 마지막 줄 수를 표시하는 명령입니다. 기본적으로 10줄을 표시합니다.
    tail 
    オプション -n
    出力する行数を指定する
    
    FIFO 알고리즘을 사용하여 - n 옵션을 실현한다.

    이른바 FIFO


    FIFO는 First In First Out의 약칭입니다.
    FIFO를 사용하면 모든 데이터를 스토리지에 저장하지 않고 파일을 읽을 수 있습니다.
    비키에 상세한 기록이 있다.
    https://ja.wikipedia.org/wiki/FIFO
    이 알고리즘을 사용하여tail을 실현합니다.

    사용할 라이브러리


    명령 주체의 라이브러리 목록
    main.go
    import (
    	"bufio"
    	"flag"
    	"fmt"
    	"math"
    	"os"
    )
    
    테스트 라이브러리 일람
    main_test.go
    import (
    	"bufio"
    	"fmt"
    	"os"
    	"reflect"
    	"strconv"
    	"testing"
    )
    
    우선 상상하기 편하도록 상세히 실시한다.
    대기열 초기화
    init_queue
    func init_queue() ([]string, int) {
    	queue := []string{}
    	cursor := 0
    	return queue, cursor
    }
    
    대열
    enqueue
    func enqueue(queue []string, value string) []string {
    	queue = append(queue, value)
    	return queue
    }
    
    대열
    dequeue
    func dequeue(queue []string) []string {
    	queue = queue[1:]
    	return queue
    }
    
    체크 아웃 대기열
    show_queue
    func show_queue(queue []string, n int) []string {
    	if len(queue) == n {
    		for i := n; i > 0; i-- {
    			if len(queue) != 0 {
    				fmt.Println(queue[0])
    			}
    			queue = dequeue(queue)
    		}
    	} else {
    		for i := len(queue); i > 0; i-- {
    			if len(queue) != 0 {
    				fmt.Println(queue[0])
    			}
    			queue = dequeue(queue)
    		}
    	}
    	return queue
    }
    
    일련의 프로세스를tail로 정의
    tail
    func tail(stream *os.File, err error, n int) []string {
    	queue, cursor := init_queue()
    	scanner := bufio.NewScanner(stream)
    	for scanner.Scan() {
    		if n < 1 {
    			n = int(math.Abs(float64(n)))
    			if n == 0 {
    				n = 10
    			}
    		}
    		queue = enqueue(queue, scanner.Text())
    		if n-1 < cursor {
    			queue = dequeue(queue)
    		}
    		cursor++
    	}
    	return queue
    }
    
    실행을 해보면 이런 느낌이 든다. ※이해하기 쉽도록'queue'를 보여 줍니다.test.txt는 1~100의 시퀀스 번호를 한 줄 한 줄 포함하는 파일이다.
    bash
    $ for i in `seq 100`
    for> echo $i >> test.txt
    
    main.go
    $ go run main.go test.txt
    [1]
    [1 2]
    [1 2 3]
    [1 2 3 4]
    [1 2 3 4 5]
    [1 2 3 4 5 6]
    [1 2 3 4 5 6 7]
    [1 2 3 4 5 6 7 8]
    [1 2 3 4 5 6 7 8 9]
    [1 2 3 4 5 6 7 8 9 10]
    [1 2 3 4 5 6 7 8 9 10 11]
    [2 3 4 5 6 7 8 9 10 11 12]
    [3 4 5 6 7 8 9 10 11 12 13]
    [4 5 6 7 8 9 10 11 12 13 14]
    [5 6 7 8 9 10 11 12 13 14 15]
    [6 7 8 9 10 11 12 13 14 15 16]
    [7 8 9 10 11 12 13 14 15 16 17]
    [8 9 10 11 12 13 14 15 16 17 18]
    [9 10 11 12 13 14 15 16 17 18 19]
    [10 11 12 13 14 15 16 17 18 19 20]
    [11 12 13 14 15 16 17 18 19 20 21]
    [12 13 14 15 16 17 18 19 20 21 22]
    [13 14 15 16 17 18 19 20 21 22 23]
    [14 15 16 17 18 19 20 21 22 23 24]
    [15 16 17 18 19 20 21 22 23 24 25]
    [16 17 18 19 20 21 22 23 24 25 26]
    [17 18 19 20 21 22 23 24 25 26 27]
    [18 19 20 21 22 23 24 25 26 27 28]
    [19 20 21 22 23 24 25 26 27 28 29]
    [20 21 22 23 24 25 26 27 28 29 30]
    [21 22 23 24 25 26 27 28 29 30 31]
    [22 23 24 25 26 27 28 29 30 31 32]
    [23 24 25 26 27 28 29 30 31 32 33]
    [24 25 26 27 28 29 30 31 32 33 34]
    [25 26 27 28 29 30 31 32 33 34 35]
    [26 27 28 29 30 31 32 33 34 35 36]
    [27 28 29 30 31 32 33 34 35 36 37]
    [28 29 30 31 32 33 34 35 36 37 38]
    [29 30 31 32 33 34 35 36 37 38 39]
    [30 31 32 33 34 35 36 37 38 39 40]
    [31 32 33 34 35 36 37 38 39 40 41]
    [32 33 34 35 36 37 38 39 40 41 42]
    [33 34 35 36 37 38 39 40 41 42 43]
    [34 35 36 37 38 39 40 41 42 43 44]
    [35 36 37 38 39 40 41 42 43 44 45]
    [36 37 38 39 40 41 42 43 44 45 46]
    [37 38 39 40 41 42 43 44 45 46 47]
    [38 39 40 41 42 43 44 45 46 47 48]
    [39 40 41 42 43 44 45 46 47 48 49]
    [40 41 42 43 44 45 46 47 48 49 50]
    [41 42 43 44 45 46 47 48 49 50 51]
    [42 43 44 45 46 47 48 49 50 51 52]
    [43 44 45 46 47 48 49 50 51 52 53]
    [44 45 46 47 48 49 50 51 52 53 54]
    [45 46 47 48 49 50 51 52 53 54 55]
    [46 47 48 49 50 51 52 53 54 55 56]
    [47 48 49 50 51 52 53 54 55 56 57]
    [48 49 50 51 52 53 54 55 56 57 58]
    [49 50 51 52 53 54 55 56 57 58 59]
    [50 51 52 53 54 55 56 57 58 59 60]
    [51 52 53 54 55 56 57 58 59 60 61]
    [52 53 54 55 56 57 58 59 60 61 62]
    [53 54 55 56 57 58 59 60 61 62 63]
    [54 55 56 57 58 59 60 61 62 63 64]
    [55 56 57 58 59 60 61 62 63 64 65]
    [56 57 58 59 60 61 62 63 64 65 66]
    [57 58 59 60 61 62 63 64 65 66 67]
    [58 59 60 61 62 63 64 65 66 67 68]
    [59 60 61 62 63 64 65 66 67 68 69]
    [60 61 62 63 64 65 66 67 68 69 70]
    [61 62 63 64 65 66 67 68 69 70 71]
    [62 63 64 65 66 67 68 69 70 71 72]
    [63 64 65 66 67 68 69 70 71 72 73]
    [64 65 66 67 68 69 70 71 72 73 74]
    [65 66 67 68 69 70 71 72 73 74 75]
    [66 67 68 69 70 71 72 73 74 75 76]
    [67 68 69 70 71 72 73 74 75 76 77]
    [68 69 70 71 72 73 74 75 76 77 78]
    [69 70 71 72 73 74 75 76 77 78 79]
    [70 71 72 73 74 75 76 77 78 79 80]
    [71 72 73 74 75 76 77 78 79 80 81]
    [72 73 74 75 76 77 78 79 80 81 82]
    [73 74 75 76 77 78 79 80 81 82 83]
    [74 75 76 77 78 79 80 81 82 83 84]
    [75 76 77 78 79 80 81 82 83 84 85]
    [76 77 78 79 80 81 82 83 84 85 86]
    [77 78 79 80 81 82 83 84 85 86 87]
    [78 79 80 81 82 83 84 85 86 87 88]
    [79 80 81 82 83 84 85 86 87 88 89]
    [80 81 82 83 84 85 86 87 88 89 90]
    [81 82 83 84 85 86 87 88 89 90 91]
    [82 83 84 85 86 87 88 89 90 91 92]
    [83 84 85 86 87 88 89 90 91 92 93]
    [84 85 86 87 88 89 90 91 92 93 94]
    [85 86 87 88 89 90 91 92 93 94 95]
    [86 87 88 89 90 91 92 93 94 95 96]
    [87 88 89 90 91 92 93 94 95 96 97]
    [88 89 90 91 92 93 94 95 96 97 98]
    [89 90 91 92 93 94 95 96 97 98 99]
    [90 91 92 93 94 95 96 97 98 99 100]
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    
    다음에 기억이 나면 팩스로 할게요.
    가능한 한 함수를 총결하다.
    tail
    func tail(stream *os.File, n int) []string {
    	queue := []string{}
    	scanner := bufio.NewScanner(stream)
    	for scanner.Scan() {
    		queue = append(queue, scanner.Text())
    		if n <= len(queue)-1 {
    			queue = queue[1:]
    		}
    	}
    	return queue
    }
    
    func show(queues []string) {
    	for _, queue := range queues {
    		fmt.Println(queue)
    	}
    }
    

    매개변수, flag 설정


    main에 파일 읽기, 표준 입력 읽기, 옵션 표시를 지우는 처리를 씁니다.
    여러 개의 파일을 읽어야 하기 때문에 그 처리도 써야 한다.
    main
    func main() {
    	const USAGE string = "Usage: gotail [-n #] [file]"
    	intOpt := flag.Int("n", 10, USAGE)
    	flag.Usage = func() {
    		fmt.Println(USAGE)
    	}
    	flag.Parse()
    	n := int(math.Abs(float64(*intOpt)))
    	if flag.NArg() > 0 {
    		for i := 0; i < flag.NArg(); i++ {
    			if i > 0 {
    				fmt.Print("\n")
    			}
    			if flag.NArg() != 1 {
    				fmt.Println("==> " + flag.Arg(i) + " <==")
    			}
    			fp, err := os.Open(flag.Arg(i))
    			if err != nil {
    				fmt.Println("Error: No such file or directory")
    				os.Exit(1)
    			}
    			defer fp.Close()
    			show(tail(fp, n))
    		}
    	} else {
    		show(tail(os.Stdin, n))
    	}
    }
    

    단원 테스트


    이어서 시험을 쓴다.
    여기에 미리 준비한 test.txt을 읽어 검증한다.
    또한 파일에 대해 고려할 수 있는 옵션을 테스트할 수 있습니다.
    이번test.txt은 100줄의 번호 숫자로 -n 1 ~ -n 100까지 순서대로 테스트했다.
    testTail
    func TestTail(t *testing.T) {
    	var actual_all []string
    	var expected_all []string
    	count := 0
    	fp, err := os.Open("./test.txt")
    	if err != nil {
    		fmt.Println("Error: No such file or directory")
    		os.Exit(1)
    	}
    	defer fp.Close()
    	scanner := bufio.NewScanner(fp)
    	for scanner.Scan() {
    		count++
    	}
    	n_all := 1
    	for i := count; i > 0; i-- {
    		fp, err = os.Open("./test.txt")
    		actual_all = tail(fp, n_all)
    		expected_all = append([]string{strconv.Itoa(i)}, expected_all...)
    		if reflect.DeepEqual(actual_all, expected_all) {
    			t.Log(reflect.DeepEqual(actual_all, expected_all))
    		} else {
    			t.Errorf("got %v\nwant %v", actual_all, expected_all)
    		}
    		n_all++
    	}
    }
    

    테스트 덮어쓰기 확인


    또한 테스트 덮어쓰기를 확인합니다.
    다음 명령을 통해 확인할 수 있습니다.
    사용자가 사용할 수 있는 자원이 제한될 수 있습니다$ ulimit -a를 통해 확인합니다.필요에 따라 집행$ ulimit -n 500 등.
    testcover
    $ go test main_test.go main.go -coverprofile=cover.out
    $ go tool cover -html=cover.out -o cover.html
    $ open cover.html
    
    이런 식으로 확인할 수 있다.

    80퍼센트 정도 넘었기 때문에 계속 전진했다.
    docker를 시작하여build을 실행합니다.
    방금 Docker file의 설정에 따라test의 구현도 진행됩니다.
    cmd.sh에서 Stage2에서 실행하고자 하는 명령을 기술합니다.
    cmd.sh
    #!/bin/sh -eux
    ./main < test.txt
    
    기술을 완성한 후 컨테이너를 가동하다.
    하나하나 지령을 내리는 것은 번거롭기 때문에 Makefile로 지령을 간소화합니다.
    Makefile
    NAME := gotail
    
    .PHONY: all
    all: docker-build docker-run
    
    .PHONY: docker-build
    docker-build:
    	docker build -t $(NAME) .
    
    .PHONY: docker-run
    docker-run:
    	docker run --rm $(NAME)
    
    $ make
    
    이렇게 되면 일련의 집행을 확인하면 OK.

    GiitHub Action 기반 CI/CD(Go, Docker)


    GiitHub Actions에서는 GiitHub에서 테스트를 뛸 수도 있고 build를 할 수도 있어 매우 편리하다.
    이번에는 고 단일 검증 동작과 Docker를 시작하는 동작을 시도해 봅시다.
    먼저 다음 디렉토리 구조로 미리 설정해야 합니다.
     gotail
     └─.github
        └── workflows
            └── go.yml
    
    Action의 설정은 YAML 파일에 기술되어 있습니다.
    우선 고.yml를 기술하세요.
    go.yml
    on:
      push:
        branches: [ master ]
      pull_request:
        branches: [ master ]
    
    jobs:
    
      build:
        runs-on: ubuntu-latest
        steps:
        - uses: actions/checkout@v2
    
        - name: Set up Go
          uses: actions/setup-go@v2
          with:
            go-version: 1.15
    
        - name: Build
          run: go build -v ./main.go
    
        - name: Test
          run: go test -v ./main_test.go main.go
    
    대상의 지점에push와pullrequest를 실행할 때 Action이 실행되도록 설정합니다.
    또한build와test를 실행할 수 있습니다.
    다음에도 docker를 시작해 봅시다.
     gotail
     └─.github
        └── workflows
            └── docker.yml
    
    docker.yml
    name: CI to Docker Hub
    on:
      push:
        branches: [ master ]
      pull_request:
        branches: [ master ]
    
    jobs:
        build:
            runs-on: ubuntu-latest
            steps:
                - name: Check Out Repo
                  uses: actions/checkout@v2
                - name: Login to Docker Hub
                  uses: docker/login-action@v1
                  with:
                      username: ${{ secrets.DOCKER_HUB_USERNAME }}
                      password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
                - name: Set up Docker Buildx
                  id: buildx
                  uses: docker/setup-buildx-action@v1
                - name: Build and push
                  id: docker_build
                  uses: docker/build-push-action@v2
                  with:
                        context: ./
                        file: ./Dockerfile
                        push: true
                        tags: ${{ secrets.DOCKER_HUB_USERNAME }}/simplewhale:latest
                - name: Image digest
                  run: echo ${{ steps.docker_build.outputs.digest }}
    
                - name: Run
                  run: make
    
    이 시크릿은 Giithub과 Docker hub에서 설정해야 합니다.
    먼저 Docker Hub에 액세스합니다.
    https://hub.docker.com/
    로그인이 완료되면 오른쪽 상단에 있는 아이콘을 누르고 메뉴를 표시하고 아래 항목을 누르십시오.

    이어 Security→New Access Token 순으로 클릭한다.

    Token을 표시하려면 title 등을 적당히 입력하고 창을 복사하고 닫으십시오.

    이어서 Giithub을 방문합니다.
    https://github.com/
    Setting→Secret→New repository secret을 클릭하여 방금 Token과 Docker Hub의 사용자 이름을 설정합니다.



    이상 설정이 완료되었습니다.
    마지막으로 창고에서push나pullrequest를 진행할 때 Action을 자동으로 실행합니다.
    이번 코드 등은 이 글을 쓰는 단계에서는 아직 마스터에 통합되지 않았지만, 지허브도 올라오기 때문에 참고가 될 수 있어 미리 링크를 붙일 수 있다.
    https://github.com/Iovesophy/gotail

    총결산


    수고하셨습니다. 여기까지 읽어주셔서 감사합니다!
    Go 언어는 처음 사용해 Go 언어의 장점으로 자주 열거되는데, 초보자라도 쉽게 이해할 수 있고 처리 속도가 빠르고 코드가 적으며 라이브러리가 풍부해 병행 처리 가능성이 높고 안전성이 높다는 느낌이 확실히 든다.
    다음은 간단한 응용 프로그램 제작과 병행 처리를 시도해 보고 싶습니다.

    좋은 웹페이지 즐겨찾기