728x90

 

 

echo 를 시용해서 서버를 만드는 것을 super easy 하다.

 

main.go 파일을 다음과 같이 수정하자.

 

package main

import (
	"net/http"

	"github.com/labstack/echo"
)

func main() {
	e := echo.New()
	e.GET("/", handleHome)
	e.Logger.Fatal(e.Start(":1323"))
}

func handleHome(c echo.Context) error {
	return c.String(http.StatusOK, "Hello, World!")
}

 

그리고 실행하면 다음과 같이 실행 결과가 출력 되고, 1323 포트를 통해 접속할 수 있다.

 

 

 

정상적으로 실행 되는것을 확인하긴 했지만, myScraper 를 전혀 사용하고 있지 못하고 있다.

그래서 문자열을 출력하는 대신, 화면을 출력할 수 있도록 html 파일을 하나 만들어 보자.

 

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Go Jobs</title>
</head>
<body>
    <h1>Go Jobs</h1>
    <h3>Indeed.com scraper</h3>

    <form>
        <input placeholder="what job do u want" />
        <button>Search</button>
    </form>
</body>
</html>

 

그리고 이 파일을 사용할 수 있도록 main.go 파일도 다음과 같이 수정한다.

 

package main

import (
	"github.com/labstack/echo"
)

func main() {
	e := echo.New()
	e.GET("/", handleHome)
	e.Logger.Fatal(e.Start(":1323"))
}

func handleHome(c echo.Context) error {
	return c.File("home.html")
}

 

적용되는 것을 확인해보기 위해 서버를 재시작 하고 다시 접속해보자.

 

 

그럼 마지막으로 form 의 method 와 action 을 정의하고, main.go 파일에서 echo 의 핸들러를 등록한 다음에 myScrap 패키지의 MyScrap 을 input 으로 받은 값을 넘겨주고 실행하도록 해보자.

 

그 전에 myScrap.go 파일의 cleanString function 을 외부에서도 사용할 수 있도록 CleanString function 으로 이름을 변경해준다.

이렇게 하면 public 접근을 할 수 있게 되어 외부에서도 호출해서 사용할 수 있게 된다.

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Go Jobs</title>
</head>
<body>
    <h1>Go Jobs</h1>
    <h3>Indeed.com scraper</h3>

    <form method="POST" action="/myScrap">
        <input placeholder="what job do u want" name="term"/>
        <button>Search</button>
    </form>
</body>
</html>

 

form 태그에 전송 방식을 POST 로 하고 action 을 /myScrap 으로 입력한다.

그리고 input 태그의 name 속성을 지정하여 핸들러에서 이 값을 사용할 수 있도록 한다.

 

package main

import (
	"learngo/part4_job_scrapper/part4_1_getPages_part_one/myScrap"
	"os"
	"strings"

	"github.com/labstack/echo"
)

const FILE_NAME string = "jobs.csv"

func main() {
	e := echo.New()
	e.GET("/", handleHome)
	e.POST("/myScrap", handleScrap)
	e.Logger.Fatal(e.Start(":1323"))
}

func handleHome(c echo.Context) error {
	return c.File("home.html")
}

func handleScrap(c echo.Context) error {

	// 실행 이후 서버에서 파일을 삭제한다.
	defer os.Remove(FILE_NAME)

	term := strings.ToLower(myScrap.CleanString(c.FormValue("term")))
	myScrap.MyScrap(term)
	return c.Attachment(FILE_NAME, FILE_NAME)
}

 

handleScrap function 을 정의하고 16라인에서 "/myScrap" POST 요청을 처리 할 핸들러로 등록 한다.

29라인 에서는 myScrap 에서 public 접근을 하도록 수정한 CleanString 을 사용하고, form 태그의 name 속성의 값을 가져와 MyScrap 을 실행할 때 전달하는 매개변수를 처리한다.

 

31라인의 c.Attachment 는 파일을 다운받을 수 있도록 한다.

 

여기까지 수정 했으면 서버를 재시작하고 브라우저에서 실행해보자.

 

 

정상적으로 실행되는 것을 확인할 수 있다.

 

 

echo 에 대한 더욱 다양한 사용 방법에 대한 document 링크이다.

필요한 기능이 있으면 쉽게 찾아서 사용해볼 수 있을것 같다.

 

 

 

 

728x90
728x90

 

 

이번에는 go echo 서버를 만들어 본다.

본격적으로 만들어보기에 앞서 필요한 사전 작업들을 해보자.

 

