본문 바로가기
Study/Go 언어

[Go언어] 동기화 객체, 파일처리, JSON

by Jamie Lim 2020. 8. 8.
사용 교재 : 가장 빨리 만나는 Go언어
3주차 : UNIT 35 ~ 51

[UNIT 35] 동기화 객체 사용하기

 - 동기화 객체

   > Mutex : 상호 배제라고도 하며 여러 스레드에서 공유되는 데이터를 보호할 때 사용

   > RWMutex : 읽기/쓰기 뮤텍스., 읽기와 쓰기 동작을 나누어 잠금을 걸 수 있다

   > Cond : 조건 변수, 대기하고 있는 하나의 객체를 깨울 수 있고 여러 개를 동시에 깨울 수 있다

   > Pool : 멀티 스레드에서 사용할 수 있는 객체, 자주 사용하는 객체를 품에 보관했다 다시 사용한다

   > WaitGroup : 고루틴이 모두 끝날 때까지 기다리는 기능

   > Atomic : 원자적 연산, 더 이상 쪼갤 수 없는 연산이라는 뜻으로 멀티 스레드, 멀티 코어 환경에서 안전하게 값을 연산하는 기능

 

 1. 뮤텍스 사용하기

   - 여러 고루틴이 공유하는 데이터를 보호할 때 사용하며 sync 패키지에서 제공하는 뮤텍스 구조체와 함수

     > sync.Mutex

     > func (m *Mutex) Lock() : 뮤텍스 잠금

     > func (m *Mutex) Unlock() : 뮤텍스 잠금 해제

 

   - 고루틴 두 개에서 각각 1000번씩 슬라이스에 값을 추가하기

     > data12000번 추가했기 때문에 길이는 2000이 되어야 하는데 그렇지 않음

     > 두 고루틴이 동시에 data에 접근했기 때문에 append 함수가 일일히 정확하게 처리되지 않음 -> 경쟁 조건이라 한다

     > runtime.Gosched( ) : 다른 고루틴이 CPU를 사용할 수 있도록 양보한다
       (time.Sleep( )
보다 runtime.Gosched( )가 더 명확함)

 

   * CPU 코어가 한 개였다면 경쟁 조건 상황이 발생하지 않고 정확하게 2000이 출력될 수 있다. 하지만 반복 횟수가 많아지면 CPU 코어가 하나라도 경쟁 조건이 발생한다. 또한, CPU 코어를 여러 개 사용하면 동시에 공유 데이터에 접근할 수 있어 경쟁 조건이 발생한다.

 

   - data 슬라이스를 뮤텍스로 보호

    > 뮤텍스는 sync.Mutex를 할당하고 고루틴에서 Lock, Unlock 함수로 사용

    > 보호를 시작하는 부분에서 Lock( )을 사용

    > 보호를 끝낼 부분에서 Unlock( )을 사용

    > LockUnlock의 짝이 맞춰주어야 하며 맞지 않으면 데드락이 발생한다

    > 각자의 슬라이스를 보호하며 실행하기 때문에 정확히 2000이 출력된다

 

   

 2. 읽기, 쓰기 뮤텍스 사용하기

   - 읽기, 쓰기 뮤텍스는 읽기 동작과 쓰기 동작을 나눠 잠금을 걸 수 있다

    > 읽기 락 : 읽기 락끼리는 서로 막지 않는다. But, 읽기 시도 중에 값이 바뀌면 안 되므로 쓰기 락은 막는다

    > 쓰기 락 : 쓰기 시도 중 다른 곳에서 다른 곳에서 이전 값을 읽으면 안 되고, 다른 곳에서 값을 바꾸면 안 되므로 읽기, 쓰기 락을 모두 막는다

 

   - sync 패키지에서 제공하는 읽기, 쓰기 뮤텍스 구조체와 함수

    > sync.RwMutex

    > func (rw *RWMutex) Lock( ), func (rw *RWMutex) Unlock( ) : 쓰기 뮤텍스 잠금, 잠금 해제

    > func (rw *RWMutex) RLock( ), func (rw *RWMutex) RUnlock( ) : 읽기 뮤텍스 잠금 및 해제

 

   - 읽기 쓰기 뮤텍스를 사용하지 않고 고루틴에서 값 출력하기

     > 값을 쓰는 고루틴은 1, 값을 읽는 고루틴은 2개를 생성한다

     > 아래 예제는 간단한 상황이라 큰 문제가 없지만 중요한 데이터를 쓸 때는 다른 곳에서 이전 데이터를 읽거나 읽기 시도 중 값이 바뀌는 문제가 생길 수 있다

 

   - 읽기, 쓰기 뮤텍스 사용해 읽기, 쓰기 동장 실행이 보장되도록 하기

    > 읽기 동작이 시작할 부분에 RLock 함수를, 읽기 동작을 끝낼 부분에 RUlock 함수를 사용한다

    > 쓰기 동작을 시작할 부분에 Lock 함수를, 쓰기 동작을 끝낼 부분에 Unlock 함수를 사용한다

    > RLcok, RUnlock, Lock, Unlock 함수는 반드시 짝이 맞아야 하고 아니라면 데드락이 발생한다

    > 읽기 동작은 서로 막지 않고 항상 동시에 실행

 

     

