사람인에 있는 공고들을 크롤링 해오려고 한다.

 

jobscrapper라는 폴더를 하나 만들고 main.go 파일을 하나 만들었다.

 

Add baseURL

var baseURL string = "https://www.saramin.co.kr/zf_user/search/recruit?&searchword=python"

Add getPages()

func main() {
	getPages()
}

func getPages() int {
	res, err := http.Get(baseURL)
	checkErr(err)
	checkStatusCode(res)

	return 0
}

func checkErr(err error) {
	if err != nil {
		log.Fatalln(err)
	}
}

func checkStatusCode(res *http.Response) {
	if res.StatusCode != 200 {
		log.Fatalln("Request failed with Status:", res.StatusCode, res.Status)
	}
}

처음에 indeed에서 크롤링을 막아놓아서 그런지 http.Get(url) 방법으로는 403 에러가 발생했다. User-Agent와 프록시로 해봤지만, 해결되지 않았고 사람인 페이지로 대체했다.

ADD>
이후에 찾아보니 indeed에서 크롤링하는 것을 indeed 측에서 막았다. http://www.indeed.com/robots.txt를 확인해보면, Disallow했다. robots.txt를 알고싶은 사람은 링크를 통해 글을 읽어보면 좋겠다.

 

go query라는 별도의 라이브러리를 추가해주자. 참고로 위의 error 처리는 go query에 적혀있는 방식을 보고 따라했다.

터미널에서 아래 명령어를 실행해주자.

go get github.com/PuerkitoBio/goquery

Add go query

func getPages() int {
	...

	defer res.Body.Close()

	doc, err := goquery.NewDocumentFromReader(res.Body)
	checkErr(err)
    
	fmt.Println(doc)
	...
}

출력

별도의 문제없이 성공이 되고 응답이 오는 모습이다.

참고!

checkErr()나 checkStatusCode()라는 함수를 통해 에러를 처리했다. 이와같은 방식은 함수 명을 통하여 의미를 부여할 수 있지만 코드가 길어지는 경우 어떻게 에러를 처리하는지 확인하고 싶을 때, depth가 한번 더 들어가있기 때문에 가독성이 떨어질 수 있다. 상황에 따라서 똑똑하게 사용하자!

 

go query는 github에서 잘 읽어보면 사용방법이 나와있다.

  // Find the review items
  doc.Find(".left-content article .post-title").Each(func(i int, s *goquery.Selection) {
		// For each item found, get the title
		title := s.Find("a").Text()
		fmt.Printf("Review %d: %s\n", i, title)
	})

div class 이름을 Find 안에 적어주고 적혀있는 내용을 가져올 수 있다.

페이지의 개수를 알고싶기 때문에 페이지를 나타내는 div의 class네임인 pagination을 개발자 도구탭에서 찾아냈다.

	doc, err := goquery.NewDocumentFromReader(res.Body)
	checkErr(err)

	doc.Find(".pagination").Each(func(i int, s *goquery.Selection) {
		fmt.Println(s.Html())
	})

출력

이렇게 안에 적힌 페이지의 개수를 가져올 수 있다.

a href에 적힌 #recruit_info는 현재 페이지를 나타내는 듯 하다. 

	doc.Find(".pagination").Each(func(i int, s *goquery.Selection) {
		fmt.Println(s.Find("a").Length())
	})

a의 개수를 출력해 본 결과 10개로 정확하게 나온다.

https://www.saramin.co.kr/zf_user/search/recruit?=&searchword=python&recruitPage=2&recruitPageCount=50

크롤링 하는 방법은 아래와 같다.

  • 원하는 검색 키워드를 searchword에 넣어준다.
  • 원하는 페이지 번호를 recruitPage에 넣어준다. (공고를 가져오기 위함)
  • 원하는 페이지 공고 개수를 recruitCount에 넣어준다.

일단, getPages는 페이지 개수를 구하기 위한 함수였기 때문에 아래와 같이 수정해준다.

func getPages() int {
	pages := 0
	res, err := http.Get(baseURL)
	checkErr(err)
	checkStatusCode(res)

	defer res.Body.Close()

	doc, err := goquery.NewDocumentFromReader(res.Body)
	checkErr(err)

	doc.Find(".pagination").Each(func(i int, s *goquery.Selection) {
		pages = s.Find("a").Length()
	})

	return pages
}

페이지들의 개수를 가져오싸으니, 각각 페이지의 공고들을 가져와 보자.

func main() {
	total := getPages()
	fmt.Println(total)

	for i := 1; i <= total; i++ {
		getPage(i)
	}
}

main에서 페이지의 개수들을 getPages()를 통해 가져오고 각각 페이지를 반복문을 통해서 가져온다. 페이지 숫자는 1~total까지 있기때문에 1부터 tatal까지 반복문을 돌려준 모습이다.

func getPage(page int) {
	pageURL := baseURL + "&recruitPage=" + strconv.Itoa(page)
	fmt.Println("Requesting :", pageURL)
	res, err := http.Get(pageURL)
	checkErr(err)
	checkStatusCode(res)
	
}
https://www.saramin.co.kr/zf_user/search/recruit?&searchword=python&recruitCount=50&recruitPage=10

페이지를 넘기는 방법은 위처럼 recruitPage를 사용하여 페이지 번호를 넘길 수 있다. 기본 URL에 recruitPage 번호를 통해 각각의 모든 공고들을 페이지 번호를 넘겨가며 크롤링할 수 있다.

 

각 공고 div

이제 페이지별로 각 공고들을 크롤링해오기 위해 공고가 해당되는 div class 이름을 가져오자. 사람인의 경우 item_recruit으로 되어있다.

 

func getPage(page int) {
	...
    
	defer res.Body.Close()

	doc, err := goquery.NewDocumentFromReader(res.Body)
	checkErr(err)

	doc.Find(".item_recruit").Each(func(i int, s *goquery.Selection) {
		fmt.Println(s.Html())
	})
}

getPage 함수에 goquery를 통해 item_recruit에 해당하는 데이터들을 가져오자.

출력

아주 많은 html 코드들이 출력된다. (잘 되고 있군)

 

	doc.Find(".item_recruit").Each(func(i int, s *goquery.Selection) {
		id, _ := s.Attr("value")
		title := s.Find(".job_tit>a").Text()
		condition := s.Find(".job_condition").Text()
		fmt.Println(id, title, condition)
	})

출력

필요한 정보들만 찾아서 출력해보면 위와 같다. 

 

이제 가져올 수 있는 필요한 정보들을 묶어 struct로 만들자.

type extractedJob struct {
	id string
	title string
	location string
	summary string
	company string
}

그 후, extractJob 함수를 추가해서 struct에 넣어주자.

func getPage(page int) {
	...

	doc, err := goquery.NewDocumentFromReader(res.Body)
	checkErr(err)

	doc.Find(".item_recruit").Each(func(i int, card *goquery.Selection) {
		extractJob(card)
	})
}
func claanString(str string) string {
	return strings.Join(strings.Fields(strings.TrimSpace(str)), "")
}

func extractJob(card *goquery.Selection ) {
	id, _ := card.Attr("value")
	title := claanString(card.Find(".job_tit>a").Text())
	location := claanString(card.Find(".job_condition>span>a").Text())
	summary := claanString(card.Find(".job_sector").Clone().ChildrenFiltered(".job_day").Remove().End().Text())
	company := claanString(card.Find(".area_corp>strong>a").Text())
	fmt.Println(id, title, location, summary, company)
}