우선 main package 에 있던 main function 을 다른 package 로 만들고, 실행이 아닌 호출 가능한 형태로 변경하려 한다.

 

 

우선 myScrap 라는 이름의 폴더를 만들고 그 안에 main.go 파일을 옮긴다.

그리고 main.go 파일의 이름을 myScrap.go 로 이름을 변경한다.

 

myScrap.go 가 된 파일을 열고

1. package 이름 변경

2. main function 이름을 myScrap function 으로 변경

3. 전역변수 baseURL 을 myScrap function 내부로 이동

4. myScrap function 에 string 타입의 변수 term 을 받아서 검색어로 사용할 수 있도록 baseURL 을 수정

한다.

 

 

전역변수를 지역변수로 옮긴 다음에는 다른 function 에서 사용할 수 있도록 매개변수로 추가해 준다.

 

 

 

그리고 myScrap 을 사용하는 main.go 파일을 만들어 보자.

 

 

새로 작성한 main.go 는 다음과 같다.

 

package main

import "learngo/part4_job_scrapper/part4_1_getPages_part_one/myScrap"

func main() {
	myScrap.MyScrap("term")
}

 

 

실행 결과 여전히 잘 동작하는 것을 확인할 수 있다.

 

마지막으로 echo 패키지를 설치만 해보자.

go get github.com/labstack/echo 명령어로 패키지를 설치 한다.

 

 

 

사용은 다음시간에 해볼 수 있을것 같다.

 

 

 

 

728x90
728x90

 

 

사실 이 전에 작성한 코드로 URL Checker 구현이 끝났지만 출력하는 부분이 없었다.

앞서 다른 예제에서 goroutine 으로 실행한 결과를 channel 을 통해 수신하도록 '<- c' (c 는 channel 변수
) 를 적절하게 작성해 결과를 받아 처리하면 된다.

 

channel 을 통해 받은 모든 결과에 대해 'result = <-c' 를 생성된 goroutine 에 맞게 작성해도 동작 하지만, for 문을 사용해서 간결하게 작성하는게 더 좋다.

 

package main

import (
	"errors"
	"fmt"
	"net/http"
)

// channel 을 통해 주고 받을 데이터 타입으로 사용 할 struct 선언
type requestResult struct {
	url    string
	status string
}

// 사용자 정의 error
var errRequestFailed = errors.New("Request failed")

func main() {

	// url 접속 결과를 담을 비어있는 map 선언
	results := make(map[string]string)

	// channel 생성
	c := make(chan requestResult)

	// 접속을 시도 할 url 목록
	urls := []string{
		"https://www.airbnb.com/",
		"https://www.google.com/",
		"https://www.amazon.com/",
		"https://www.reddit.com/",
		"https://www.google.com/",
		"https://soundcloud.com/",
		"https://www.facebook.com/",
		"https://www.instagram.com/",
		"https://academy.nomadcoders.co/",
		"https://xxxelppa.tistory.com/",
		"https://nimkoes.github.io/",
	}

	// 반복문을 사용하여 각 url 에 접속 시도
	for _, url := range urls {
		go hitURL(url, c)
	}

	// 결과를 map 에 담음

	for i := 0; i < len(urls); i++ {
		result := <-c
		results[result.url] = result.status
	}

	// map 에 담긴 결과 출력
	for url, status := range results {
		fmt.Println(url, status)
	}
}

func hitURL(url string, c chan<- requestResult) {

	// Go reference 참고하여 url 에 Get 요청
	resp, err := http.Get(url)

	// requestResult struct 의 status 값으로 사용 할 변수 선언
	status := "Ok"

	// err 가 있거나 http 응답 코드가 400 과 같거나 큰 경우 예외 처리
	if err != nil || resp.StatusCode >= 400 {
		status = "FAILED"
	}

	c <- requestResult{url: url, status: status}
}

 

URL Checker 의 완성된 코드이다.

수정한 것은 '<- c' 를 통해 받은 응답 데이터를 map 에 담고, 반복문을 사용해서 map 의 데이터를 보기 좋게 출력했을 뿐이다.

 

실행해보면 정말 빠르다는것을 확인할 수 있다.

 

 

goroutine 을 사용하기 전에는 url 하나씩 순차적으로 확인했던것과 비교했을 때와 비교해보면 엄청나게 빨라진걸 알 수 있다.