읽기, 쓰기 뮤텍스를 사용한 고루틴의 실행 순서

       * 읽기 2번 쓰기 1번 순서로 진행된다

 

   - 읽기, 쓰기 뮤텍스는 중요한 쓰기 작업을 할 때 다른 곳에서 이전 데이터를 읽지 못하게 방지하거나 읽기 작업을 할 때 데이터가 바뀌는 상황을 방지해준다

   - 읽기, 쓰기 뮤텍스는 쓰기 동작보다 읽기 동작이 많을 때 유리함

 

 3. 조건 변수 사용하기

   - sync 패키지에서 제공하는 조건 변수의 함수

    > sync.Cond

    > func NewCond(l Locker) *Cond : 조건 변수 생성

    > func (c *Cond) Wait( ) : 고루틴 실행을 멈추고 대기

    > func (c *Cond) Signal( ) : 대기하고 있는 고루틴 하나만 깨움

    > func (c *Cond) Broadcast( ) : 대기하고 있는 모든 고루틴 깨움

 

   - 대기하고 있는 고루틴 하나씩 깨우기

 

조건 변수 Signal 함수로 고루틴을 하나씩 깨우는 과정

 

   - 대기하고 있는 모든 고루틴 깨우기

     

 

 4. 함수를 한 번만 실행하기

   - Once를 사용하면 함수를 한 번만 실행할 수 있다

   - Once는 한 번만 실행되는 특성이 있어 복잡한 반복문 안에서 각종 초기화에 유용하다

 

   - sync 패키지에서 제공하는 Once의 구조체 함수

    > sync.Once

    > func (*Once) Do(f func()) : 함수를 한 번만 실행

 

   - hello, world 한 번만 실행되게 하기

    > Once를 할당 한 뒤 Do 함수로 실행할 함수를 지정한다

   

 

 5. 풀 사용하기

   - 풀은 객체를 사용한 후 보관해 두었다 다시 사용하게 해주는 기능이다

   - 풀은 캐시라고도 할 수 있고 메모리 할당과 해제 횟수를 줄여 성능을 높이고자 할 때 사용한다

   - 풀은 고루틴에서 동시에 사용할 수 있다

 

   - sync 패키지에서 제공하는 풀의 구조체와 함수

    > sync.Pool

    > func (p *Pool) Get() interface{} : 풀에 보관된 객체를 가져옴

    > func (p *Pool) Put(x interface{}) : 풀에 객체를 보관

 

   - 풀을 이용해 정수 10개짜리 슬라이스 공유

    > 첫 번째 고루틴에서 슬라이스에 랜덤한 숫자 10개 저장해 출력

    > 두 번째 고루틴에서 짝수 10개를 저장해 출력

  