출력

잘 가져오는 모습이다.

func extractJob(card *goquery.Selection ) extractedJob {
	id, _ := card.Attr("value")
	title := claanString(card.Find(".job_tit>a").Text())
	location := claanString(card.Find(".job_condition>span>a").Text())
	summary := claanString(card.Find(".job_sector").Clone().ChildrenFiltered(".job_day").Remove().End().Text())
	company := claanString(card.Find(".area_corp>strong>a").Text())
	return extractedJob {
		id : id,
		title : title,
		location : location,
		summary : summary,
		company : company,
	}
}

extractJob을 return 해주도록 수정하자.

func getPage(page int) []extractedJob{
	...

	var jobs []extractedJob
	doc.Find(".item_recruit").Each(func(i int, card *goquery.Selection) {
		job := extractJob(card)
		jobs = append(jobs, job)
	})
	return jobs
}

getPage도 수정해주자.

func main() {
	...
	
	var jobs []extractedJob
	for i := 1; i <= total; i++ {
		extractedJobs := getPage(i)
		jobs = append(jobs, extractedJobs...)
	}
}

main도 수정해주자.

출력

총 10페이지 모두 가져오는 것을 볼 수 있다.

 

이제 가져온 데이터를 csv파일에 쓸 것이다. 아래 Go Package에서 지원하는 csv package를 사용할 것이다.

 

csv package - encoding/csv - Go Packages

Discover Packages Standard library encoding csv Version: go1.21.6 Opens a new window with list of versions in this module. Published: Jan 9, 2024 License: BSD-3-Clause Opens a new window with license information. Imports: 8 Opens a new window with list of

pkg.go.dev

사용 예시나 설명은 해당 링크에 자세하게 나와있다. 

 

func writeJobs(jobs []extractedJob) {
	file, err := os.Create("jobs.csv")
	checkErr(err)

	w := csv.NewWriter(file)
	defer w.Flush()		// must

	headers := []string{"Id", "Title", "Location", "Summary", "Company"}
	
	Werr := w.Write(headers)
	checkErr(Werr)
	
	for _, job := range(jobs) {
		jobSlice := []string{job.id, job.title, job.location, job.summary, job.company}
		jobErr := w.Write(jobSlice)
		checkErr(jobErr)
	}
}

해당 라이브러리를 근거로 위와 같이 작성해주었다. 함수가 종료될 때, csv를 쓰도록 defer를 사용했다. 먼저, headers를 작성하여 쓰기 작업을 한 후 jobs의 데이터들을 하나씩 쓰도록 코드를 구현했다. w.Write의 경우 error를 return하기 때문에 error체크도 병행해주었다.

코드를 돌려보면 jobs.csv 파일이 생성되고 쓰기 작업을 한 작업들이 담겨진 모습이다.

 

CSV Viewer and Editor

Save Your result: .csv or .xlsx EOL: CRLFLF Include Header

www.convertcsv.com

위 페이지에 csv파일에 담겨진 문자열을 전체 복사한 후 붙여넣으면 아래와 같이 보기 좋게 볼 수 있다.

 

id에 해당하는 값은 해당 공고의 고유 번호이다. 실제로 공고를 눌러서 확인하면 ID값이 연결된 것을 볼 수 있다.

https://www.saramin.co.kr/zf_user/jobs/relay/view?rec_idx=47446357

위는 1번 공고의 id 값으로 연결된 공고 url이다. 여기서 rec_idx가 바로 id 값이다. id를 link로 이어지도록 고쳐보자.

func writeJobs(jobs []extractedJob) {
	...
	
	for _, job := range(jobs) {
		jobSlice := []string{"https://www.saramin.co.kr/zf_user/jobs/relay/view?rec_idx="+job.id, job.title, job.location, job.summary, job.company}
		jobErr := w.Write(jobSlice)
		checkErr(jobErr)
	}
}

writeJobs 함수를 위와 같이 수정해주자.

id값이 위와같이 수정되고, id를 복사해서 웹에 넣어주면 아래와 같이 공고를 확인할 수 있다.

Add
Change id -> link 

 

 

goroutine 적용

함수들을 보면 goroutine을 사용하면 더 빠르게 동작할 수 있는 함수들이 있음을 느꼈을 수 있다. getPage()와 extractJob() 함수의 경우 goroutine을 사용하면 병렬적으로 보다 빠르게 데이터를 가져올 수 있다. 

먼저, getPage()를 고쳐보자. 데이터를 가져와야하기 때문에 channel을 사용한다.

func main() {
	total := getPages()
	
	var jobs []extractedJob
	c := make(chan []extractedJob)
	for i := 1; i <= total; i++ {
		go getPage(i, c)
	}

	for i := 1; i <= total; i++ {
		job := <- c
		jobs = append(jobs, job...)
		// same
		// jobs = append(jobs, <- c...)
	}
	writeJobs(jobs)
}

getPage()를 부르는 main()함수에 channel을 생성하고 getPage() 파라미터에 추가해주자. 

func getPage(page int, mainC chan <-[]extractedJob) {
	pageURL := baseURL + "&recruitPage=" + strconv.Itoa(page) 
	fmt.Println("Requesting :", pageURL)
	res, err := http.Get(pageURL)
	checkErr(err)
	checkStatusCode(res)
	
	defer res.Body.Close()

	doc, err := goquery.NewDocumentFromReader(res.Body)
	checkErr(err)

	var jobs []extractedJob
	c := make(chan extractedJob)

	cards := doc.Find(".item_recruit")
	cards.Each(func(i int, card *goquery.Selection) {
		job := extractJob(card)
		jobs = append(jobs, job)
	})
	mainC <- jobs
}

파라미터를  추가하고, return을 없앤 뒤 jobs를 채널에 전송하자.

이제 extractJob() 함수에 goroutine을 추가해보자. 위 getPage() 함수에서 채널을 만든 뒤, extractJob 파라미터에 추가하자.

func getPage(page int, mainC chan <-[]extractedJob) {
	pageURL := baseURL + "&recruitPage=" + strconv.Itoa(page) 
	fmt.Println("Requesting :", pageURL)
	res, err := http.Get(pageURL)
	checkErr(err)
	checkStatusCode(res)
	
	defer res.Body.Close()

	doc, err := goquery.NewDocumentFromReader(res.Body)
	checkErr(err)

	var jobs []extractedJob
	c := make(chan extractedJob)

	cards := doc.Find(".item_recruit")
	cards.Each(func(i int, card *goquery.Selection) {
		go extractJob(card, c)
	})

	for i:=0; i< cards.Length(); i++ {
		job := <- c
		jobs = append(jobs, job)
	}
	mainC <- jobs
}
func extractJob(card *goquery.Selection, c chan<- extractedJob) {
	link, _ := card.Attr("value")
	title := claanString(card.Find(".job_tit>a").Text())
	location := claanString(card.Find(".job_condition>span>a").Text())
	summary := claanString(card.Find(".job_sector").Clone().ChildrenFiltered(".job_day").Remove().End().Text())
	company := claanString(card.Find(".area_corp>strong>a").Text())
	c <- extractedJob {
		link : link,
		title : title,
		location : location,
		summary : summary,
		company : company,
	}
}

extractJob에서 파라미터를 추가하고, return을 없앤 뒤 채널에 추출한 extractJob을 전송하면 된다.

goroutine 사용 전
goroutine 사용 후

