728x90

요약

 - 컨텍스트 : 고루틴에 작업을 요청할 때 작업 취소나 작업 시간 등을 설정할 수 있는 작업 명세서 역할

 

새로운 고루틴으로 작업을 시작할 때 일정 시간 동안만 작업을 지시하거나 외부에서 작업을 취소할 때 사용한다

또한 작업 설정에 관한 데이터를 전달할 수도 있다

 

작업 취소가 가능한 컨텍스트

// 작업이 취소될 때까지 1초마다 메시지를 출력하는 고루틴
package main

import (
	"context"
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

func main() {
	wg.Add(1)
	ctx, cancel := context.WithCancel(context.Background()) // 1. 컨텍스트 생성
	go PrintEverySecond(ctx)
	time.Sleep(5 * time.Second)
	cancel()

	wg.Wait()
}

func PrintEverySecond(ctx context.Context) {
	tick := time.Tick(time.Second)

	for {
		select {
		case <-ctx.Done():
			wg.Done()
			return
		case <-tick:
			fmt.Println("Tick")
		}
	}
}

// Tick 5번 출력

 

작업 시간을 설정한 컨텍스트

// 일정 시간동안만 작업을 지시할 수 있는 컨텍스트
package main

import (
	"context"
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

func main() {
	wg.Add(1)
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) // 1. 컨텍스트 생성
	go PrintEverySecond(ctx)
	time.Sleep(5 * time.Second)
	cancel()

	wg.Wait()
}

func PrintEverySecond(ctx context.Context) {
	tick := time.Tick(time.Second)

	for {
		select {
		case <-ctx.Done():
			wg.Done()
			return
		case <-tick:
			fmt.Println("Tick")
		}
	}
}

// Tick 3번 출력

 

특정 값을 설정한 컨텍스트

// 컨텍스트에 특정 키로 값을 읽어올 수 있도록 설정할 수 있다
package main

import (
	"context"
	"fmt"
	"sync"
)

var wg sync.WaitGroup

func main() {
	wg.Add(1)
	ctx := context.WithValue(context.Background(), "number", 9) // 1. 컨텍스트 생성 (값 추가)
	ctx = context.WithValue(ctx, "float", 5.0)
	ctx = context.WithValue(ctx, "keyword", "Lilly")
	go print(ctx)

	wg.Wait()
}

func print(ctx context.Context) {
	if v := ctx.Value("number"); v != nil { // 2. 컨텍스트에서 값 읽기
		n := v.(int)
		fmt.Println("number:", n*n)
	}

	if v := ctx.Value("float"); v != nil { // 3. 컨텍스트에서 값 읽기
		n := v.(float64)
		fmt.Println("float:", n*n)
	}

	if v := ctx.Value("keyword"); v != nil { // 4. 컨텍스트에서 값 읽기
		n := v.(string)
		fmt.Println("keyword:", n)
	}

	wg.Done()
}

number: 81
float: 25
keyword: Lilly

 

책 참조 : Tucker의 Go언어 프로그래밍

728x90

'Basic Programming > Golang' 카테고리의 다른 글

Golang - 채널 / 셀렉트  (1) 2023.09.16
Golang - 슬라이스 정렬  (1) 2023.07.13
Golang - 슬라이스 요소 삭제/추가  (0) 2023.07.13
Golang - 슬라이스 복제  (0) 2023.07.13
Golang - Slicing  (0) 2023.07.13
728x90

요약

- 채널 : 고루틴 끼리 메시지를 전달하는 메시지 큐

- 셀렉트 : 여러개의 채널을 동시에 기다리는 구문

 

메시지 큐

 - 윈도우 프로그래밍을 하면서 경험했던 윈도우 메시지 큐와 동일한 개념이다

 - 메시지(다양한 정보)를 Queue에 순차적으로 쌓이게 되고, 순차적으로 처리된다

 

채널 인스턴스 생성

                           ┏ 채널 타입               ┏ 메시지 타입
var messages chan string = make(chan string)
       ┗ 채널 인스턴스 변수                  ┗ 채널 키워드

 - make() 를 통해 생성한다

 - 채널 타입은 channel을 의미하는 chan과 메시지 타입을 합쳐서 표현한다

 - chan string은 string 타입 메시지를 전달하는 채널의 타입이다

 

채널에 데이터 넣기

   ┏ 채널 인스턴스   ┏ 넣을 데이터
messages <- "This is a message"
                ┗ 연산자

 

채널에서 데이터 빼기

       ┏ 빼낸 데이터를 담을 변수 ┏ 채널 인스턴스
var msg string <- messages   ┛
                     ┗ 연산자

 

채널에서 데이터를 하나 넣고 빼는 예제

package main
import (
    "fmt"
    "sync"
    "time"
)

func main()
{
    var wg sync.WaitGroup
    ch := make(chan int)    // 채널 생성
    
    wg.Add(1)
    go square(&wg, ch)      // 고루틴 생성
    ch <- 9                 // 채널에 데이터 넣음
    wg.Wait()               // 작업이 완료되때까지 대기
}

func square(wg *sync.WaitGroup, ch chan int)
{
    n := <- ch              // 데이터를 빼옴
                            // 여기서 데이터가 수신될때까지 대기
    
    fmt.Printf("Square: %d \n", n*n) // Output Square: 81
    wg.Done()               // 작업 완료를 알림
}

 

채널의 크기

 - 일반적으로 채널을 생성하면 크기가 0인 채널이 만들어진다. 크기가 0이라는 뜻은 채널에 들어온 데이터를 담아둘 곳이 없다는 얘기가 된다

 - 채널의 크기가 0이면 데이터 수신자가 없으면 데이터 송신자가 대기를 한다

 - 채널의 크기가 존재하면 데이터 송신자는 데이터를 넣고 원래의 동작을 수행한다

 - 채널의 크기가 존재하지만 채널의 크기를 초과한 데이터가 송신되면 담아둘 곳이 없어서 데이터 송신자는 멈추게 된다

 

채널에서 데이터를 가져가지 않아서 프로그램이 멈추는 예제

package main
import "fmt"

func main() {
    ch := make(chan int)       // 크기가 0인 채널 생성
    
    ch <- 9                    // 데이터 수신자가 없기 때문에 여기서 멈춤
    fmt.Println("Never print") // 실행되지 않는다.
}

 

크기를 가지는 채널 생성 예제

package main

import f "fmt"

func main() {

	f.Println("beffered")

	messages := make(chan string, 2) // string 타입의 채널을 만들고 해당 채널의 버퍼는 2이다.
	messages <- "wisoft"
	messages <- "lab"
    
	f.Println(<-messages)
	f.Println(<-messages)
}

make의 두번째 인자에 숫자를 넣어주면 그 크기로 생성된다

 

채널의 크기를 넘어가는 경우엔?

package main

import f "fmt"

func main() {

	f.Println("beffered")

	messages := make(chan string, 2)
	messages <- "wisoft"
	messages <- "lab"
	messages <- "golang"             // 여기서 deadlock 발생
	f.Println(<-messages)
	f.Println(<-messages)
	f.Println(<-messages)
}

 

------------ 아래 부터는 영역을 나누는 방법 ------------

 

채널에서 데이터 대기

package main

import (
	"fmt"
	"sync"
)

func square(wg *sync.WaitGroup, ch chan int) {
	for n := range ch {
		fmt.Println("Square: %d \n", n*n)// 2. 데이터를 계속 기다림
	}

	wg.Done()                            // 4. 실행되지 않음
}

func main() {
	var wg sync.WaitGroup
	ch := make(chan int)

	wg.Add(1)
	go square(&wg, ch)

	for i := 0; i < 10; i++ {
		ch <- i * 2                      // 1. 데이터 송신
	}

	wg.Wait()                            // 3. 작업 완료를 기다림
}

위에 코드에서는 for range 구문에서 계속해서 데이터가 들어오기를 기다리기 때문에 4번이 실행되지 않고 deadlock

 

채널 닫기

package main

import (
	"fmt"
	"sync"
)

func square(wg *sync.WaitGroup, ch chan int) {
	for n := range ch {
		fmt.Println("Square: %d \n", n*n)
	}

	wg.Done()                            
}

func main() {
	var wg sync.WaitGroup
	ch := make(chan int)

	wg.Add(1)
	go square(&wg, ch)

	for i := 0; i < 10; i++ {
		ch <- i * 2                      
	}
    
    close(ch)                            // 채널 닫음

	wg.Wait()                            
}

데이터를 모두 넣고 채널을 닫아주면 for range에서는 채널이 닫혀서 더이상 수신을 할 수 없기 때문에 for문을 빠져나오고 정상 종료된다

 

Select 문

Select문은 여러 채널을 동시에 기다릴 수 있다. 하지만 어떤 채널에서라도 데이터를 읽어오면 해당 구문을 실행하고 select문은 종료된다. 하나의 case만 처리되면 종료되기 때문에 반복해서 데이터를 처리하고 싶다면 for문과 함께 사용해야 한다

select {
case n := <-ch1
    ...  // ch1 채널에서 데이터를 빼낼 수 있을 때 실행
case n := <-ch2
    ...  // ch2 채널에서 데이터를 빼낼 수 있을 때 실행
case ...
}

 

select를 이용한 데이터 읽고 처리하는 예제

package main

import (
	"fmt"
	"sync"
)

func square(wg *sync.WaitGroup, ch chan int, quit chan bool) {
	for {
		select { // ch와 quit 양쪽을 모두 기다림 (non blocking)
		case n := <-ch:
			fmt.Println("Square: %d \n", n*n)
		case <-quit:
			wg.Done()
			return
		default:
			fmt.Println("test") // 실행되지 않음
		}
	}
}

func main() {
	var wg sync.WaitGroup
	ch := make(chan int)
	quit := make(chan bool)

	wg.Add(1)
	go square(&wg, ch, quit)

	for i := 0; i < 10; i++ {
		ch <- i * 2
	}

	quit <- true
	wg.Wait()
}

 

------------ 아래 부터는 역할을 나누는 방법 ------------

자동차 공장에서 자동차를 만드는 예제

package main

import (
	"fmt"
	"sync"
	"time"
)

type Car struct {
	Body  string
	Tire  string
	Color string
}

var wg sync.WaitGroup
var startTime = time.Now()

func main() {
	tireCh := make(chan *Car)
	paintCh := make(chan *Car)

	fmt.Printf("Start Factory \n")

	wg.Add(3)
	go MakeBody(tireCh) // 1. 고루틴 생성
	go InstallTire(tireCh, paintCh)
	go PaintCar(paintCh)

	wg.Wait()
	fmt.Println("Close the factory")
}

func MakeBody(tireCh chan *Car) { // 2. 차체 생산
	tick := time.Tick(time.Second)
	after := time.After(10 * time.Second)

	for {
		select {
		case <-tick:
			// Make a Body
			car := &Car{}
			car.Body = "Sports car"
			tireCh <- car
		case <-after: // 3. 10초 뒤 종료
			close(tireCh)
			wg.Done()
			return
		}
	}
}

func InstallTire(tireCh chan *Car, paintCh chan *Car) { // 4. 바퀴 설치
	for car := range tireCh {
		// Make Tire
		time.Sleep(time.Second)
		car.Tire = "Winter tire"
		paintCh <- car
	}

	wg.Done()
	close(paintCh)
}

func PaintCar(paintCh chan *Car) { // 5. 도색
	for car := range paintCh {
		// Make a body
		time.Sleep(time.Second)
		car.Color = "Red"
		duration := time.Now().Sub(startTime) // 6. 경과 시간 출력
		fmt.Printf("%.2f Complete Car: %s %s %s\n", 
                    duration.Seconds(), car.Body, car.Tire, car.Color)
	}

	wg.Done()
}

 

 

 

 

책 참조 : Tucker의 Go언어 프로그래밍

사이트 참조 (고루틴/채널/동기화/셀렉트) : https://judo0179.tistory.com/88

사이트 참조 (c++ select) : https://reakwon.tistory.com/117

 

[리눅스] 다중입출력 - select개념과 설명과 예제

select, poll, epoll과 같이 더 많은 정보와 예제를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요. https://reakwon.tistory.com/233 리눅스 프로그래밍 note 배포 티스토리에

reakwon.tistory.com

 

728x90

'Basic Programming > Golang' 카테고리의 다른 글

Golang - 컨텍스트  (0) 2023.09.16
Golang - 슬라이스 정렬  (1) 2023.07.13
Golang - 슬라이스 요소 삭제/추가  (0) 2023.07.13
Golang - 슬라이스 복제  (0) 2023.07.13
Golang - Slicing  (0) 2023.07.13
728x90

int 슬라이스 정렬

import(
       "fmt"
       "sort"
)

func main()
{
    s := []int{5,2,6,3,1,4}
    sort.Ints(s) // 정렬 끝 [1,2,3,4,5,6]
}

 

구조체 슬라이스 정렬

import(
       "fmt"
       "sort"
)

type Student struct
{
    Name string
    Age int
}

type Students []Student

func (s Students) Len() int { return len(s) }
func (s Students) Less(i, j int) bool { return s[i].Age < s[j].Age }
func (s Students) Swap(i, j int) { s[i], s[j] = s[j], s[i] }

func main()
{
    s := []Student
    {
        {"가", 31},
        {"나", 52},
        {"다", 42},
        {"라", 38},
        {"마", 20}
    }
    
    sort.Sort(Students(s))
}

 

728x90

'Basic Programming > Golang' 카테고리의 다른 글

Golang - 컨텍스트  (0) 2023.09.16
Golang - 채널 / 셀렉트  (1) 2023.09.16
Golang - 슬라이스 요소 삭제/추가  (0) 2023.07.13
Golang - 슬라이스 복제  (0) 2023.07.13
Golang - Slicing  (0) 2023.07.13
728x90

슬라이스 요소 삭제

slice1 := []int{1, 2, 3, 4, 5}
idx := 2 // 삭제할 인덱스

for i := idx + 1; i < len(slice); i++
{
    slice2[i - 1] = slice[i]    // slice1의 모든 값을 앞으로 당기기
}

slice = slice[:len(slice)-1]    // 마지막 값을 삭제
                                // [1, 2, 4, 5, 6]

 

append를 이용한 슬라이스 요소 삭제

slice1 := []int{1, 2, 3, 4, 5}
idx := 2

// append를 이용한 slice1 복사
slice2 := append(slice1[:idx], slice1[idx+1:])

// slice1[:idx]는 [1,2]
// slice1[idx+1:]는 [4,5,6]
// 두개를 합치면 [1,2,4,5,6]

 

슬라이스 요소 추가

slice := []int{1, 2, 3, 4, 5, 6}
slice = append(slice, 0) // 맨 뒤에 요소 추가

idx := 2 // 추가하려는 위치

for i := len(slice)-2; i <= idx; i--
{
    slice[i+1] = slice[i] // 맨 뒤부터 추가하려는 위치까지 값을 하나씩 밀기
}

slice[idx] = 100 // 값 변경

 

append를 이용한 슬라이스 요소 추가

slice := []int{1, 2, 3, 4, 5, 6}
idx := 2

// append를 이용한 slice1 복사
slice = append(slice[:idx], append([]int{100}, slice[idx:]))

// slice[:idx]는 처음부터 삽입하는 위치까지의 슬라이스 [1, 2]
// []int{100}은 삽입하려는 값으로 10 한개만 갖는 슬라이스
// slice[idx:]는 삽입하려는 위치부터 끝까지의 슬라이스 [3,4,5,6]
// append함수로 []int{100}에 slice[idx:]를 합쳐서 [100, 3, 4, 5, 6]
// append함수로[1, 2]에 [100, 3, 4, 5, 6]를 합쳐서 [1, 2, 100, 3, 4, 5, 6]

 

copy로 요소 추가

slice := []int{1, 2, 3, 4, 5, 6}
idx := 2

slice = append(slice, 0) // 맨 뒤에 요소 추가
copy(slice[idx+1:], slice[idx:]) // 값 복사
slice[idx] = 100 // 값 변경

 

- 출처 : Tucker의 Go 언어 프로그래밍

728x90

'Basic Programming > Golang' 카테고리의 다른 글

Golang - 채널 / 셀렉트  (1) 2023.09.16
Golang - 슬라이스 정렬  (1) 2023.07.13
Golang - 슬라이스 복제  (0) 2023.07.13
Golang - Slicing  (0) 2023.07.13
Golang - Slice append()를 사용할 때 발생하는 문제  (0) 2023.07.12
728x90

슬라이스 복제 (이전에 봤던 같은 곳을 가르켜서 값이 같이 변경되는 것을 방지)

slice1 := []int{1, 2, 3, 4, 5}
slice2 := make([]int, len(slice1)) // slice1과 같은 길이의 슬라이스 생성

for i, v := range slice1
{
    slice2[i] = v    // slice1의 모든 요소값 복사
}

slice1[1] = 100    // slice1 [1, 100, 3, 4, 5]
                   // slice2 [1, 2, 3, 4, 5]

 

append를 이용한 슬라이스 복제

slice1 := []int{1, 2, 3, 4, 5}

// append를 이용한 slice1 복사
slice2 := append([]int{}, slice1[0], slice1[1], slice1[2], slice1[3], slice1[4])

 

copy를 이용한 슬라이스 복사

slice1 := []int{1, 2, 3, 4, 5}
slice2 := make([]int, len(slice1)) // slice1과 같은 길이의 슬라이스 생성

copy(slice2, slice1)

 

- 출처 : Tucker의 Go 언어 프로그래밍

728x90
728x90

슬라이싱(Slicing)은 배열의 일부를 포인터로 가져오는 기능이다.

// array[startIdx:endIdx]

arr := [5]int{1, 2, 3, 4, 5}
arr[1:3] // [2, 3] - (1)부터 (3-1)까지의 값을 가져온다.

 

배열의 슬라이싱

func main()
{
    array := [5]int{1, 2, 3, 4, 5}
    slice := array[1:2]           // 슬라이싱으로 2 값의 포인터를 가져온다
    
    array[1] = 100                // array : [1, 100, 3, 4, 5]
                                  // slice : [100]
                      
    slice = append(slice, 500)    // array : [1, 100, 500, 4, 5]
                                  // slice : [100, 500]
}

위와 같이 array의 값들 중에서 원하는 값을 포인터로 가져올 수 있다.

 

슬라이스의 슬라이싱

func main()
{
    slice1 := []int{1, 2, 3, 4, 5}
    slice2 := slice1[1:2]          // 슬라이싱으로 2 값의 포인터를 가져온다
    
    slice1[1] = 100                // slice1 : [1, 100, 3, 4, 5]
                                   // slice2 : [100]
                      
    slice1 = append(slice1, 500)   // slice1 : [1, 100, 500, 4, 5]
                                   // slice2 : [100, 500]
}

 

슬라이싱의 문법

slice1 := []int{1, 2, 3, 4, 5}
slice2 := slice1[0:3] // [1, 2, 3] 처음부터 3-1까지 슬라이싱

// 첫번째 0 인덱스는 제외가 가능하다.
slice3 := slice1[:3] // [1, 2, 3] 처음부터 3-1까지 슬라이싱

slice4 := slice1[2:len(slice1)] // [3, 4, 5] 2부터 끝까지 슬라이싱

// 마지막 인덱스는 제외가 가능하다
slice5 := slice1[2:] // [3, 4, 5] 2부터 끝까지 슬라이싱

// 위에 것을 응용하면 전체 슬라이싱이 간편하다
slice6 := slice1[:] // [1,2, 3, 4, 5] 전체 슬라이싱

// 인덱스 3개로 슬라이싱
//slice[시작 : 끝 : 최대]
slice := []int{1, 2, 3, 4, 5}
slice1 := slice[1:3:4] // [2, 3] 이고 capacity는 4(최대)-1(시작)인 3

 

- 출처 : Tucker의 Go 언어 프로그래밍

728x90
728x90

slice에서 append()를 사용할 때 발생하는 문제 1

slice1 := make([]int, 3, 5)     // [0, 0, 0] len : 3, cap : 5
slice2 := append(slice1, 4, 5)  // [0, 0, 0, 4, 5] len : 5, cap : 5

slice1[1] = 100                 // slice1 : [0, 100, 0] len : 3, cap : 5
                                // slice2 : [0, 100, 0, 4, 5] len : 5, cap : 5
                    
slice1 = append(slice1, 500)    // slice1 : [0, 100, 0, 500] len : 4, cap : 5
                                // slice2 : [0, 100, 0, 500, 5] len : 5, cap : 5

append()를 이용했는데 slice1과 slice2가 같은 저장 공간을 사용하는 것 처럼 보인다.

 

slice에서 append()를 사용할 때 발생하는 문제 2

slice1 := []int{1, 2, 3}        // [1, 2, 3] len : 3, cap : 3
slice2 := append(slice1, 4, 5)  // [1, 2, 3, 4, 5] len : 5, cap : 6

slice1[1] = 100                 // slice1 : [1, 100, 3] len : 3, cap : 3
                                // slice2 : [1, 2, 3, 4, 5] len : 5, cap : 6
                    
slice1 = append(slice1, 500)    // slice1 : [1, 100, 3, 500] len : 4, cap : 6
                                // slice2 : [1, 2, 3, 4, 5] len : 5, cap : 6

append를 이용했는데 slice1과 slice2가 다른 저장 공간을 사용하는 것 처럼 보인다.

 

이 두개의 차이를 이해하려면 slice의 동작에 대해 이해를 해야한다.

문제 1의 코드를 보면 capacity 값이 2개가 남아있는 상태로 만들어졌었다. 그렇기 때문에 append()로 4,5를 추가해도 slice1의 메모리 주소가 복사되서 같은 저장 공간을 사용하는 것 처럼 보인 것이다.

즉 slice1과 slice2는 다른 저장공간을 가지고 있지만 안에 값은 포인터로 같은 곳을 가르키고 있다.

문제 2의 코드를 보면 capacity 값이 0인 상태로 append()를 했기 때문에 새로운 메모리 공간을 생성한 이후에 slice2에 저장이 되었기 때문에 slice1과 slice2는 전혀 다른 메모리 공간이 된 것이다.

즉 slice1과 slice2는 다른 저장공간을 가지고 있고 안에 값도 다른 포인터를 가르키고 있다.

 

- 출처 : Tucker의 Go 언어 프로그래밍

728x90

'Basic Programming > Golang' 카테고리의 다른 글

Golang - 슬라이스 정렬  (1) 2023.07.13
Golang - 슬라이스 요소 삭제/추가  (0) 2023.07.13
Golang - 슬라이스 복제  (0) 2023.07.13
Golang - Slicing  (0) 2023.07.13
Golang - Slice 기본 문법  (0) 2023.07.12
728x90

Golang에서는 Slice는 C++의 Vector와 Pointer를 섞은 듯한 느낌이다

각각의 선언방법 확인

// c++ array
int array[10]

// golang array
var array [10]int

// c++ vector
std::vector<int> vec;

// golang 
var slice []int

 

slice 사용 예제

// slice 선언 예
var slice []int // 크기가 0인 int형 slice
var slice []int{1,2,3} // [1,2,3]
var slice []int{1, 5:2, 10:3} // [1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 3]
var slice = make([]int, 3) // [0, 0, 0] 크기가 3인 int형 slice

// 주의사항
var array = [...]int{1,2,3} // 크기가 고정인 배열 선언
var slice = []int{1,2,3} // slice 선언

// slice 길이
sliceLen = len(slice)

// 슬라이스 순회
var slice = []int{1,2,3}
for i := 0; i < len(slice); ++i
{
    slice[i] += 10
}

// range를 이용하면 c++로 치면 tuple 처럼 값을 가져온다.
// i는 index
// v는 요소의 값
for i, v := range slice
{
    slice[i] = v * 2
}

// append 이용해서 값 추가
var slice = [int]{1,2,3}
slice = append(slice, 4)
slice2 := append(slice, 5, 6, 7)

// string의 값 바꾸기
var str string = "Hello World"
var slice []byte = []byte(str)

slice[2] = 'a' // == Healo World

 

슬라이스와 배열의 동작 차이

func changeArray(array2 [5]int)
{
    array2[2] = 200
}

func changeSlice(slice2 []int)
{
    slice2[2] = 200
}

func main()
{
    array := [5]int{1,2,3,4,5}
    slice := []int{1,2,3,4,5}
    
    changeArray(array)
    changeSlice(slice)
    
    fmt.Println("array:", array)    // {1, 2, 3, 4, 5}
    fmt.Println("slice:", slice)    // {1, 2, 200, 4, 5}
}

위 코드에서 결과를 보면 changeArray()는 값복사, changeSlice()는 주소 복사가 일어난 것을 알 수 있다.

 

- 출처 : Tucker의 Go 언어 프로그래밍

728x90

+ Recent posts