Go

[Go] Go language 스터디 내용 정리

Razelo 2022. 5. 8. 20:07

올해 초 겨울방학에 프로젝트에 지쳐서 숨돌릴겸 Rust와 Go 언어에 대해서 잠시 알아볼 시간을 가졌는데 그중 Go 언어는 노마드 코더님의 무료 강의를 보면서 notion에 내용을 정리했다. 4개월 정도 이전에 했던 스터디 내용이라 강의가 업데이트해서 강의내용이랑은 다른 내용이 있을 수도 있지만 내용 자체는 기본기 다지기에 좋을것 같아서 다시 보면 좋을 듯 싶어 업로드했다.  


go 언어란 무엇일까?

  1. 09년 구글의 로버트 그리즈머, 홉 파이크, 켄 톰슨이 개발했다.
  2. 고 루틴이 있다. → 멀티스레드 매커니즘이지만 자체적인 스케줄러에 의해 관리되는 경량 스레드이고 os에서 관리하는 경량 스레드보다 더 경량이다.
  3. 일급객체로 정수와 실수와 같은 데이터 타입과 동급으로 취급한다.
  4. channel을 통해 동시성을 지원한다.
  5. 멀티 패러다임 언어이다. (근데 함수형 언어라고 하기에는 함수의 순수성과 불변성을 보장하진 않는다. )

장점을 알아볼까요?

  1. 문법이 매우 간단하다!
  2. loop문법에 while이 없고 for만 있다.
  3. class 문법도 없다.
  4. 정적타입 / 강타입 언어이다.
  5. 컴파일 속도가 빠른 컴파일 언어이다. 그런데? 컴파일 속도가 빨라서 인터프리터 언어처럼 쓸 수 있다.
  6. 속도가 굉장히 빠르다.
  7. 고루틴이 있다. 고루틴은 쓰레드보다 매우 가볍고 쉽게 이용할 수 있다.
  8. 기본 모듈이 매우 풍부하다. 그래서 별도의 웹 프레임워크를 사용하지 않아도 웹을 개발할 수 있다. 파이썬으로 순수 웹개발을 못하고 플라스크 장고를 써야만 하는 것에 비교된다.
  9. 신뢰할 수 있는 대기업이 제작했고 사용한다. 구글이 만들었고 유튜브 개발에 사용한다.
  10. 컨벤션이 통일되어있다. 컨벤션에 맞춰서 코드를 수정해주는 go fmt 기능이 있다.
  11. 채널 기반 언어다. 스레드라는 것을 명시적으로 주지 않고 고루틴을 생성하면 알아서 스레드를 생성해주고 적절한 스레드에 고루틴을 할당한다. 고루틴 사이의 커뮤니케이션을 전부 채널을 통해서 한다면 동기화 문제를 신경쓰지 않아도 된다.
  12. 네이티브 바이너리가 나온다. 결과를 배포할때 네이티브 바이너리가 나온다는 것은 매우 큰 장점이다. 요즘 서버에는 대부분 파이썬이나 JVM이 설치되어있지만 배포는 역시 네이티브 바이너리로 하는 것이 가장 편하다. 그런데! 바이트 코드를 생성하는 언어가 아니기 때문에 바이너리만 배포하는 경우 (C나 C++처럼) 타켓 머신에 맞춰서 각각 컴파일 해야 한다.
  13. 인터페이스 기반의 다형성 → GO에서 폴리모픽한 코드를 작성하는 유일한 방법은 인터페이스를 인자로 넘기는 것이다. 이 덕분에 인터페이스 단위의 추상화가 자연스럽게 이루어진다.

단점은요?

  1. 없는게 너무 많다. 제너릭없음, 클래스문법없음, 예외처리문법없음, public , private키워드 없음, this없음
  2. 예외처리문법이 없어서 if가 많이 사용되어 지저분한 코드가 생긴다.
  3. 좋은 ide가 없다. vscode말고 별다른게 없다.
  4. verbose한 코드 → go에는 템플릿이나 매크로도 없다. 이 덕에 go 코드가 단순해지지만, 단순한 코드가 반복되서 등장한다.

<기본적인 숙지 사항>