goroutine 사용 전 후를 비교해보면 recruitPage의 순서가 다르다는 것을 볼 수 있다. goroutine을 사용하여 병렬적으로 수행했기 때문이다. 프로그램을 돌려보면 이전보다 확실히 빠르다는 것을 느낄 수 있다. 체감상 2~3배 정도 빨라진 것 같다.

 

 

Add echo server

현재는 python으로만 검색하여 공고들을 스크랩했지만, python을 동적으로 변경하여 입력한 기술스택으로 공고를 수집할 수 있도록 만들어보자. go에서는 다양한 웹 프레임워크가 존재한다. echo는 그 중 성능이 뛰어난 것으로 알고 있다. echo web framework를 통해 동적으로 스크랩할 수 있도록 구현해보려한다.

일단, 기존 main.go에 있던 코드들을 scrapper directory안 scrapper.go로 이동시켜주었다. main() 함수를 Scrape()이라는 함수로 변경하고 string 파라미터를 추가하여 python을 대체할 문자열을 받을 수 있도록 했다. 첫 문자를 대문자로 한 이유는 export하기 위함이다. main()에서 접근하기 위함! 변경된 코드는 아래와 같다.

package scrapper

import (
	"encoding/csv"
	"fmt"
	"log"
	"net/http"
	"os"
	"strconv"
	"strings"

	"github.com/PuerkitoBio/goquery"
)

type extractedJob struct {
	link string
	title string
	location string
	summary string
	company string
}

// Scrape saramin by a term
func Scrape(term string) {
	var baseURL string = "https://www.saramin.co.kr/zf_user/search/recruit?&searchword="+term+"&recruitCount=50"
	total := getPages(baseURL)
	
	var jobs []extractedJob
	c := make(chan []extractedJob)
	for i := 1; i <= total; i++ {
		go getPage(i, baseURL, c)
	}

	for i := 1; i <= total; i++ {
		job := <- c
		jobs = append(jobs, job...)
		// same
		// jobs = append(jobs, <- c...)
	}
	writeJobs(jobs)
}

func writeJobs(jobs []extractedJob) {
	file, err := os.Create("jobs.csv")
	checkErr(err)

	w := csv.NewWriter(file)
	defer w.Flush()		// must

	headers := []string{"Link", "Title", "Location", "Summary", "Company"}
	
	Werr := w.Write(headers)
	checkErr(Werr)
	
	for _, job := range(jobs) {
		jobSlice := []string{"https://www.saramin.co.kr/zf_user/jobs/relay/view?rec_idx="+job.link, job.title, job.location, job.summary, job.company}
		jobErr := w.Write(jobSlice)
		checkErr(jobErr)
	}
}

func getPage(page int, baseURL string, mainC chan <-[]extractedJob) {
	pageURL := baseURL + "&recruitPage=" + strconv.Itoa(page) 
	fmt.Println("Requesting :", pageURL)
	res, err := http.Get(pageURL)
	checkErr(err)
	checkStatusCode(res)
	
	defer res.Body.Close()

	doc, err := goquery.NewDocumentFromReader(res.Body)
	checkErr(err)

	var jobs []extractedJob
	c := make(chan extractedJob)

	cards := doc.Find(".item_recruit")
	cards.Each(func(i int, card *goquery.Selection) {
		go extractJob(card, baseURL, c)
	})

	for i:=0; i< cards.Length(); i++ {
		job := <- c
		jobs = append(jobs, job)
	}
	mainC <- jobs
}

func claanString(str string) string {
	return strings.Join(strings.Fields(strings.TrimSpace(str)), "")
}

func extractJob(card *goquery.Selection, baseURL string, c chan<- extractedJob) {
	link, _ := card.Attr("value")
	title := claanString(card.Find(".job_tit>a").Text())
	location := claanString(card.Find(".job_condition>span>a").Text())
	summary := claanString(card.Find(".job_sector").Clone().ChildrenFiltered(".job_day").Remove().End().Text())
	company := claanString(card.Find(".area_corp>strong>a").Text())
	c <- extractedJob {
		link : link,
		title : title,
		location : location,
		summary : summary,
		company : company,
	}
}

func getPages(baseURL string) int {
	pages := 0
	res, err := http.Get(baseURL)
	checkErr(err)
	checkStatusCode(res)

	defer res.Body.Close()

	doc, err := goquery.NewDocumentFromReader(res.Body)
	checkErr(err)

	doc.Find(".pagination").Each(func(i int, s *goquery.Selection) {
		pages = s.Find("a").Length()
	})

	return pages
}

func checkErr(err error) {
	if err != nil {
		log.Fatalln(err)
	}
}

func checkStatusCode(res *http.Response) {
	if res.StatusCode != 200 {
		log.Fatalln("Request failed with Status:", res.StatusCode, res.Status)
	}
}

 

main을 추가하고 python string을 보내 테스트해보자.

package main

import "github.com/qazyj/jobscrapper/scrapper"

func main() {
	scrapper.Scrape("python")
}

(잘 돌아간다!)

 

이제 main에 echo를 사용할 코드를 추가해보자! 아래 링크를 참고했다.

 

GitHub - labstack/echo: High performance, minimalist Go web framework

High performance, minimalist Go web framework. Contribute to labstack/echo development by creating an account on GitHub.

github.com

go get github.com/labstack/echo

명령어를 터미널에 쳐주자.

 

예제대로 main에 코드를 추가 했다. (middleware는 사용하지 않을 것이기때문에 추가해주지 않았다.)

package main

import (
	"net/http"

	"github.com/labstack/echo"
)

func main() {
	// Echo instance
	e := echo.New()
  
	// Routes
	e.GET("/", hello)
  
	// Start server
	e.Logger.Fatal(e.Start(":1323"))
}

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

1323포트 번호로 서버를 열었다.

테스트

테스트해보면 위와같이 문제없이 돌아가는 것을 확인할 수 있다.

참고!
server를 종료하고자 한다면 mac기준 control+c를 누르면 된다.

 

이제 검색어를 입력할 수 있는 input box와 button이 있는 페이지를 만들어보자.

html 추가

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Go Jobs</title>
  </head>
  <body>
    <h1>Go Jobs</h1>
    <h3>www.saramin.co.kr scrapper</h3>
    <form method="POST" action="/scrape">
      <input placeholder="what job do you want" name="term" />
      <button>Search</button>
    </form>
  </body>
</html>

이제 메인에서 handler를 통해 localhost:1323으로 주소를 입력할 때 home.html이 나올 수 있도록 만들어주자.

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

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

 

잘 나온다.

 

이제 Search 버튼을 누르면 동작할 POST method를 추가해보자.

func main() {
	...
	e.POST("/scrape", handlerScrape)
  
	...
}

func handlerScrape(c echo.Context) error {
	fmt.Println(c.FormValue("term"))
	return nil
}

그 후 돌려보면 아래와 같이 검색한 검색어가 정상적으로 들어오는 것을 확인할 수 있다.

 

이제 csv를 다운로드할 수 있도록 구현해볼 것이다.

const fileName string = "jobs.csv"

func handlerScrape(c echo.Context) error {
	defer os.Remove(fileName)
	term := strings.ToLower(scrapper.CleanString(c.FormValue("term")))
	scrapper.Scrape(term)
	return c.Attachment(fileName, fileName)
}

handlerScrape 함수를 위와 같이 작성해주었따. scrapper에 term을 파라미터로 보내 jobs.csv를 만들고 해당 csv 파일이 바로 다운로드 되도록 구현했다. 해당 함수가 종료되면 jobs.csv는 삭제되도록 defer를 사용했다.

 

 