코드
결과

     

 

 6. 대기 그룹 사용하기

   - 대기 그룹은 고루틴이 모두 끝날 때까지 기다리고 사용

 

   - sync 패키지에서 제공하는 대기 그룹의 구조체와 함수

    > sync.WaitGroup

    > func (wg *WaitGroup) Add(delta int) : 대기 그룹에 고루틴 개수 추가

    > func (wg *WaitGroup) Done() : 고루틴이 끝났다는 것을 알려줄 때 사용

    > func (wg *WaitGroup) Wait() : 모든 고루틴이 끝날 때까지 기다림

     

    > Add 함수에 설정한 값과 Done 함수가 호출되는 횟수는 같아야 함

    > Done 함수는 defer와 함께 사용해 지연 호출로 사용할 수 있다

 

 7. 원자적 연산 사용하기

   - 원자적 연산이란 더 이상 쪼갤 수 없는 연산이라는 뜻이다

   - 보통 원자적 연산은 CPU 명령어를 직접 사용하여 구현되어 있다

 

   - 고루틴 사용해 정수형 변수를 2000번 더하고, 1000번 빼기

    > 예상 결과는 1000이지만 결과는 그렇지 않음 -> 여러 변수에 고루틴이 동시에 접근하면서 정확하게 연산 되지x

     

 

   - 원자적 연산을 사용해 계산하기

    > 정확하게 1000이 출력됨

     

 

   - sync/atomic 패키지에서 제공하는 원자적 연산자

    > Add 계열 : 변수에 값을 더하고 결과를 리턴한다

    > CompareAndSwap 계열 : 변수 AB를 비교해 같으면 C를 대입하고 true를 리턴하고 다르면 false를 리턴한다

    > Load 게열 : 변수에서 값을 가져온다

    > Store 계열 : 변수에서 값을 저장한다

    > Swap 계열 : 변수에 새 값을 대입하고, 이전 값을 리턴한다

 

[UNIT 36] 리플렉션 사용하기 (pg. 213)

 - 리플렉션 : 실행 시점에 인터페이스나 구조체 등의 타입 정보를 얻어내거나 결정하는 기능

 - Java, C#처럼 가상 머신 위에서 실행되는 언어나 Python, Ruby 등의 스크립트 언어에서 주로 사용하였다

 

 - 변수와 구조체의 타입을 표시하기

    

 

 - 리플렉션으로 변수의 타입과 값에 대한 상세한 정보도 얻어올 수 있다

    

 

 1. 구조체 태그 가져오기

   - 리플렉션으로 구조체의 태그 가져오기

     > 구조체 필드의 태그는 `(백쿼트)태그명:”내용”` 형식으로 지정한다

     > 구조체 필드에 태그를 여러 개 지정할 때는 공백으로 구분해준다

     > reflect.TypeOf 함수에 구조체 인스턴스를 넣으면 reflect.Type이 리턴된다

     > Get 함수를 이용해 태그를 가져올 수 있다

      

 

 2. 포인터와 인터페이스의 값 가져오기

   - 일반 포인터와 인터페이스의 값을 가져오는 방법

    > 포인터는 일반 변수와 다르게 값을 가져오기 위해선 reflect.ValueOf 함수로 값 정보를 가져와 Elem 함수로 값을 가져와야 한다

    > 빈 인터페이스 b1을 대입해 int 타입이지만 b는 인터페이스이기 때문에 컴파일 에러가 발생한다

      * 인터페이스 값을 가져올 때 변수 타입에 맞는 Int, Float, String 등의 함수를 사용해선 안 된다

 

   

[UNIT 37] 동적으로 함수 생성하기

 - 리플렉션을 사용하여 동적으로 함수를 만들어 내기

   > reflect.MakeFunc 함수를 사용하기

  

 

   - 정수, 실수, 문자열 더하기를 처리하는 함수 만들기

  

코드
결과

 