컴파일러가 찾는 순서: main package -> main function main은 컴파일을 위한거다. 컴파일안할거면 메인 필요없음. go에서는 export하고 싶으면 upper case로 시작한다.그래서 go에서는 대문자로 작성된 function들을 많이 볼 수 있다. 예시: fmt.Println 변수 선언: var name string = "nico" 상수 선언: const name string = "nico" 함수 안이라면 : name := "nico" 이렇게도 가능함. go가 type을 찾아줄거임.

function 밖에서는 var name bool = false 이렇게만 가능. 즉 축약형은 function 안에서만 가능함.

go 에서는 function 이 여러개의 value 를 return 할 수 있음. 아래 코드를 보자.

package main

import (
	"fmt"
	"strings"
)

func multiply(a, b int) int { // 하나의 int 타입을 리턴하겠다는 뜻
	return a * b
}

func lenAndUpper(name string) (int, string) {  // 지금 보면 여러개를 리턴하고 있다. 
	return len(name), strings.ToUpper(name)
}

func main() {
	fmt.Println(multiply(2, 2))
	totalLength, upperName := lenAndUpper("nico") // 두 변수에 리턴되는 두 값을 할당 가능 
	fmt.Println(totalLength, upperName)
}

그런데 위 코드에서 만약 하나만 리턴받고 싶다면?

totalLength , _ := lenAndUpper(”nico”) 이렇게 작성하면 됨. underscore는 그냥 값을 무시하는 거임.

func repeatMe(words ...string) { // 점 3개? ...? 여러개가 온다는 뜻. 파이썬에서 본적있음. 
	fmt.Println(words)
}

func main() {
	repeatMe("nico", "lynn", "dal", "marl", "flynn")
}

... 을 써주면 여러개를 받을 수 있다.

naked return이란 무엇일까?

명시적으로 return해서 반환하는 거 없이 그냥 쓸 수 있다. 왜냐? 함수 정의에서 이미 변수 생성했기 때문에! body에서는 그냥 그거 업데이트만 하고 return 만 딱 쓰는거다. body에서 하는게 변수 생성이 아니라 그냥 함수 정의에서 생긴 변수 업데이트한다고 보면 된다. 즉 := 가 아니라 = 를 쓰는걸 주의 깊게 봐라.

// naked return 
func lenAndUpper(name string) (length int, uppercase string) {
	length = len(name) // length 는 이미 function 정의에 생성된거다. 그래서 여기선 할당을 하는거다. 그러니 := 로 생성을 해서는 안된다. 
	uppercase = strings.ToUpper(name) // 대문자로 만들어주자. 
	return 
}

func main() {
	totalLength , _ := lenAndUpper("nico")
	fmt.Println(totalLength) 
}

defer이란 무엇일까? 다른 언어에서는 본적 없는데 굉장히 매력적인 기능이다.

함수가 끝난뒤? 뭘 할지 정의해준다. 유용하게 쓰일 수 있을 듯 싶다.

// defer
func lenAndUpper(name string) (length int, uppercase string) {
	defer fmt.Println("finish") // return 뒤에 실행시킨다.
	length = len(name)
	uppercase = strings.ToUpper(name)
	return
}

// defer
// function이 끝나고도 뭔가를 하고 싶을때! 쓴다.

func main() {
	totalLength, up := lenAndUpper("nico")
	fmt.Println(totalLength, up)
}

함수가 실행되고 난 뒤에 작동된다? 매력적이네. 결과보면 finish 찍히는거 확인할 수 있다.

loop ?

고에서는 for 문만 가능하다. foreach, map, for in, for of 뭐 이런거 없다. 그냥 for만 있음.

func superAdd(numbers ...int) int {
	//range? range는 array에 loop을 적용시켜준다.
	for index, number := range numbers {
		fmt.Println(index, number) // 결과로 0부터 시작된다? 왜냐? range는 index를 주기 때문이다. 그러니 숫자도 같이 출력해보자.
	}
	return 1 // 이건 그냥 써준거다. return을 해야하긴하니까. 
}

func main() {
	superAdd(1, 2, 3, 4, 5, 6, 7, 8)
}

위 코드 처럼 쓸 수도 있지만? 아래처럼도 쓸 수 있다.