Reference

'Go' 카테고리의 다른 글

URL Checker & Go Routine  (0) 2024.01.14
method를 활용한 map 구현(Search, Add, Delete, Update)  (0) 2024.01.10
struct/public,private/String()/error handling  (0) 2024.01.07
Struct  (0) 2024.01.04
Maps  (0) 2023.12.30

Go Routine을 사용하지 않은 URL Checker 

지금까지 배운 Go 사용법으로 URL Checker를 만들어 보려고한다.

package main

import (
	"errors"
	"net/http"
)

var errRequestFailed = errors.New("Request is failed")

func main() {
	urls := []string{
		"https://www.naver.com/",
		"https://www.google.com/",
		"https://www.amazon.com/",
		"https://www.facebook.com/",
		"https://www.instagram.com/",
		"https://www.naver.com/",
	}
	for _, url := range urls {
		fmt.Println(hitURL(url))
	}
}

func hitURL(url string) error {
	resp, err := http.Get(url)
	if err != nil || resp.StatusCode >= 400 {
		return errRequestFailed
	}
	return nil
}

main.go

일단 여러 url들을 배열안에 넣어두고 for문을 통해 url이 정상적인 응답을 주는지 확인해보았다. http의 경우 Go에서 기본적으로 지원하는 라이브러리를 사용했다. 링크를 통해 사용법을 배웠다.

resp, err := http.Get("http://example.com/")
if err != nil {
	// handle error
}

짤막하게 설명하자면 http method 중 하나인 Get을 사용하기 위해서는 http.Get 메소드에 url을 넣어서 요청하면 response와 error를 반환한다. error를 받은 경우 개발자가 스스로 error handling을 할 수 있도록 구현해두었다.

 

위의 사용법을 기반으로 아래와 같은 hitURL이라는 함수를 작성했다.

func hitURL(url string) error {
	resp, err := http.Get(url)
	if err != nil || resp.StatusCode >= 400 {
		return errRequestFailed
	}
	return nil
}

url에 요청을 보내고 받은 response와 error를 받아오고 err가 발생했거나 응답의 상태코드가 400이상이면 request가 실패했다는 error를 return하고 아닐 시 nil을 return 하도록 했다.

 

프로젝트를 돌려보면 6개의 응답 모두 nil을 return하여 문제없는 url임을 확인할 수 있다. 조금 더 결과물을 보기 좋게하기위해 map을 사용해보자.

package main

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

var errRequestFailed = errors.New("Request is failed")

func main() {
	results := map[string]string{}			// ADD
	urls := []string{
		"https://www.naver.com/",
		"https://www.google.com/",
		"https://www.amazon.com/",
		"https://www.facebook.com/",
		"https://www.instagram.com/",
		"https://www.naver.com/",
		"https://www.airbnb.com/",
		"https://www.reddit.com/",
	}
	for _, url := range urls {
		result := "OK"			// ADD
		err := hitURL(url)
		if err != nil {			// ADD
			result = "FAILED"			// ADD
		}			// ADD
		results[url] = result			// ADD
	}
	// ADD
	for url, result := range results {
		fmt.Println(url, result)
	}
}

func hitURL(url string) error {
	fmt.Println("Checking: ", url)			// ADD
	resp, err := http.Get(url)
	if err != nil || resp.StatusCode >= 400 {
		return errRequestFailed
	}
	return nil
}

출력

string 배열에있는 모든 url을 확인한 뒤, 결과물인 성공, 실패 여부를 map에 저장하여 출력해 준 모습이다. 문제없는 url만 저장해서인지 FAILED가 하나도 없다.

 

하지만, 이러한 URL Checker를 만드는 데에는 다른 언어와의 큰 차이점을 느낄 수 없다. 현재는 모든 URL을 하나하나하나 Checking하며, 이전 작업이 완료되어야만 다음 작업을 수행하고 있다. Go에는 이러한 작업을 한 번에 수행할 수 있도록 해주는 Go Routine이라는 기술이 있는데, 현재 사용해보면 좋을 것 같아서 적용해보려고 한다.

tip!

go에서는 make()라는 함수가 있다.
// The make built-in function allocates and initializes an object of type
// slice, map, or chan (only). Like new, the first argument is a type, not a
// value. Unlike new, make's return type is the same as the type of its
// argument, not a pointer to it. The specification of the result depends on
// the type:
//
//	Slice: The size specifies the length. The capacity of the slice is
//	equal to its length. A second integer argument may be provided to
//	specify a different capacity; it must be no smaller than the
//	length. For example, make([]int, 0, 10) allocates an underlying array
//	of size 10 and returns a slice of length 0 and capacity 10 that is
//	backed by this underlying array.
//	Map: An empty map is allocated with enough space to hold the
//	specified number of elements. The size may be omitted, in which case
//	a small starting size is allocated.
//	Channel: The channel's buffer is initialized with the specified
//	buffer capacity. If zero, or the size is omitted, the channel is
//	unbuffered.
func make(t Type, size ...IntegerType) Type

command+make()라는 함수를 클릭하면 위와같은 builtin.go라는 파일에 코드가 생성되어 있다. go에서 제공해주는 함수인데, make()라는 함수는 주석을 정리해보면 slice, map, chan과 같은 타입의 object를 할당 및 초기화 해준다고 설명되어 있다.  사용법도 각각의 object 별로 주석으로 설명이 잘 되어있기 때문에 읽어보고 사용해봐도 좋다.

 

위 map을 make()라는 함수를 통해 만들어보자.

// Map: An empty map is allocated with enough space to hold the
// specified number of elements. The size may be omitted, in which case
// a small starting size is allocated.
results := map[string]string{}

기존 key와 value가 string인 map을 생성할 때 위와같이 작생했다.

results := make(map[string]string)

make를 사용하면 위와같은 코드로 만들 수 있다.

 

음.... map을 생성할 때는 make를 굳이 사용할 필요가 있을까? 라는 의문이 든다. {} -> make() 의 차이점은 오히려 코드를 많이 적게 만드는 것이 아닌가?라는 의문이 들지만, 가독성을 생각한다면 사용할만한 가치가 있다고 생각한다. 코드상의 큰 차이점은 없지만, make라는 의미를 부여하기 때문이다. map[string]string이라는 object를 생성한다는 것을 보다 직관적으로 알 수 있기 때문이다.

results := make(map[string]string)
results := map[string]string{}

위 코드를 보며, 개인적으로 보다 나은 방식을 선택하면 좋을 것 같다.


Go Routine

다른 함수와 동시에 실행시키는 함수

Go에서 최적화하는 방법은 동시에 작업을 처리하는 것이다. Go에서는 이러한 동시 작업 처리를 위해서는 Go routine이라는 컨셉을 이해해야 한다. 

func main() {
	testCount("test1")
	testCount("test2")
}

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

main.go

테스트를 보다 쉽게하기 위해 조금 더 이해해보기 쉽게 main.go를 위와같이 작성했다.

출력

순서대로 출력되는 모습을 확인할 수 있다. Go routine을 한 번 적용해보자. Go routine을 적용하는 방법은 아주 간단하다. 함수를 불러오는 코드에 함수 앞에 go만 붙이면 된다. 예를들어, testCount()를 go testCount()로 작성하면 된다.

func main() {
	go testCount("test1")
	testCount("test2")
}

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