체크하는 URL 이 각각 1초, 2초, 3초가 걸린다고 하면, goroutine 을 사용하기 전에는 6초가 걸렸겠지만 지금은 3초에 모든 작업이 끝난다.

 

작업의 선후관계가 명확한 프로그램이라면 또 고민을 해봐야겠지만, 지금 처럼 서로 독립적인 작업에 대해 쉽고 빠르게 처리할 수 있다.

 

 

 

 

728x90
728x90

 

 

지금까지 goroutine 에 대해 알아보았으니 'main.go' 파일에 작성했던 URL Checker 를 goroutine 을 사용해서 개선해보자.

 

package main

import (
	"errors"
	"fmt"
	"net/http"
)

// channel 을 통해 주고 받을 데이터 타입으로 사용 할 struct 선언
type result struct {
	url    string
	status string
}

// 사용자 정의 error
var errRequestFailed = errors.New("Request failed")

func main() {

	// url 접속 결과를 담을 비어있는 map 선언
	results := make(map[string]string)

	// channel 생성
	c := make(chan result)

	// 접속을 시도 할 url 목록
	urls := []string{
		"https://www.airbnb.com/",
		"https://www.google.com/",
		"https://www.amazon.com/",
		"https://www.reddit.com/",
		"https://www.google.com/",
		"https://soundcloud.com/",
		"https://www.facebook.com/",
		"https://www.instagram.com/",
		"https://academy.nomadcoders.co/",
		"https://xxxelppa.tistory.com/",
		"https://nimkoes.github.io/",
	}

	// 반복문을 사용하여 각 url 에 접속 시도
	for _, url := range urls {
		go hitURL(url, c)
	}

	// 실행 결과 출력
	for url, result := range results {
		fmt.Println(url, result)
	}
}

func hitURL(url string, c chan<- result) {

	// 현재 request 시도 하는 url 출력
	fmt.Println("Checking:", url)

	// Go reference 참고하여 url 에 Get 요청
	resp, err := http.Get(url)

	// result struct 의 status 값으로 사용 할 변수 선언
	status := "Ok"

	// err 가 있거나 http 응답 코드가 400 과 같거나 큰 경우 예외 처리
	if err != nil || resp.StatusCode >= 400 {
		status = "FAILED"
	}

	c <- result{url: url, status: status}
}

 

기존 'main.go' 와 다른 부분은 다음과 같다.

  1. channel 을 사용해서 결과를 주고 받음
  2. hitURL function 에서 channel 을 받을 때 'chan<-' 을 사용해서 function 내에서 channel 을 통할 때 방향을 지정. chan 을 중심으로 화살표가 향하고 있으므로 channel 에 데이터를 넣는 것만 하도록 강제했다. 만약 function 내에서 channel 의 데이터를 수신하는것만 하도록 강제 하려면 '<-chan' 으로 작성할 수 있다.

 

이 프로그램을 실행하면 다음과 같이 아무 결과를 출력하지 않는다.

 

 

만약 지금까지 내용을 이해했다면 이렇게 출력되는게 당연하다.

왜냐하면 channel 을 사용해서 hitURL function 을 사용한 main function 의 어느 곳에서도 '<-c' 와 같이 channel 의 데이터를 기다리는 곳이 아무데도 없기 때문이다.

 

그래서 main 스레드는 goroutine 으로 생성한 별도의 스레드들을 시작했지만 그 실행 결과를 기다리지 않고 바로 종료해버린 것이다.

 

main function 에서 channel 의 데이터를 기다리는 부분은 다음 포스팅에서 정리한다.

이번 예제에서 기억할 만한 것은, channel 을 사용할 때 function 에서 chan 의 데이터 송수신 방향을 정해놓고 쓸 수 있다는 것이다.

 

 

 

 

728x90
728x90

 

 

앞서 goroutine 과 관련하여 기억해야 할 것을 정리하면

  1. '<-c' (예제에서 c 라고 하였기 때문에 c 라고 표기, c 는 사용자 정의 임의의 값) 는 blocking operation 이다.
  2. main function 이 종료 되면 이후 goroutine 은 무의미해 진다.
  3. channel 을 생성할 때, channel 을 통해 어떤 데이터를 주고 받을지 반드시 명시 해야 한다.
  4. channel 을 통해 메시지를 전달할 때는 화살표(arrow) 연산자를 사용하고, channel 을 향하게 한다. (ex, c <- data)
  5. '<-c' 를 통해 데이터를 전달 받을 때, 반드시 전송하는 수와 일치할 필요는 없지만 더 많이 작성하면 안된다. 더 적게 작성했을 경우 늦게 처리된 데이터는 갈 곳을 잃고 무시 된다.

 

 

 

 