[UNIT 49] 파일 처리하기

 1. 파일 쓰기

   - os 패키지에서 제공하는 파일 함수와 파일 쓰기 함수

    > func Create(name string) (file *File, err error) : 기존 파일을 열거나 새 파일을 생성

    > func (F *File) Close() error : 열린 파일을 닫음

    > func (f *File) Write(b []byte) (n int, err error) : 파일에 값을 씀, 파일에 쓴 데이터의 길이와 에러 값을 리턴

 

   - hello.txt 파일에 “Hello world” 문자열 저장

 

 

 2. 파일 읽기

   - os 패키지에서 제공하는 파일 열기, 파일 정보, 파일 읽기 함수

    > func Open(name string) (file *File, err error) : 파일 열기

    > func (f * File) Stat() (fi FileInfo, err error) : 파일의 정보를 얻어옴

    > func (f *File) Read(b []byte) (n int, err error) : 파일에서 값을 읽음. 파일에서 읽은 데이의 길이와 에러 값을 리턴

 

   - 파일 읽기

     

 

 3. 파일 읽기 쓰기

   - os 패키지에서 제공하는 파일 열기 함수와 파일 포인터 설정 함수

    > func OpenFile(name string, flag int, perm FileMode) (file *File, err error) : 파일 플래그, 파일 모드를 지정해 파일 열기

    > func (f *File) Seek(offset int64, whence int) (ret int64, err error) : 파일을 읽거나 쓸 위치로 이동

 

   - os.OpenFile 함수를 사용해 읽기/쓰기 모드로 파일을 연 뒤 파일 읽기

  

          

   - os.OpenFile 함수는 파일 이름, 플래그, 파일 모드를 받는다

   - |(OR 연산자)를 사용해 조합할 수 있다

 

   - 플래그 종류

     > os.O_RDONLY : 읽기 전용으로 파일 열기

     > os.O_WRONLY : 쓰기 전용으로 파일 열기

     > os.O_RDWR : 읽기, 쓰기를 모두 할 수 있도록 파일 열기

     > os.O_APPEND : 파일을 저장했을 때 끝부분에 내용을 추가한다

     > os.O_CREATE : 파일이 없으면 파일을 새로 생성, 파일이 있으면 해당 파일 열기

     > os.O_EXCL : 파일이 없을 때만 새로 생성 (os.O_CREATE와 사용)

     > os.O_SYNC : 동기 I/O를 사용

     > os.O_TRUNC : 파일이 있다면 파일을 연 뒤 내용 삭제

 

   - fiel.Seek()에서 첫 매개변수는 offset 두 번째 매개변수는 기준점이다

   - 설정할 수 있는 기준점

     > os.SEEK_SET : 파일의 맨 처음을 기준으로 함

     > os.SEEK_CUR : 현재 읽기/쓰기 OFFSET 값을 기준으로 함

     > os.SEEK_END : 파일의 맨 끝을 기준으로 함

 

 4. ioutil 패키지 사용하기

   - ioutil 패키지를 사용하면 보다 간단하게 파일을 읽고 쓸 수 있음

   - ioutil 패키지에서 제공하는 파일 함수

     > func WriteFile(filename string, data []byte, per os.FileMode) error : 파일 쓰기, 에러 값 리턴

     > func ReadFile(filename string) ([]byte, error) : 파일 읽기, 읽은 데이터(바이트 슬라이스)와 에러 값 리턴

 

   - ioutil을 사용해 파일 읽기

 

   

[UNIT 50] 입출력 인터페이스 사용하기

 - io.Readerio.Writer 인터페이스를 사용해 입출력을 할 수 있다

 

 - io.Readerio.Writer의 정의

type Reader interface {

    Read(p []byte) (n int, err error)

}


type Writer interface {

    Write(p []byte) (n int, err error)

}


 1.
파일 처리하기 

   - bufio 패키지에서 제공하는 입출력 인터페이스 함수

       * bufioBuffered I/O를 뜻함

     > func NewReader(rd io.Reader) *Reader : io.Reader 인터페이스로 io.Reader 인터페이스를 따르는 읽기 인스턴스 생성

     > func NewWriter(w io.Writer) *Writer : io.Writer 인터페이스로 io.Writer 인터페이스를 따르는 쓰기 인스턴스 생성

     > func (b *Writer) WriterString(s string) (int, error) : 문자열 버퍼에 저장

     > func (b *Writer) Flush() error : 버퍼의 데이터를 파일에 저장

 

   - bufiofmf 사용해 파일 읽고 쓰기

     > bufio로 파일을 저장하게 되면 바로 저장하지 않고 임시 공간(버퍼)에 쌓아 두었다 Flush 매서드를 통해 파일에 저장한다

     

 