func superAdd(numbers ...int) int {
	for i := 0; i < len(numbers); i++ {
		fmt.Println(numbers[i])
	}
	return 1
}

또한 아래처럼 쓸 수 도 있음. 다 합산해서 리턴해주자.

func superAdd(numbers ...int) int {
	total := 0
	for _, number := range numbers {
		total += number
	}
	return total
}

func main() {
	result := superAdd(1, 2, 3, 4, 5, 6, 7, 8)
	fmt.Println(result)
}

if and else 에 대해서 알아봅시다.

func canIDrink(age int) bool {

	if koreanAge := age + 2; koreanAge < 18 {
		return false
	}
	return true
}

func main() {
	fmt.Println(canIDrink(16))
}

// variable expression? variable을 if를 쓰는 순간에 생성할 수 있게 해준다.
// if 조건에서만 쓰기 위해서 생성했구나 라는걸 확실하게 알 수 있다.

위에 보면 variable expression이란걸 쓰고 있다. variable을 if 조건안에서만 쓰고 다른데서는 안쓰겠다는걸 확실히 보여주기 위해 쓰는것같다. 가독성때문에 쓰이는듯

switch? 아래와 같이 쓴다. 굉장히 심플하다.

func canIDrink(age int) bool {
	switch age {
	case 10:
		return false
	case 18:
		return true
	}
	return false
}

switch? 좀더 살펴보자.

// func canIDrink(age int) bool {
// 	switch {
// 	case age < 18:
// 		return false
// 	case age == 18:
// 		return true
// 	case age > 50:
// 		return false
// 	}
// 	return false
// }
func canIDrink(age int) bool {
	switch koreanAge := age + 2; koreanAge {
	case 10:
		return false
	case 18:
		return true
	}
	return false
}

두 방식이 보일거다. 첫번째 주석문을 보면 switch인데 인자가 없는걸 볼 수 있을거다. 그냥 age를 비교하는거다. 이렇게도 가능하다. 그리고 아까 if else에서 variable expression이라고 해서 조건 내에서 선언되는 변수가 있었지? 그것도 switch에서 가능하다.

지금까지

naked return

defer

range

if else 이런것들을 살펴봤다. 개인적으로 이중에서 defer가 굉장히 유용할것 같은 생각이 든다.

이제 go 에만 있는 특유한 것들을 살펴보자.

포인터? c에도 있긴한데 어쨋든 보자. (하이레벨이라고 생각했는데 초반에 rust랑 비교됬던걸 보면 역시나 포인터가 포함되어있기에 비교됬던것 같다. 포인터가 있을줄은 몰랐다. )

func main() {
	a := 2
	b := 5
	fmt.Println(&a, &b)
	// 0xc000012088 0xc0000120a0
}

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

func main() {
	a := 2
	b := &a
	fmt.Println(*b)
	// 2
} 
// 값을 복사시키는게 아니라 메모리에 저장된 object를 서로 똑같이 가지고 싶도록 원할때 쓰면 된다.
// 아주 무거운 데이터 구조를 다루고 있다면 이렇게 하는게 좋다. 계속 복사본을 만들면 별로 좋지 않다. 

func main() {
	a := 2
	b := &a
	a = 5
	fmt.Println(*b)
	// 5
}
// 포인터 알면 여기도 왜 그런지 알겠지? 

 func main() {
	a := 2
	b := &a
	*b = 20
	fmt.Println(a)
	// 20
}
// b는 a를 살펴보는 포인터이기 때문에 이렇게 변경가능한거다.
// go 에서도 로우 레벨 프로그래밍이 가능하다. 

arrays and slices? 형태가 좀 생소할 수 있으니 잘 봐두자.

go 에서는 array가 좀 다르다. slice를 많이 사용하게 될거다.

func main() {
	names := [5]string{"nico", "lynn", "dal"} // 0 1 2 
	names[3] = "alala"
	names[4] = "alala"
  // names[5] = "alala" <- 얘는 오류가 나겠지? 배열의 크기를 넘어서니까!  
	fmt.Println(names)
}