출력

test1, test2가 같이실행되는 것을 볼 수 있다. 그렇다면, 두 함수 모두 Go routine으로 처리하면 어떻게 될까?

func main() {
	go testCount("test1")
	go testCount("test2")
}

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

코드를 위와같이 수정하고 돌려지만, 출력되는 결과물은 없었다. 그 이유는 main()에 작성된 두 함수에 접근하며 아래작성 된 코드를 실행하는데, testCount("test1")과 testCount("test2")를 돌리는 중간에 main() 함수가 종료되었기 때문이다. Go routine은 프로그램이 작동하는 동안만 유효하기 때문에 이 점을 유념해서 코드를 작성해야 한다.

 

그렇다면, go routine을 쓰면 해당 작업이 끝나는 시간을 고려해서 이후 코드들을 작성해야하는가..?  google 개발자들이 이렇게 허술하게 만들었을리가 없다고 생각했다면 역시나 이를 고려해서 channel이라는 기능을 만들었다. 

 

Channel

goroutine 간 통신을 위한 메커니즘으로, 데이터를 안전하게 전달하고 동기화하는 데 사용

ChatGPT에 의하면 Channel은 아래와 같은 기능을 위해 존재한다고 말한다.

  1. 동시성과 통신: channel은 고루틴 간의 통신을 위해 디자인되었습니다. 여러 고루틴이 병렬로 실행되는 환경에서 안전하게 데이터를 주고받을 수 있도록 합니다.
  2. 데이터 전달: channel을 사용하여 데이터를 안전하게 전달할 수 있습니다. 데이터를 보낼 때는 <- 연산자를 사용하며, 데이터를 받을 때는 <- 연산자를 반대로 사용합니다.
  3. 동기화: channel은 데이터를 전달할 때 발신자와 수신자 간에 동기화를 제공합니다. 즉, 데이터를 주고 받는 과정에서 발신자가 데이터를 보낼 때까지 대기하게 되며, 수신자는 데이터를 받을 때까지 대기하게 됩니다.

여기서 우리가 봐야할 건, 동기화에 있는 수신자는 데이터를 받을 때까지 대기하게 됩니다.이다. 즉, main에서 channel을 생성하여 goroutine을 사용하면, 수신자인 main에서 데이터를 받을 때까지 대기한다는 말이다.

 

실제로 적용해보자.

func main() {
	c := make(chan string)
	people := [2]string{"test1", "test2"}
	for _, person := range people {
		go testCount(person, c)
	}
	result := <- c
	fmt.Println(result)
}

func testCount(person string, c chan string) {
	c <- person
}

출력

string으로 channel을 생성하고 testCount()에 포함하여 보내주고, channel로 person이라는 string 데이터를 보냈다. 그 후 channel에서 받은 데이터를 result에 넣어주고 출력해주었다. 이전 아무런 출력이 일어나지 않은 것과는 다르게 test2라는 string이 출력된 모습을 볼 수 있다. test1, test2 두개의 string을 보냈는데 test2만 출력되는 이유는 channel은 queue나 pipe와 비슷한 형태로 1개씩 데이터를 보내고 받을 수 있기 때문이다. 

func main() {
	c := make(chan string)
	people := [2]string{"test1", "test2"}
	for _, person := range people {
		go testCount(person, c)
	}
	fmt.Println(<- c)			// 데이터 꺼내기
	fmt.Println(<- c)			// 데이터 꺼내기
}

func testCount(person string, c chan string) {
	c <- person
}

출력

위와같이 두 번 데이터를 꺼내면 전달한 두 개의 데이터를 모두 받을 수 있다.

그렇다면, 보낸 데이터가 없을 때 꺼낸다면 어떻게 될까?

func main() {
	c := make(chan string)
	people := [2]string{"test1", "test2"}
	for _, person := range people {
		go testCount(person, c)
	}
	fmt.Println(<- c)			// 있는 데이터 꺼내기
	fmt.Println(<- c)			// 있는 데이터 꺼내기
	fmt.Println(<- c)			// 없는 데이터 꺼내기
}

func testCount(person string, c chan string) {
	c <- person
}

출력

2개의 데이터를 꺼내고 교착상태가 일어난 모습을 볼 수 있다. 모든 goroutines은 모두 종료되어 sleep상태가 되었지만, main에서 데이터를 기다리고 있기때문에 deadlock이 발생한 상황이다. 

 

이러한 문제를 막기 위해서는 다양한 방법이 존재한다. 그 중 하나는 sync.WaitGroup을 사용해서 막는 것이다. goroutine이 종료되면 channel을 닫아주면 된다.

func main() {
	c := make(chan string)
	people := [2]string{"test1", "test2"}
	var wg sync.WaitGroup

	for _, person := range people {
		wg.Add(1)
		go testCount(person, c, &wg)
	}

	go func() {
		wg.Wait()
		close(c)
	}()

	for msg := range c {
		fmt.Println(msg)
	}
}

func testCount(person string, c chan string, wg *sync.WaitGroup) {
	defer wg.Done()
	c <- person
}

goroutine을 1개 부를 때마다 wg.Add()를 추가하고 goroutine이 종료되면 wg.Done()으로 추가한 것을 하나씩 제거한다. wg.Wait()으로 모든 goroutine이 종료됨을 확인하고 다 종료되었다면 close(c)를 통해 channel을 닫아준다. 그 후 channel의 범위만큼 반복문을 돌린다면 이전과 같은 상황을 막을 수 있다.

 

아니면 goroutine을 사용하는 수만큼 반복문을 돌려도 된다.