bufio로 문자열을 파일에 저장

 

 2. 문자열을 파일로 저장하기

   - strings 패키지에서 제공하는 입출력 인터페이스 함수

     > func NewReader(s string) *Reader : 문자열로 io.Reader 인터페이스를 따르는 읽기 인스턴스를 생성한다

 

   - bufio 패키지에서 제공하는 입출력 인터페이스 함수

     > func (b *Writer) ReadFrom(r io.Reader) (n int64, err error) : io.Reader의 데이터를 읽어 io.Writer에 저장

 

   - 문자열을 io.Reader 인터페이스로 만들어 파일에 저장하기

  

                    

   - io.Copy 함수를 사용해 io.Reader의 데이터를 io.Writer로 복사할 수 있음

     > func Copy(dst Writer, src Reader) (written int64, err error) : io.Readerio.Writer로 복사하기

  

                  

 3. 문자열을 화면에 출력하기

   - 문자열을 io.Redaer를 그대로 화면에 출력하기

     > os.Stdoutio.Writer 인터페이스를 따르기에 io.Copy 함수로 io.Reader를 복사해주면 콘솔에 그대로 출력된다

  

 

 4. 기본 입출력 함수 사용하기

   - 기본 입출력 함수와 입출력 인터페이스를 사용해 출력하기

      

 

   - bufio와 기본 입출력 함수를 사용해 파일에 값 저장하기

     

 

   - io.Reader, io.Writer 인터페이스만 맞다면 어디든 사용할 수 있음

  

io.Reader, io.Writer 인터페이스는 다양한 패키지에서 사용

 

 5. 읽기, 쓰기 인터페이스를 함께 사용하기

   - io.ReadWriter 인터페이스로 읽기/쓰기 처리하기

     * io.ReadWriter 인터페이스는 io.Reader, io.Writer 인터페이스를 포함하고 있다

type ReadWriter interface {

    Reader

    Writer

}

io.ReadWriter 인터페이스

 

   - bufio 패키지에서 제공하는 입출력 인터페이스 함수

     > func (b *Reader) ReadLine() (line []byte, isPrefix bool, err error) : io.Reader에서 문자열 한 중을 읽어 바이트 슬라이스로 리턴

 

   - io.ReadWriter 인터페이스를 사용해 파일에 문자열 쓰고 읽기

     > io.ReadWriter 인터페이스를 사용해 읽기 및 쓰기 동작을 인스턴스 한 개로 처리할 수 있음

   

  

[UNIT 51] JSON 문서 사용하기

 - encoding/json 패키지에서 제공하는 JSON 함수

   > func Marshal(v interface{}) ([]byte, error) : Go 언어 자료형을 JSON 텍스트로 변환

   > func Marchallndent(v interface{}, prefix, indent string) ([]byte, error) : Go 언어 자료형을 JSON 텍스트로 변환하고 사람이 보기 편하도록 들여쓰기를 해준다

   > func Unmarchal(data []byte, v interface{}) error : JSON 텍스트를 Go 언어 자료형으로 변환

 

 - JSON 문서를 읽는 방법

   > var data map[string ]interface{}를 통해 JSON 문서의 데이터를 저장할 공간을 간단하게 맵으로 만들 수 있다

   > JSON 문서에서 키는 문자열이고 값은 문자열과 숫자를 사용한다

   > 맵을 만들 때 키는 문자열로 값은 interface{}로 지정해 모든 값을 넣을 수 있게 한다

   > data[“키 이름”]으로 키를 지정해 값을 가져올 수 있다

     

 

 - 맵을 JSON 형태로 변환

   > 문자열을 키로하고 모든 자료형을 값으로 저장할 수 있는 맵을 할당하고 json.Marshal 함수로 JSON 문서로 변환한다

   > 첫 번째 매개변수는 JSON 문서로 만들 데이터

   > 두 번째 매개변수는 JSON 문서의 첫 칸에 표시할 문자열

   > 세 번째 매개변수는 들여쓰기 할 문자

    

 

 1. 구조체 활용하기

   - 구조체를 사용해 좀 더 복잡한 형태인 JSON 문서를 처리하기 (배열, -배열, -값 조합)

  

코드
결과

 

   - JOSN 문서의 구조

   

 

   - 데이터 JSON 형태로 변환하기

  

코드
결과

 

   - 구조체 필드가 대문자로 시작하면 JSON 문서 안의 키도 대문자로 시작해야 한다

   - JSON 문서 안의 키를 소문자로 시작하길 원한다면 '필드명 자료형 ‘json:”키”’로 지정해주어야 한다

 

 2. JSON 파일 사용하기

   - JSON 파일을 만들고 읽기

    > JSON 파일 만들기

    

코드
결과

 

    > 파일 읽기

 

댓글