// 이게 슬라이스다. 길이를 선언하지 않고 그냥 쓰는거다. 
func main() {
	names := []string{"nico", "lynn", "dal"} // 0 1 2
	names = append(names, "flynn") // append는 names를 수정해주는게 아니다. 다만 값이 추가된 새로운 slice를 반환한다. 
	fmt.Println(names)
}
// slice는 기본적으로 array인데 length 가 없다. 이게 전부다. 
// 그냥 length 없이 쓰기 때문에 원하면 요소를 더 추가할 수  있다. 이부분은 추가 설명이 없었지만
// 아마 새로운 slice를 반환한다는것을 보면 대충은 요소크기를 짐작하고 쓰는게 나을 것 같다. 
// 어느 정도 오버헤드가 발생하지 않을까 싶다. 

maps 이라는건 뭘까요? 여기도 형태가 생소하니 잘 봐두자.

func main() {
	nico := map[string]string{"name": "nico", "age": "12"}
	fmt.Println(nico)
}
// key value 쌍이다. 
// map[string]string 에서 []안이 key타입, 밖의 string이 value타입이다.  

func main() {
	nico := map[string]string{"name": "nico", "age": "12"}
	for key, value := range nico {
		fmt.Println(key, value)
	}
}

go 에도 struct가 있다고 ? 앞으로 생겨날 언어에는 struct를 기본으로 가지고 있게될건가? rust에도 struct가 있던데

// 여기 보면 신기한게 콤마로 구분하지 않는다. 그냥 쓴다. 
type person struct {
	name    string
	age     int
	favFood []string // slice of string
}

func main() {
	favFood := []string{"kimchi", "ramed"}
	nico := person{"nico", 18, favFood}
	fmt.Println(nico.name)
}

// 그런데 위의 방식보다 더 좋은 방식이 존재한다. 
type person struct {
	name    string
	age     int
	favFood []string // slice of string
}

func main() {
	favFood := []string{"kimchi", "ramed"}
	nico := person{name: "nico", age: 18, favFood: favFood}
	fmt.Println(nico.name)
}

// 아래가 좀더 깔끔해 보인다. 
// 그런데 위 방식과 아래 방식을 혼합해서는 쓸 수 없다. 
// 쓸거면 기왕이면 아래 방식을 쓰자. 

go 는 class 가 없다.

python 이나 자스의 object 같은 것도 없다.

go에는 constructor method가 없다. 파이썬에는 init 이 있다.

그런데 고에는? 이런게 없다. struct에는 constructor가 없다.

go 에서 struct는 매우 중요하다. 거의 모든게 struct로부터 나온다.

여타 프로그래밍 언어와 좀 다르다.

참고! go에서 소문자는 private을 의미하고 대문자는 public을 의미한다.

go에서는 constructor가 없어서 흔한 패턴이 쓰이는데 바로 다른 function을 만들어서 그걸 constructor 처럼 쓴다.

go의 마법? channel과 go routine. 이것도 프로젝트 해보면서 공부해보자.

프로젝트 1 - Bank Account 구현

main.go

package main

import (
	"fmt"

	"github.com/yuny0623/learngo/accounts"
)

func main() {
	account := accounts.NewAccount("nico")
	account.Deposit(10)
	//err := account.Withdraw(20) // 에러를 강제하는 이러한 방식이 더 좋을 수도 있다.
	//if err != nil {
	//log.Fatalln(err) // 프로그램을 종료시켜준다. Println을 하고 종료시킨다.
	//fmt.Println(err)
	//}
	fmt.Println(account)
}

// 소문자가 private이고 대문자가 public이다.

accounts/accounts.go

package accounts

import (
	"errors"
	"fmt"
)

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

var errNoMoney = errors.New("Can't withdraw") // 에러를 변수에 저장할 수 있는데 이렇게 하려면 errFOO와 같은 네이밍을 줘라.

// NewAccount creates Account
func NewAccount(owner string) *Account {
	account := Account{owner: owner, balance: 0}
	return &account // 새로운 object를 만들어서 리턴한다.
} // go의 흔한 패턴, constructor가 없기에 function으로 만든다.