728x90
728x90

 

 

이번에는 Channel 로 주고 받는 데이터를 bool 타입이 아닌 string 타입을 사용해보자.

아래는 수정한 main_goroutine.go 파일이다.

 

package main

import (
	"fmt"
	"time"
)

func main() {

	// 길이 2 의 문자열 배열 생성
	people := [5]string{"nico", "nimkoes", "go", "java", "spring"}

	// bool 타입을 주고받을 수 있는 channel 생성, c 는 임의의 이름으로 사용 가능
	c := make(chan string)

	// 반복문을 실행 하면서 두 개의 goroutine 을 실행
	for _, person := range people {
		// channel 을 같이 전달
		go isSexy(person, c)
	}

	fmt.Println("waiting... ")

	// main function 은 channel 로부터 받는 값을 기다린다.
	// 기다린다는 것은 스레드를 종료하지 않는다는 것을 의미한다.
	fmt.Println(<-c)
	fmt.Println(<-c)
	fmt.Println(<-c)
	fmt.Println(<-c)
	fmt.Println(<-c)

	fmt.Println("DONE !")
}

func isSexy(person string, c chan string) {
	// 5초 동안 스레드를 멈춘다.
	time.Sleep(time.Second * 3)

	// channel 로 bool 값을 전달한다.
	c <- person + " is sexy"
}

 

이전 예제와 달라진 부분은 people 배열의 요소 수를 5로 늘렸고

channel 을 통해 주고 받을 데이터의 타입을 string 으로 바꾼 것과

channel 이 응답 받을 데이터를 기다리는 것을 확인하기 위해 (blocking 하는 것을 확인하기 위해) '<- c' 작업 전 후로 문자열을 출력한 것이다.

마지막으로 'isSexy' function 의 sleep 5초가 너무 긴 것 같아서 3초로 줄였다.

 

실행하면 다음과 같다.

 

 

프로그램을 실행하면 'waiting...' 문자열이 출력 되고, 설정한 3초 동안 다른 스레드에서 channel 을 통해 전달하는 결과를 '<- c' 에서 기다린다.

 

blocking 관련해서 개선하는 부분에 대해서 다음에 다룰것 같다.

 

우선 의도한대로 잘 동작하는 코드를 작성하긴 했는데, 'fmt.Println(<-c)' 코드가 배열 길이가 길어질수록 반복해서 나타나고 있다.

반복해서 나타나는 중복 코드는 나쁜 냄새가 나는 코드다.

그래서 이걸 반복문을 사용해서 다음과 같이 개선할 수 있다.

 

for i := 0; i < len(people); i++ {
	fmt.Println(<-c)
}

 

이제부터는 people 배열의 길이에 상관 없이 channel 을 통해 받는 값을 처리할 수 있게 되었다.

 

실행 결과는 동일하기 때문에 최종 수정한 코드만 첨부 한다.

 

package main

import (
	"fmt"
	"time"
)

func main() {

	// 길이 2 의 문자열 배열 생성
	people := [5]string{"nico", "nimkoes", "go", "java", "spring"}

	// bool 타입을 주고받을 수 있는 channel 생성, c 는 임의의 이름으로 사용 가능
	c := make(chan string)

	// 반복문을 실행 하면서 두 개의 goroutine 을 실행
	for _, person := range people {
		// channel 을 같이 전달
		go isSexy(person, c)
	}

	fmt.Println("waiting... ")

	for i := 0; i < len(people); i++ {
		fmt.Println(<-c)
	}

	fmt.Println("DONE !")
}

func isSexy(person string, c chan string) {
	// 5초 동안 스레드를 멈춘다.
	time.Sleep(time.Second * 3)

	// channel 로 bool 값을 전달한다.
	c <- person + " is sexy"
}

 

 

 

 

728x90
728x90

 

 

Channel 은 goroutine 과 main function 사이 또는 goroutine 간 정보 전달을 하기 위한 방법 이다.

앞서 make 가 map 을 만들기 위한 function 이라고 정리 했었는데, make function 은 slice, map 그리고 channel 을 만들 수 있는 function 이 맞는것 같다.

 

 

아무튼, 이번에는 channel 이라는 것을 사용해보려 한다.

코드에 대한 설명은 주석에 작성 하였다.

 

package main

import (
	"fmt"
	"time"
)