func main() {
	c := make(chan string)
	people := [2]string{"test1", "test2"}

	for _, person := range people {
		go testCount(person, c)
	}

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

func testCount(person string, c chan string) {
	c <- person
}

people의 배열 크기만큼 goroutine의 수를 늘리기 때문에 배열 크기만큼 데이터를 받아오는 것이다. 그렇다면, 이전 URL Checker에 Go Routine을 적용해보자. 

 

 

URL Checker + Go Routine

type requestResult struct {
	url string
	status string			// OK, FAILED
}

먼저, channel로 전달할  url과 성공, 실패 여부 필드를 담은 struct를 추가했다.

c := make(chan requestResult)

만든 struct type의 chan을 생성했다.

	for _, url := range urls {
		go hitURL(url, c)
	}

main에서 hitURL을 goroutine으로 부르고

func hitURL(url string, c chan requestResult) {
	fmt.Println("Checking: ", url)
	resp, err := http.Get(url)
	status := "OK"
	if err != nil || resp.StatusCode >= 400 {
		status = "FAILED"
	}
	c <- requestResult{url: url, status: status}
}

hitURL에서 channel에 성공 여부를 struct에 담아서 보냈다.

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

main에서 channel을 통해 받은 result struct를 출력했다.

출력

package main

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

type requestResult struct {
	url string
	status string			// OK, FAILED
} 

var errRequestFailed = errors.New("Request is failed")

func main() {
	c := make(chan requestResult)
	urls := []string{
		"https://www.naver.com/",
		"https://www.google.com/",
		"https://www.amazon.com/",
		"https://www.facebook.com/",
		"https://www.instagram.com/",
		"https://www.naver.com/",
		"https://www.airbnb.com/",
		"https://www.reddit.com/",
	}
	for _, url := range urls {
		go hitURL(url, c)
	}

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

func hitURL(url string, c chan requestResult) {
	fmt.Println("Checking: ", url)
	resp, err := http.Get(url)
	status := "OK"
	if err != nil || resp.StatusCode >= 400 {
		status = "FAILED"
	}
	c <- requestResult{url: url, status: status}
}

전체코드이다. goroutine을 사용하지 않았을 때와 비교가 크게 될 정도로 빠르게 종료되는 모습을 볼 수 있을 것이다. 병렬적으로 수행하기 때문. 정말 편리하게 병렬적으로 함수를 실행할 수 있는 것이 Go의 엄청 큰 장점이지 않을까 싶다.

 

 

 

'Go' 카테고리의 다른 글

Job Scrapper  (0) 2024.01.29
method를 활용한 map 구현(Search, Add, Delete, Update)  (0) 2024.01.10
struct/public,private/String()/error handling  (0) 2024.01.07
Struct  (0) 2024.01.04
Maps  (0) 2023.12.30

Dictionary.go

package mydict

// Dictionary type
type Dictionary map[string]string

위와 같은 type으로 된 Dictionary map을 만들어준다. 

패키지는 위와 같다.

main.go

func main() {
	dictionary := mydict.Dictionary{"first":"first"}
	fmt.Println(dictionary)
}

main에서 위와같이 사용할 수 있을 것이다. 하지만, 에러 처리등 불편한 점이 존재하고있는 상태다. 이전에 배운 method를 활용해 main을 가독성 좋게 작성해보자.

 

Search

var errNotFound = errors.New("Not Found")

func(d Dictionary) Search(word string) (string, error) {
	value, exists := d[word]
	if exists {
		return value, nil
	}
	return "", errNotFound
}

mydict.go에 위 method를 추가한다.

func main() {
	dictionary := mydict.Dictionary{"first":"first"}
	value, err := dictionary.Search("second")
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Println(value)
	}
}

main.go에서 dictionary에 있는 search method를 사용해 출려본다.

출력

정상적으로 error handling이 되는 모습이다. err가 발생했을 때, 시스템이 더이상 돌아가지 않게 하고싶다면 다른 방법으로 if문 안을 처리하면 된다. 이러한 방식이 단순히 map을 사용하여 key를 통해 value를 얻는 방식보다 개발자가 원하는 방식대로 error를 handling할 수 있기때문에 각자의 취향대로 선택하면 될 것 같다.

 

 

Add

var errExistsWord = errors.New("That word already exists")

func(d Dictionary) Add(word, def string) error {
	_, err := d.Search(word)	// 이미 key로 된 중복된 단어가 있는지 확인
	switch err {
	case errNotFound:
		d[word] = def
	case nil:
		return errExistsWord
	}
	return nil
}

mydict.go에 위와같은 method를 추가해준다.

func main() {
	dictionary := mydict.Dictionary{}
	err := dictionary.Add("first", "first")
	if err != nil {
		fmt.Println(err)
	}
	def, err := dictionary.Search("first")
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Println(def)
	}
}

main.go에서 확인해주면 정상적으로 추가가되고, error가 handling되는 모습을 볼 수 있다.

 

 

Update Delete

var errCantUpdate = errors.New("Can`t update non-exitstiong word")

func(d Dictionary) Update(word, def string) error {
	_, err := d.Search(word)
	switch err {
	case nil:
		d[word] = def
	case errNotFound:
		return errExistsWord
	}
	return nil
}

// Delete a word
func(d Dictionary) Delete(word string) {
	delete(d, word)
	// word가 없다면 아무 작업도 하지 않음
}

mydict.go 에 위와 같은 메소드를 추가해준다.

func main() {
	dictionary := mydict.Dictionary{}
	err1 := dictionary.Add("first", "first")
	if err1 != nil {
		fmt.Println(err1)
	}
	
	err2 := dictionary.Update("first", "second")
	if err2 != nil {
		fmt.Println(err2)
	}
	word, _ := dictionary.Search("first")
	fmt.Println(word)

	dictionary.Delete("first")
	word2,err3 := dictionary.Search("first")
	if err3 != nil {
		fmt.Println(err3)
	} else {
		fmt.Println(word2)
	}
}

출력

map의 key가 "first"인 value의 값이 "second"로 변경되어 출력되었고, delete후 search를 했을 때 찾을 수 없다는 error handling이 된 것을 확인할 수 있다.

 

'Go' 카테고리의 다른 글

Job Scrapper  (0) 2024.01.29
URL Checker & Go Routine  (0) 2024.01.14
struct/public,private/String()/error handling  (0) 2024.01.07
Struct  (0) 2024.01.04
Maps  (0) 2023.12.30

Struct

다양한 struct를 만들며 struct에 대한 학습을 하려함

Backing.go 라는 파일을 만들고, 아래와 같은 struct를 만들어서 main.go에서 Import해서 사용해보려한다. 

package banking

// Account Struct
type Account struct {	
	owner string
	balance int
}

export를 하기 위해 struct의 시작 문자는 대문자로 해주어야 한다.

package main

import (
	"github.com/qazyj/learngo/banking"
)

func main() {
	account := banking.Account{owner:"qazyj", balance: 1000}
}

파일 구조

하지만, field 값을 제대로 못 읽어 오는 듯 하다.

 

이유는 Account가 private이기 때문이다. ?? Java에서 public, private를 선언해주는 것도 아닌데 왜 private인지 의문이 들 것이다. Go에서는 public과 private가 소문자, 대문자로 시작하는 것을 의미한다.

public : 대문자 시작
private : 소문자 시작

 

이처럼 field 변수명도 대문자로 시작해줘야지만 외부 파일에서 사용할 수 있다.

package banking

// Account Struct
type Account struct {	
	Owner string			// 대문자 시작
	Balance int				// 대문자 시작
}

Banking.go

package main

import (
	"github.com/qazyj/learngo/banking"
)

func main() {
	account := banking.Account{Owner:"qazyj", Balance: 1000}
}

Main.go

 

import

대문자 시작으로 바꿔주면 정상적으로 import가 되어 사용이 가능하다. 

 

Java에서는 public, private, protected를 통해 외부 클래스 파일의 접근을 허용한다. 즉, 외부의 접근을 신경쓰기 위해 public, private, protected 중 하나를 추가해야된다는 뜻이다. 하지만, Go에서는 시작 문자를 소/대문자를 통해 public과 private를 구분한다. 코드가 더 간결해지고 한 눈에 알아볼 수 있다는 장점이 있다. 

 

정리

package banking

// Account Struct
type Account struct {	
	owner string			// private
	Balance int				// public
}

 

 

하지만, 나는 Owner라는 이름은 Account를 생성할 때 받고싶지만 Balance는 0이라는 고정값으로 설정하고 싶다. 

func main() {
	account := banking.Account{Owner:"qazyj"}
	fmt.Println(account)
}

출력

Go에서는 struct에 대한 다양한 생성자를 자동으로 만들어주는 듯 하다. struct를 생성할 때 설정되지 않은 필드값은 자동으로 default값으로 설정될 수 있도록 말이다. 위와같이 code를 변경하면, Balance라는 필드값은 private인 balance로 변경해도 문제없이 작동한다.

 

struct의 필드값을 public으로 변경하면 아래와 같은 작업도 가능하다.

func main() {
	account := banking.Account{Owner:"qazyj"}
	account.Owner = "test"
	fmt.Println(account)
}

출력

필드값 변경이 용이하다는 장점이 있지만, 필드값 변경이 무분별하게 진행될 수 있다는 단점도 존재한다. 이러한 단점을 막기위해서는 function을 만들어주어야 한다. 