// Deposit x amount on your account
func (a *Account) Deposit(amount int) { // 이게 메소드다. 이렇게 함으로써 Account가 Deposit이라는 메소드를 갖게 되었다.(a Account)를 Receiver라고 부른다. receiver작성 규칙은 항상 struct의 첫글자를 따서지어야 한다는거다. 그래서 지금 a가 있는거다.
	a.balance += amount
	// (a *Account) 가 있지? 복사본을 받아오는게 아니라 실제로 Deposit을 호출한 Account를 사용하라는 뜻이다.
	// 이걸 pointer Receiver라고 말한다.
}

// Balance of your acount
func (a Account) Balance() int {
	return a.balance
}

// Withdraw x amount from your account
func (a *Account) Withdraw(amount int) error {

	if a.balance < amount {
		return errNoMoney
	}
	a.balance -= amount
	return nil // error에는 두개의 상태가 있다. null과 같은 nil과 if문에서 리턴시키는 error가 있다.
}

// go에는 try except 뭐 이런건 없다. catch도 없다. error를 그냥 return해서 거기서 처리해야 한다.
// error를 다룰 코드를 우리가 직접 만들어야 한다.

// ChangeOwner of the acount
func (a *Account) ChangeOwner(newOwner string) {
	a.owner = newOwner
}

// Owner of the account
func (a Account) Owner() string {
	return a.owner
}

func (a Account) String() string { // account를 그냥 출력시키려고 하면 이런 String같은 메소드들이 자동으로 출력된다. 파이썬의 class를 print할경우 __str__출력과 비슷함.
	return fmt.Sprint(a.Owner(), "'s account. \\nHas: ", a.Balance())
}

// method를 struct에만 추가할 수 있는게 아니다. type에도 추가가능하다? 호오

프로젝트2 - Dictionary 구현

main.go

package main

import (
	"fmt"

	"github.com/yuny0623/learngo/mydict"
)

func main() {
	dictionary := mydict.Dictionary{}
	baseWord := "hello"
	dictionary.Add(baseWord, "First")
	dictionary.Search(baseWord)
	dictionary.Delete(baseWord)
	word, err := dictionary.Search(baseWord)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(word)
}

mydict/mydict.go

package mydict

import "errors"

// Dictionary type
type Dictionary map[string]string // 이건 struct가 아니다. 그냥 map[string]string에 대해서 alias를 만들어 줄 수 있는거다.

var ( // 이런 식으로 한번에 선언하는 것도 가능하다.
	errNotFound   = errors.New("Not Found")
	errCantUpdate = errors.New("Cant update non-existing word")
	errWordExists = errors.New("That word already exists")
)

// type에 method를 추가할 수 있다는 장점이 있다.

// Search for a word
func (d Dictionary) Search(word string) (string, error) {
	value, exists := d[word] // go의 굉장히 편리한 기능중 하나이다. word가 있는지 찾으면 그게 두개 값을 리턴을 하고 value와 존재여부를 리턴해준다.
	if exists {
		return value, nil
	}
	return "", errNotFound
}

// Add a word to the dictionary
func (d Dictionary) Add(word, def string) error {
	_, err := d.Search(word)
	switch err {
	case errNotFound:
		d[word] = def
	case nil:
		return errWordExists
	}
	return nil
}

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

// Delete a word
func (d Dictionary) Delete(word string) {
	delete(d, word) // 이건 기본으로 제공되는 함수이다.
}

프로젝트3 - URL CHECKER & GO ROUTINES

main.go - 고루틴없이 작성함. 이후에 최적화해봅시다.

package main

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

var errRequestFailed = errors.New("Request Failed")

func main() {
	//var results = map[string]string{} // 이렇게 만들면 empty map을 만들 수 있다.
	var results = make(map[string]string) // 위의 구문과 동일한 기능을 한다. empty map
	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>",
	}

	for _, url := range urls {
		result := "OK"
		err := hitURL(url)
		if err != nil { // 에러가 있다면?
			result = "FAILED"
		}
		results[url] = result
	}
	for url, result := range results {
		fmt.Println(url, result)
	}
}

func hitURL(url string) error {
	fmt.Println("Chekcing:", url)
	resp, err := http.Get(url) //go에서 제공하는 표준
	if err != nil || resp.StatusCode >= 400 {
		fmt.Println(err, resp.StatusCode)
		return errRequestFailed
	}
	return nil
}