func main() {

	// 길이 2 의 문자열 배열 생성
	people := [2]string{"nico", "nimkoes"}

	// bool 타입을 주고받을 수 있는 channel 생성, c 는 임의의 이름으로 사용 가능
	c := make(chan bool)

	// 반복문을 실행 하면서 두 개의 goroutine 을 실행
	for _, person := range people {
		// channel 을 같이 전달
		go isSexy(person, c)
	}

	// main function 은 channel 로부터 받는 값을 기다린다.
	// 기다린다는 것은 스레드를 종료하지 않는다는 것을 의미한다.
	fmt.Println(<-c)
	fmt.Println(<-c)
}

func isSexy(person string, c chan bool) {
	// 5초 동안 스레드를 멈춘다.
	time.Sleep(time.Second * 5)
	fmt.Println(person)

	// channel 로 bool 값을 전달한다.
	c <- true
}

 

 

실행 결과를 보면 main function 이 goroutine 을 실행하고 바로 종료하지 않고, channel 을 통해 값을 전달 받기를 기다리는 것을 확인할 수 있다.

 

추가로 확인할 수 있는건, person 과 bool 결과가 쌍을 이루지 않고 person 을 모두 출력한 다음 bool 결과를 출력 하였는데, 그 이유는 각 스레드가 병렬 처리 되고 있기 때문이다.

 

여러번 실행 하다보면 다음과 같이 person 다음에 bool 값을 출력하는 것도 볼 수 있다.

 

 

 

 

 

728x90
728x90

 

 

Go 에는 멀티스레드를 생성해서 실행하는게 정말 간단한 것 같다.

예를 들어 Java 같은 경우 Runnable 인터페이스를 구현 한다던가 하는 작업이 필요하고, 그렇게 하려다보면 장황한 코드가 작성 되기도 한다. 물론 람다식을 쓰면 좀 나아지겠지만 말이다.

 

다른 실행 프로그램과 구분하기 위해 main_goroutine.go 파일을 새롭게 작성 했다.

 

그리고 1초 간격으로 문자열을 출력하는 반복문을 가진 sexyCount 라는 function 을 작성했다.

 

package main

import (
	"fmt"
	"time"
)

func main() {
	sexyCount("nico")
	sexyCount("nimkoes")
}

func sexyCount(person string) {
	for i := 0; i < 5; i++ {
		fmt.Println(person, "is sexy", i)
		time.Sleep(time.Second)
	}
}

 

main function 에서 sexyCount function 을 두 번 호출 해서 사용하고 있는데, 실행하면 다음과 같이 실행 된다.

 

 

각 function 을 실행하는데 5초씩, 총 10초가 필요하다.

만약 이 두 function 을 동시에 실행할 수 있다면 다음과 같이 5초가 걸릴 것이다.

 

 

이렇게 실행 시간을 반으로 줄이는데 공백을 포함해서 문자 3개만 추가했을 뿐이다.

 

func main() {
	go sexyCount("nico")
	sexyCount("nimkoes")
}

 

main function 에서 "nico" 문자열을 매개변수로 전달하는 sexyCount 호출할 때 'go ' 를 추가햇을 뿐이다.

이렇게 하면 Go 가 알아서 별도의 스레드를 생성해서 동시에 처리 한다고 한다.

 

그럼 왜 두 번 호출 하는데 한쪽에만 'go ' 를 붙였을까?

둘 다 붙이고 실행하면 다음과 같이 실행 된다.

 

func main() {
	go sexyCount("nico")
	go sexyCount("nimkoes")
}

 

 

다른 언어에 경험이 있다면 이런 결과가 나오는걸 당연하게 생각할 수 있다.

main function 안에서 호출한 각 sexyCount function 에 대해 스레드를 생성해서 실행하고난 다음 main function 의 스레드는 더 이상 할게 없기 때문에 끝나버렸기 때문이다.

그래서 처음 main function 안에서 sexyCount function 을 호출할 때 한쪽에만 'go ' 를 붙여서 붙이지 않은 function 이 main function 이 실행중인 스레드에서 동작하도록 한 것이다.

 

하지만 이렇게 'go ' 를 포함하지 않는 function 을 하나 남기는 방법으로 처리 하기에는 리스크가 있다.

아직 진도가 거기까지 나가지 못했지만, 다른 언어에서와 마찬가지로 안전하게 처리할 수 있는 장치가 있을거라 생각한다.

 

 

 

 

728x90

+ Recent posts