type Account struct {	
	owner string
	balance int
}

func NewAccount(owner string) *Account{
	account := Account{
		owner: owner,
		balance: 0,
	}
	return &account
}

많은 예제 코드들을 확인해본 결과 생성자에는 New+struct 이름으로 만드는 듯 하다.

func main() {
	account := accounts.NewAccount("qazyj")
	fmt.Println(account)
}

출력

실제 메모리 address를 return하여 복사본이 아닌 만든 값을 return 해주면 된다. account라는 struct를 출력할 때, field값들이 출력되는 이유는 struct를 출력할 때 내부적으로 String 메소드를 호출하여 출력하기 때문이다. String 메소드를 정의하지 않은 경우 Go에서 자동으로 기본 field 값을 포맷팅하여 return 해준다. 링크를 통해 확인해보니 printValue라는 메소드를 통해 아래와 같이 포인트인지 확인한다.

	case reflect.Pointer:
		// pointer to array or slice or struct? ok at top level
		// but not embedded (avoid loops)
		if depth == 0 && f.UnsafePointer() != nil {
			switch a := f.Elem(); a.Kind() {
			case reflect.Array, reflect.Slice, reflect.Struct, reflect.Map:
				p.buf.writeByte('&')
				p.printValue(a, verb, depth+1)
				return
			}
		}
		fallthrough

그 후 다시 printValue로 들어가 아래와 struct인지 확인해주고 출력될 string을 추가로 만들어준다.

	case reflect.Struct:
		if p.fmt.sharpV {
			p.buf.writeString(f.Type().String())
		}
		p.buf.writeByte('{')
		for i := 0; i < f.NumField(); i++ {
			if i > 0 {
				if p.fmt.sharpV {
					p.buf.writeString(commaSpaceString)
				} else {
					p.buf.writeByte(' ')
				}
			}
			if p.fmt.plusV || p.fmt.sharpV {
				if name := f.Type().Field(i).Name; name != "" {
					p.buf.writeString(name)
					p.buf.writeByte(':')
				}
			}
			p.printValue(getField(f, i), verb, depth+1)
		}
		p.buf.writeByte('}')

이러한 코드 방식을 거쳐서 default 값을 만들어준다.

 

내가 원하는 방식으로 string을 출력하고 싶다면 String()이라는 메소드를 만들어주면 된다. override와 비슷한 방식이라고 생각한다. 아래와 같은 메소드를 정의준 뒤, 출력해주자.

// String 메소드 정의
func (a Account) String() string {
	return fmt.Sprintf("Owner: %s, Balance: %d", a.owner, a.balance)
}

출력

이전과는 다른 방식으로 출력이 되는 것을 볼 수 있다. 하지만, field가 추가되는 경우 다시 수정해줘야하는 단점이 존재할 것이다. 기본적으로 제공하는 것을 사용하는 방법이 좋아 보이긴 하다.

 

값을 수정할 수 있는 method를 만들어보려 한다. Go에서 method는 function과는 조금 다르다. 바로 receiver를 추가해줘야 한다. func과 함수명 사이에 receiver를 추가해주면, method가 된다. receiver에서 지켜야 할 사항이 있는데 receiver명은 struct의 첫글자를 따서 사용한다는 것이다. 편의를 위해서 정해놓은 암묵적인 rule 인것 같다. 물론, 지키지않아도 사용가능하다!

// make method
// func receiver name(prameter)
func (a *Account) Deposit(amount int) {
	a.balance += amount
}

위와 같은 method를 이용하여 struct의 값을 private로 설정해도 수정할 수 있다.

 

출력된 balance 값을 확인하기 위해 아래와 같은 method를 추가해주었다.

func (a Account) Balance() int {
	return a.balance
}

확인해본 결과 10이 추가된다.

tip)
(a *Account) 에서 *를 붙여준 이유는 *를 붙여줘야지만 내가 설정해놓은 struct를 receiver로 가져오기 때문이다. *를 제외하면 struct를 복사해서 가져오기때문에 수정한 값이 적용되지 않는다.

 

 

 

Error handling

Go에서는 exception, try-catch와 같은 에러 핸들링 도구가 존재하지 않는다. 그렇기때문에 직접 error를 직접 체크하고 return 해줘야 한다. 아래와 같은 기능을 하는 Withdraw 메소드를 만든다.

func(a *Account) Withdraw(amount int) {
	a.balance -= amount
}

이때, a.balance 값이 amount 값보다 작은 경우 작업을 하지않고 error를 return 해주려고 한다.

func(a *Account) Withdraw(amount int) error {
	if a.balance < amount {
		return errors.New("Can`t withdraw")
	}
	a.balance -= amount
	return nil
}

위와같이 작성해주면 된다. Go에서 error는 error와 nil이라는 값으로 나타낼 수 있다. 보통 nil은 에러가 없다는 값으로 사용한다. main에서 한 번 확인해보자.

func main() {
	account := accounts.NewAccount("qazyj")
	account.Deposit(10)
	fmt.Println(account.Balance())
	err := account.Withdraw(20)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(account.Balance())
}

출력

balance값보다 큰 값으로 Withdraw를 했기때문에 nil이 아닌 error가 return이 되었고, if문을 통해 err가 nil이 아닌 경우 출력하여 제대로 error handling을 하는지 확인했다.

Go에서는 error가 발생했다고 시스템을 종료시키거나 하지 않는다. 순전히 개발자가 error를 스스로 return해주고, error에 관한 처리를 스스로 해주어야만 한다. 그렇기때문에 error를 return해도 아래 코드들이 정상적으로  실행된다.

 

에러를 로그로 남기며 시스템을 종료하고 싶다면, 아래와 같이 handling할 수 있다.

func main() {
	account := accounts.NewAccount("qazyj")
	account.Deposit(10)
	fmt.Println(account.Balance())
	err := account.Withdraw(20)
	if err != nil {
		log.Fatalln(err)
	}
	fmt.Println(account.Balance())
}

출력

error가 return되었고, error가 return되었을 때 log를 찍으며 치명적인 로그이기때문에 시스템을 종료하도록 하는 것이다. 시스템이 정상적으로 돌아갔다면 10이 한번 더 출력이 되어야 하지만, 바로 종료되었기 때문에 출력이되지 않는 모습이다. 

'Go' 카테고리의 다른 글

URL Checker & Go Routine  (0) 2024.01.14
method를 활용한 map 구현(Search, Add, Delete, Update)  (0) 2024.01.10
Struct  (0) 2024.01.04
Maps  (0) 2023.12.30
Arrays/Slices  (0) 2023.12.29

Struct

이전에 map에 대해배웠다. map의 경우 key와 value의 타입이 고정되어 있기 때문에, 아래와 같은 형태는 컴파일 에러가 발생한다.

user := map[string]string{"name":"qazyj", "age":1}

이러한 경우 Struct라는 구조체를 이용해서 관리하면 된다. map보다 동적이며, 원하는 type으로 생성할 수 있다. Java의 class와 같은 느낌이다.

 

테스트

type person struct {
	name string
	age int
	favFood []string
}

func main() {
	favFood := []string{"ramen", "kimchi"}
	user := person{"qazyj", 20, favFood}
	fmt.Println(user)
}

출력

func main() {
	favFood := []string{"ramen", "kimchi"}
	user := person{"qazyj", 20, favFood}
	fmt.Println(user.name)
}