// 패닉이란? 컴파일러가 찾아내지 못하는 에러
// 왜 go 로 이 작업을 하는걸까? 파이썬에서도 할 수 있는데? 동시에 한방에 빠르게 해버리기 위해서이다.
// url들을 동시에 처리해보자.

go routines은 뭘까요?

package main

import (
	"fmt"
	"time"
)

func main() {
	go sexyCount("nico")
	go sexyCount("flynn")
	time.Sleep(time.Second * 5) // 이렇게 하면 5초만 출력하다가 그냥 종료된다. 왜냐? 메인이 5초 살아있다가 바로 죽기 때문이다. 
}

func sexyCount(person string) {
	for i := 0; i < 10; i++ {
		fmt.Println(person, "is sexy", i)
		time.Sleep(time.Second) // go 표준 패키지
	}
}

// go 에서 최적화란? 동시에 처리!
// 고루틴은 기본적으로 다른 함수와 동시에 실행시키는 함수다.
// 고루틴은 프로그램이 작동되는 동안만 유효하다.! 메인함수가 실해오디는 동안만!
// 위 구문에 두개의 go 구문이 있으면? go 두개 하고 메인이 끝나버린다. 그러니 고루틴도 다 끝나버리는거다./ 메인 함수가 고루틴을 기다려주지 않는다.
// 메인 끝나면 고루틴도 그냥 끝나는거다.

channels은 어떻게 쓸까?

package main

import (
	"fmt"
	"time"
)

func main() {
	c := make(chan bool) // 채널을 만들고 리턴타입을 적어주자. // 지금 하나의 채널을 만들어서 두개의 함수에 이어서 쓰고 있는거다.
	people := [2]string{"nico", "flynn"}
	for _, person := range people {
		go isSexy(person, c) // 채널을 함수에 보내주고 있다. 지금 isSexy nico, isSexy flynn 즉 두개의 함수로 채널을 보내주고 있다.
	}
	//result := <-c       // 그 채널의 메시지를 result로 받는거다.
	//fmt.Println(result) // 채널로부터 뭔가를 받을때는 , 메인은 그걸 받을때까지 기다린다.

	fmt.Println(<-c)
	fmt.Println(<-c)
	fmt.Println(<-c) // 이건 에러가 발생한다. 데드락이 발생한다. 왜냐면 지금 메시지를 기다리고 있는데 실제로 만든 고루틴은 끝나버렸기 때문이다. 즉 두개를 만들었는데 세개를 기다릴 순 없는거다.
}

func isSexy(person string, c chan bool) {
	time.Sleep(time.Second * 5)
	fmt.Println(person)
	c <- true // 이렇게 true를 보낼 수 있게 된다.
}

// 채널은 고루틴이랑 메인 함수 사이에 정보를 주고받기 위한 방법이다.
// 고루틴끼리도 가능하다. 어떻게 통신할까?
// 채널은 파이프같은거다. 주고받을 수 있는거다.
package main

import (
	"fmt"
	"time"
)

func main() {
	c := make(chan string)
	people := [5]string{"nico", "flynn", "dal", "japanguy", "larry"}
	for _, person := range people {
		go isSexy(person, c)
	}
	//fmt.Println("Wating for messages")
	//fmt.Println("Received this message: ", <-c) // 이걸보면 메인은 멈춘다. 왜냐? 이건 blocking operation이기 때문이다. 이건 우리가 채널로부터 뭔가를 얻고 있다는 소리다.
	//fmt.Println("Received this message: ", <-c) // 여기서도 기다린다. 이전 구문에서 얻으면 여기로 넘어오는거다.그 다음에는? 밑에 줄로 가서 메인이 끝난다.

	// fmt.Println("Wating for messages")
	// resultOne := <-c
	// resultTwo := <-c
	// fmt.Println("Received this message: ", resultOne)
	// fmt.Println("Received this message: ", resultTwo)

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

func isSexy(person string, c chan string) { // 채널의 타입은 chan 이다. 그리고 채널을 통해서 어떤 타입의 데이터를 주고받을지 알려준다. -> string
	time.Sleep(time.Second * 5)
	c <- person + " is sexy"
}

// waiting for a message? -> block operation

waiting for a message? -> block operation 메시지를 받는 것은 blocking operation이다. 명심!

  1. 메인이 죽으면 고루틴도 죽는다 무조건!
  2. 채널로 주고받을 데이터에 대해서 타입을 받을건지 구체적으로 지정해줘야 한다.
  3. 메세지를 채널로 보내는 방법은 <- 를 쓴다.
  4. 메시지를 받을 곳이 없어도 메시지를 보낼 수 있다. blocking operation이란 메인 함수가 뭔가를 받기까지 동작을 멈춘다는거다.

고루틴을 적용한 결과

main.go

package main

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

type requestResult struct {
	url    string
	status string
}

var errRequestFailed = errors.New("Request Failed")

func main() {
	results := make(map[string]string)
	c := make(chan requestResult)
	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>",
	}
	for _, url := range urls {
		go hitURL(url, c)
	}

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

	for url, status := range results {
		fmt.Println(url, status)
	}
}

func hitURL(url string, c chan<- requestResult) { // 여기 추가된 arrow는 send only라는 뜻이다.이렇게 명시적으로 해주는거다.
	resp, err := http.Get(url) //go에서 제공하는 표준
	status := "OK"
	if err != nil || resp.StatusCode >= 400 {
		status = "FAILED"
	}
	c <- requestResult{url: url, status: status} // 채널로 우리의 struct를 보내자.
}

// 이 채널은 데이터를 받을 수만 있고 보낼 수는 없어! 이런 식으로 코드상에서 명시해주는게 더 좋다.
// 최적화가 끝났다. 이제 체크하는데 가장 오래 걸리는 url 하나의 시간이 프로그램의 시간이다.

프로젝트4 - JOB scrapper

main.go

package main

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

	"github.com/PuerkitoBio/goquery"
)

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

var baseURL string = "<https://kr.indeed.com/jobs?q=python&limit=50>"

func main() {
	var jobs []extractedJob
	c := make(chan []extractedJob)
	totalPages := getPages()

	for i := 0; i < totalPages; i++ {
		go getPage(i, c)
	}

	for i := 0; i < totalPages; i++ {
		extractedJobs := <-c
		jobs = append(jobs, extractedJobs...)
	}

	writeJobs(jobs)
	fmt.Println("Done, extracted", len(jobs))
}

func getPage(page int, mainC chan<- []extractedJob) { // send only
	var jobs []extractedJob
	c := make(chan extractedJob)

	pageUrl := baseURL + "&start=" + strconv.Itoa(page*50) // 숫자를 string으로 바꾸도록 strconv를 사용해보자.
	fmt.Println("Requesting", pageUrl)

	res, err := http.Get(pageUrl)
	checkErr(err)
	checkCode(res)

	defer res.Body.Close()

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

	searchCards := doc.Find(".tapItem")

	searchCards.Each(func(i int, card *goquery.Selection) {
		go extractJob(card, c)
	})

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

	mainC <- jobs
}

func extractJob(card *goquery.Selection, c chan<- extractedJob) {
	id, _ := card.Attr("data-jk")
	title := cleanString(card.Find("h2>span").Text())
	location := cleanString(card.Find(".companyLocation").Text())
	salary := cleanString(card.Find(".salary-snippet").Text())
	summary := cleanString(card.Find(".job-snippet").Text())
	c <- extractedJob{
		id:       id,
		title:    title,
		location: location,
		salary:   salary,
		summary:  summary}
}

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

	defer res.Body.Close()                              // 끝나면 닫아야 한다. 그래야 메모리가 새지 않음,
	doc, err := goquery.NewDocumentFromReader(res.Body) // res.Body는 byte인데 입력출력 IO이다. 그래서 위에서 defer로 닫아야 한다.

	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 checkCode(res *http.Response) {
	if res.StatusCode != 200 {
		log.Fatalln("Request failed with Status:", res.StatusCode)
	}
}

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

func writeJobs(jobs []extractedJob) {
	file, err := os.Create("jobs.csv")
	checkErr(err)
	w := csv.NewWriter(file)
	defer w.Flush() // flush는 함수가 끝나는 시점에 파일에 데이터를 입력하는 함수이다.

	headers := []string{"Link", "Title", "Location", "Salary", "Summary"}
	wErr := w.Write(headers)
	checkErr(wErr)

	for _, job := range jobs {
		jobSlice := []string{"<https://kr.indeed.com/jobs?q=>" + job.id, job.title, job.location, job.salary, job.summary}
		jwErr := w.Write(jobSlice)
		checkErr(jwErr)
	}
} // Writer를 생성하고 writer에 데이터를 입력하고, 모든 데이터를 파일에 저장하는거다.