출력

 

하지만, 위와같이 structure를 만드는 것은 코드상에서 보기 좋지 않아 보인다. (명확하게 보이지 않기 때문이다. 어떤 value가 어떤 value인지.. 값을 찾기 위해서는 정의되어있는 person을 한 번 더 봐야하기 때문이다.) 그렇기때문에 아래와 같은 방식이 처음 쓸 때는 불편하지만, 이후에 다시 코드를 볼 때 읽기 편리하다.

func main() {
	favFood := []string{"ramen", "kimchi"}
	user := person{
		name: "qazyj", 
		age: 20, 
		favFood: favFood}
	fmt.Println(user)
}

각각 어떤 value가 어떤 value인지 person이라는 struct를 보지않아도 한 눈에 확인할 수 있다. Java보다 가독성이 뛰어난 느낌이 든다.

 

Go는 java처럼 class가 없다. 또한, python이나 js처럼 object도 없다. Java에서 class를 생성할 때 필수로 있어야하는 것이 생성자이다. Go에서는 생성자가 없다.

 

 

 

 

 

 

 

'Go' 카테고리의 다른 글

method를 활용한 map 구현(Search, Add, Delete, Update)  (0) 2024.01.10
struct/public,private/String()/error handling  (0) 2024.01.07
Maps  (0) 2023.12.30
Arrays/Slices  (0) 2023.12.29
Pointer  (0) 2023.12.28

Map

Python이나 JavaScript의 Object와 약간 비슷한 형태이다.

테스트

func main() {
	// map[key type]value type{}
	name := map[string]string{"name":"qazyj", "address":"Incheon"}
	fmt.Println(name)
}

출력

아래와 같이 for문을 이용해서 출력할 수도 있다.

테스트

func main() {
	// map[key type]value type{}
	user := map[string]string{"name":"qazyj", "address":"Incheon"}
	for key, value := range user {
		fmt.Println(key, value)
	}
}

출력

 

 

 

 

 

'Go' 카테고리의 다른 글

struct/public,private/String()/error handling  (0) 2024.01.07
Struct  (0) 2024.01.04
Arrays/Slices  (0) 2023.12.29
Pointer  (0) 2023.12.28
If/switch  (0) 2023.12.28

Arrays

Go의 Array는 다른 언어들과 다르게 크기를 마음대로 조절할 수 있다. 

테스트

func main() {
	names := [5]string{"nico", "qazyj", "dal"}
	fmt.Println(names)
}

 

출력

일반적인 배열을 만드는 방법이다. 배열의 크기를 지정하고 배열안에 값을 넣어준다.

 

Slice

java에서의 List와 같은 느낌이 든다. slice라고 부르는데, append를 이용해서 사용할 수 있다.

배열의 크기를 지정하면 append를 사용할 수 없지만, 크기가 지정되어있지 않다면 append를 사용해서 크기를 마음대로 조절할 수 있다.

테스트

func main() {
	names := []string{"nico", "qazyj", "dal"}
	fmt.Println(names)
}

출력

위와 다르게 추가된 수 만큼 배열의 크기가 생성된다. append를 사용해보자

func main() {
	names := []string{"nico", "qazyj", "dal"}
	names = append(names, "test")
	fmt.Println(names)
}

출력

크기가 1만큼 증가하며 새로운 string이 추가된 것을 확인할 수 있다.

 

그렇다면, append는 어떻게 이루어져 있을까?

// The append built-in function appends elements to the end of a slice. If
// it has sufficient capacity, the destination is resliced to accommodate the
// new elements. If it does not, a new underlying array will be allocated.
// Append returns the updated slice. It is therefore necessary to store the
// result of append, often in the variable holding the slice itself:
//
//	slice = append(slice, elem1, elem2)
//	slice = append(slice, anotherSlice...)
//
// As a special case, it is legal to append a string to a byte slice, like this:
//
//	slice = append([]byte("hello "), "world"...)
func append(slice []Type, elems ...Type) []Type

builtin.go 라는 파일에 append는 위와같이 정의되어 있다. 설명을 보면 1개만 되는 것이 아닌 여러개가 한꺼번에 뒤로 추가해주는 것도 가능하다고 한다. 사용 방법은 배열 변수명 = append(배열 변수명, 추가하고싶은 element.....) 방법으로 사용하면 된다.

 

Java는 배열과 List가 있는 반면, Go의 경우 배열과 slice가 있다.

 

 

 

 

'Go' 카테고리의 다른 글

Struct  (0) 2024.01.04
Maps  (0) 2023.12.30
Pointer  (0) 2023.12.28
If/switch  (0) 2023.12.28
for, range, args  (0) 2023.12.28

Pointer

다른 변수, 혹은 그 변수의 메모리 공간주소를 가리키는 변수를 말한다

Test

func main() {
	a := 2
	b := a
	a = 10
	fmt.Println(a, b)
}

위와 같이 int type의 변수 a를 만들고, 만들어진 a를 b에 넣어준다. 그리고, a를 10으로 변경한다면 a와 b의 값은 어떻게 될까?

출력

정답은 a는 10으로 변경되지만, b의 값은 변경되지 않는다. 그렇다면, 가리키는 메모리 주소가 다르다는 것이다. Go에서는 C와 같이 메모리 주소를 보기 위해서는 &를 변수명 앞에 사용하면 된다. 

테스트

func main() {
	a := 2
	b := a
	a = 10
	fmt.Println(&a, &b)
}

출력

예상과 같이 주소값이 다르게 나온다. 근데, 이상한 점이 있다. 왜.. 2바이트 차이가 나는거지...? 내 예상대로라면 4바이트 차이가 나야한다. size를 확인해보면 8로 나온다. os가 64bit이기 때문에 int64를 사용하는 것 같은데, 왜 2바이트 차이가 나는걸까..? 

출력

같은 코드로 여러번 돌려보면 2차이가 있을 때도, 8차이가 있을 때도 존재한다. a의 크기는 8바이트이기 때문에 8차이가 나야되는게 맞는 것 같은데 이상하다. 시스템적인 문제인 것 같다. a의 값으로 9223372036854775807를 넣어줘도 같은 형상이 나타나기 때문이다. 일단, 넘어가자.

 

Pointer를 쓰고싶다면 c언어와 같은 방식으로 사용하면 된다. 변수에 주소값을 넣어주는 것이다.

테스트

func main() {
	a := 2
	b := &a
	a = 10
	fmt.Println(&a, b)
}

출력

a가 가리키는 주소값과 b의 값이 일치하는 것을 볼 수 있다. 값을 확인하기 위해서는 c언어와 같은 방식으로 사용하면된다.

테스트

func main() {
	a := 2
	b := &a
	a = 10
	fmt.Println(a, *b)
}

출력

b는 a주소를 가리키고 있기때문에, a의 값을 변경하면 b도 같이 바뀌는 것을 확인할 수 있다. 물론, b를 이용해서 값을 변경할 수도 있다.

테스트

func main() {
	a := 2
	b := &a
	*b = 10
	fmt.Println(a, *b)
}

출력

 

Pointer의 경우 C와 매우 같은 구조를 가지고있는 것을 볼 수 있다.

 

 

'Go' 카테고리의 다른 글

Maps  (0) 2023.12.30
Arrays/Slices  (0) 2023.12.29
If/switch  (0) 2023.12.28
for, range, args  (0) 2023.12.28
Functions  (0) 2023.12.28

+ Recent posts