file 쓰기에서 오버헤드가 좀 있는것 같다. 파일쓰기도 go루틴으로 바꿔볼 수 있을까?


프로젝트5 - Web server with echo

main.go

package main

import (
	"os"
	"strings"

	"github.com/labstack/echo"
	"github.com/yuny0623/learngo/scrapper"
)

const fileName string = "jobs.csv"

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

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

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

scrapper/scrapper.go

package scrapper

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

	"github.com/PuerkitoBio/goquery"
)

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

// Scrape Indeed by a term
func Scrape(term string) {
	var baseURL string = "<https://kr.indeed.com/jobs?q=>" + term + "&limit=50"

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

	for i := 0; i < totalPages; i++ {
		go getPage(i, baseURL, c)
	}

	for i := 0; i < totalPages; i++ {
		extractedJobs := <-c
		jobs = append(jobs, extractedJobs...)
	}

	writeJobs(jobs)
	fmt.Println("Done, extracted", len(jobs))
}

func getPage(page int, url string, mainC chan<- []extractedJob) { // send only
	var jobs []extractedJob
	c := make(chan extractedJob)

	pageUrl := url + "&start=" + strconv.Itoa(page*50) // 숫자를 string으로 바꾸도록 strconv를 사용해보자.
	fmt.Println("Requesting", pageUrl)

	res, err := http.Get(pageUrl)
	checkErr(err)
	checkCode(res)

	defer res.Body.Close()

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

	searchCards := doc.Find(".tapItem")

	searchCards.Each(func(i int, card *goquery.Selection) {
		go extractJob(card, c)
	})

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

	mainC <- jobs
}

func extractJob(card *goquery.Selection, c chan<- extractedJob) {
	id, _ := card.Attr("data-jk")
	title := CleanString(card.Find("h2>span").Text())
	location := CleanString(card.Find(".companyLocation").Text())
	salary := CleanString(card.Find(".salary-snippet").Text())
	summary := CleanString(card.Find(".job-snippet").Text())
	c <- extractedJob{
		id:       id,
		title:    title,
		location: location,
		salary:   salary,
		summary:  summary}
}

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

	defer res.Body.Close()                              // 끝나면 닫아야 한다. 그래야 메모리가 새지 않음,
	doc, err := goquery.NewDocumentFromReader(res.Body) // res.Body는 byte인데 입력출력 IO이다. 그래서 위에서 defer로 닫아야 한다.

	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 checkCode(res *http.Response) {
	if res.StatusCode != 200 {
		log.Fatalln("Request failed with Status:", res.StatusCode)
	}
}

// CleanString cleans a string
func CleanString(str string) string {
	return strings.Join(strings.Fields(strings.TrimSpace(str)), " ")
}

func writeJobs(jobs []extractedJob) {
	file, err := os.Create("jobs.csv")
	checkErr(err)
	w := csv.NewWriter(file)
	defer w.Flush() // flush는 함수가 끝나는 시점에 파일에 데이터를 입력하는 함수이다.

	headers := []string{"Link", "Title", "Location", "Salary", "Summary"}
	wErr := w.Write(headers)
	checkErr(wErr)

	for _, job := range jobs {
		jobSlice := []string{"<https://kr.indeed.com/jobs?q=>" + job.id, job.title, job.location, job.salary, job.summary}
		jwErr := w.Write(jobSlice)
		checkErr(jwErr)
	}
} // Writer를 생성하고 writer에 데이터를 입력하고, 모든 데이터를 파일에 저장하는거다.

home.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>Indeed.com scrapper</h3>
    <form method="POST" action="/scrape">
      <input placeholder="what job do u want" name="term" />
      <button>Search</button>
    </form>
  </body>
</html>

 

추신:

위 내용들은 노마드 코더님의 무료 강의를 스터디하며 정리한 내용임. 감사합니다. 노마드 코더님 

반응형