Learning Go

박기원·2022년 5월 16일
0

Language

목록 보기
1/2
post-thumbnail

chap. 01

Go 개발환경

  • Scale up vs. Scale out
    1.1 GO 도구 설치
    1.2 GO 작업 공간
  • GO PATH 환경 변수
  • 작업 공간의 기본 디렉터리
    : $HOME/go

cf.) 셸이란?

'셸(Shell)'은 사용자의 명령어를 해석하고 운영체제가 알아들을 수 있게 지시해주는 것으로, 사용자와 커널을 이어주는 것이다.

'커널(kernel)'은 이와 같은 내용을 받아 하드웨어에 직접적으로 명령을 내려준다.

셸 : 사용자가 글자를 입력하여 컴퓨터에 명령할 수 있도록 하는 프로그램

터미널/콘솔 : 셸을 실행하기 위해 글자 입력을 받아 컴퓨터에 전달하거나 컴퓨터의 출력을 글자 화면에 쓰는 프로그램

1.3 go 명령어

  • go run go 파일...|go 패키지
  • 작은 프로그램 테스트
  • Go를 스크립트 언어처럼 사용 시
  • go build go 파일...|go 패키지
  • 바이너리(실행파일) 생성

  • go build hello.go
    go build -o hello_world.exe
    hello.go

1.3.2 서드 파티 Go 도구 설치

  • go는 다른 언어와 달리 자신의 코드 저장소를 공유하여 패키지를 직접 다운받고 설치 가능

  • 예)
    go install github.com/rakyll/hey@latest

1.3.3 코드 포매팅

  • 효율적인 코드 작성을 위함
  • go fmt
  • 들여쓰기 공백 수정
  • 구조체 항목 정렬
  • 연산자 주변 적절한 공백 사용 체크
  • goimports
  • import 문 정리
    • 알파벳 순 정렬
    • 사용되지 않은 import 삭제
    • import 추측(추론): 100% 정확하지 않음
  • enhanced go fmt
  • go install 명령으로 설치 필요 (p.28 참조)
  • 세미콜론 삽입 규칙
  • go fmt는 이 규칙에 의해 잘못 삽입된 중괄호({})는 수정 못함
  • Go 컴파일러가 자동으로 문장 끝에 세미콜론을 붙여줌
  • 규칙
    • 식별자
    • 기본 리터럴
    • 다음 토큰 중 하나: break, continue, fallthrough, return, ++, ‘--’, }, )

1.4 린팅, 베팅

  • Effective Go
  • Go 위키의 코드 리뷰 규칙

  • linter

  • 1978년 시작
  • 코드가 스타일 가이드를 잘 지켰는지 확인
  • 알맞은 변수 이름 지정
  • ...
  • 100% 정확도 아님

1.5 개발 도구 소개

1.5.1 비주얼 스튜디오 코드

  • Go 개발을 위한 VS Code 구성

1.5.3 Go Playground

1.6 Makefiles 텍스트 파일

  • make 프로그램 필요

chap. 02

기본 데이터 타입과 선언

2.1 내장 타입

2.1.1 제로 값

  • 선언되었으나 값이 할당되지 않은 변수에 기본 값 제로(0) 할당

2.1.2 리터럴

  • 코드에 작성하는 값 그 자체

  • 정수 리터럴

  • 10진수, 8진수, 16진수, 2진수 ⇒ 접두사 이용 (없음, 0o, 0x, 0b)
  • 밑줄(underscore): 정수 리터럴 사이에
  • 부동 소수점 리터럴
  • 점(.)
  • e(E) 지수 표현
  • 밑줄 가능
  • 룬 리터럴
  • 작은 따옴표
  • 단일 유니코드 문자, 8비트 8진 숫자, 8비트 16진수 숫자, 16비트 16진수 숫자, 32비트 유니코드
  • 이스케이프 시퀀스: ‘\n’, ‘\t’, …
  • 문자 리터럴
  • 두 가지 방법
  • 1) 해석된 문자열 리터럴: 큰 따옴표 사용
    • 이스케이프 룬이 처리가 됨
  • 2) 로(raw) 문자열: 역따옴표(백틱) 사용
  • 다른 타입으로 선언된 두 정수는 연산 불가능
  • 하지만, 부동소수점 변수에 정수 리터럴 사용 가능
    정수 변수에 부동소수점 리터럴 사용 가능
  • 왜냐, 리터럴 자체에는 데이터 타입이 없으므로
  • Go 언어는 실용적 언어 => 리터럴에는 타입이 정해져 있지 않다

2.1.3 불리언

  • true, false

2.1.4 숫자 타입

  • 정수 타입
  • 특수한 경우 아니면 int 사용
  • 특수 정수 타입들
  • byte: uint8 보다 선호
  • int: 플랫폼에 따라서 int32, int64
  • uint: 플랫폼에 따라서 uint32, uint64
  • rune: int32지만 용도가 다르다
  • uintptr: 포인터
  • 정수 연산자
  • 산술연산자의 결과는 정수
  • 복합대입연산자도 지원
  • 비교연산자
  • 비트연산자
  • 시프트연산자
  • 논리연산자
  • 부동소수 타입
  • float32, float64(기본 타입)
  • %(나머지 연산)에 대해서는 정의되어 있지 않음
  • 부동소수 변수끼리 ==, != 비교는 가능하나 사용하지 말자
  • 복소수 타입

2.1.5 문자열과 룬 맛보기

  • 문자열
  • 제로 값: 비어있는 문자열
  • 비교연산자 가능
  • 연결연산자(+)
  • 불변(immutable)
  • 큰 따옴표
  • 단일 코드 포인트
  • 작은 따옴표
  • int32의 별칭

2.1.6 명시적 타입 변환
cf.) Explicit vs. Implicit
명시적 vs. 암시적(암묵적)
⇒ Manual vs. Automatic

  • 자동 타입 변환
  • 편리하나
  • 변환 규칙이 복잡, 예기치 못한 결과 초래 가능
  • Go는 자동 타입 변환 불허
  • 의도의 명확성 및 가독성 중시 때문
  • 명시적으로 타입 변환해야 함
    -> 모든 타입 변환 규칙을 외울 필요 없음
  • 다른 Go 타입을 불리언으로 취급할 수 없다.
  • 조건식은 반드시 비교식으로 작성

2.2 var vs. :=

  • 변수 선언 스타일에 따른 의도가 존재

  • var 키워드
    [SYNTAX]
    var 변수 타입 = 값(표현식)

  • 값의 타입이 예상가능하면 타입 생략 가능
  • 값을 생략하면 타입의 제로 값이 대입
  • 같은 타입의 여러 변수 또는 다른 타입의 여러 변수 동시 선언 가능
  • 여러 변수의 제로 값 동시 선언 가능
  • 다양한 변수를 한 번에 선언
  • 선언리스트를 괄호로 묶어서
  • := 짧은 선언
  • var 키워드(X)
  • 타입(X)
  • 함수 밖에서는 사용 불가 = package 레벨에서 사용 불가

2.3 const 사용

  • 상수: 상수 변수
  • 한 번 대입한 값을 변경할 수 없다
  • 컴파일 시 값이 결정되어야 한다, 즉 변수를 대입할 수 없다.
  • 리터럴에 이름을 부여하는 방법

2.4 타입 지정 상수와 타입 미지정 상수

  • 타입 미지정 상수는 리터럴과 같은 취급
  • 타입 미지정 상수는 리터럴과 같은 취급
  • 타입 지정 상수는 해당 타입 변수에만 할당해야 함

2.5 사용하지 않는 변수

  • 지역 변수는 반드시 사용되어야 함
  • 사용되지 않는 상수는 허용

2.6 변수와 상수 이름 짓기

  • Go는 유니코드 문자를 식별자로 허용
  • 권장하지 않음
  • Go는 관용적으로 스네이크 표기법 사용하지 않음
  • 낙타 표기법, 파스칼 표기법 사용
  • 상수: 모든 문자를 대문자, 각 단어는 밑줄로 구분
  • Go는 이러한 일반 상수 이름 규칙을 따르지 않는다
  • 패키지에 포함된 요소를 외부에 노출시킬지(expose) 여부를 식별자의 첫글자가 대소문자 여부로 결정
  • 함수 내에서는 짧은 변수 이름 선호
  • 변수의 의미보다 간결성을 선호하는 Go
  • 짧은 이름
  • 코드 간결
  • 코드의 복잡도 판별 기준
  • 패키지 블록 내
  • 설명적인 변수, 상수 이름 사용하자

chap. 03

복합 타입

3.1 배열(array)

  • 같은 타입의 여러 데이터를 담음
  • 시퀀스 타입 (순서 중요)
  • Go에서는 선호하지 않음(특별한 경우에만 사용)
  • 선언 시 크기를 지정해야 함
  • 크기를 변경할 수 없다
  • 배열 간 비교 가능(==, !=)
  • 배열 인덱스는 범위를 벗어나서는 안되고 음수도 불가
  • 선언 방법
    1) 제로 값으로 선언: 배열의 크기와 배열 내 요소 타입 지정
    2) 배열 초깃값 지정: 배열 리터럴 사용
    3) 희소 배열: 대부분의 요소가 0인 배열
    4) 배열 크기 지정하지 않고: 배열 리터럴 필요
    5) 다차원 배열: []의 개수가 차원 수

  • 배열 요소 접근

  • [] 사용
  • 배열 길이
  • len() 함수
  • Go에서 배열을 잘 사용하지 않는 이유
  • 배열의 크기가 배열의 타입을 결정하는데 사용되기 때문
  • 즉, 타입이 같은 배열이라도 크기가 다르면 다른 타입
  • 크기(길이)가 다르면 타입 변환도 불가능

3.2 슬라이스

  • 일련의 값(시퀀스)을 저장하는 자료 구조

  • 순서 중요

  • 슬라이스의 크기는 타입의 일부가 아니다 (배열과의 큰 차이점)

  • 슬라이스 간 비교는 불가 (배열과 다른 점)

  • 단, nil과는 비교 가능(==, !=)

  • 슬라이스 선언 (슬라이스 크기를 지정하지 않음)
    1) 슬라이스 초깃값 지정: 슬라이스 리터럴
    2) 희소 슬라이스
    3) 다차원 슬라이스
    4) 제로 슬라이스: 슬라이스 리터럴 없이 선언만 하는 것

    • 슬라이스의 제로 값은 nil
    • nil : 값의 부재(absence of value) 상태
    • nil은 타입이 없음
      5) 비어있는 슬라이스: 슬라이스 리터럴에 초깃값이 없는 것
      6) make() 함수 사용하여 생성: 수용량 지정 가능

  • 슬라이스의 길이
  • len() 함수 사용

3.2.2 append

  • 슬라이스에 새로운 요소 추가
  • append의 결과를 할당하지 않으면 컴파일 에러

3.2.3 수용력(capacity)

  • 예약된(미리 준비된) 연속적인(consecutive) 메모리 공간의 크기
  • 길이와 수용력
  • 수용력 >= 길이
  • 요소가 추가되면 길이는 커지고 결국 수용력과 같아짐
  • 길이과 수용력이 같은 상태에서 요소가 추가되면 Go 런타임이 더 큰 수용력을 가지는 새로운 슬라이스를 할당, 원본 슬라이스의 값들은 새 슬라이스에 복사됨, 추가된 값은 새 슬라이스에 append되고 이 새 슬라이스가 반환된다.
  • 수용력이 1024 보다 작으면 2배 증가, 그렇지 않으면 25%씩 확장
  • cap() vs. len()

3.2.4 make()

  • 슬라이스에 저장될 요소 개수를 미리 알 수 있다면 make()를 사용해 효율적인 슬라이스 작업을 도모할 수 있다
  • make로 생성한 슬라이스에 append를 사용할 땐 주의할 것

3.2.5 슬라이스 선언

  • var data []int => nil

  • var data = []int{} => nil(X)

  • 슬라이스를 아래 방식 중 어떤 걸로 생성할 지 정리해 봅시다.(p.75)

  • 제로 슬라이스
  • 슬라이스 리터럴
  • make()

3.2.6 슬라이싱의 슬라이싱

  • 슬라이스 연산자(:)
  • 슬라이스로부터 슬라이스 생성
  • 슬라이스 연산자를 사용하면 복사본을 만들지 않고 메모리를 공유함

  • 슬라이싱과 append를 함께 사용하면 혼란이 가중됨

  • 하위 슬라이스의 수용력
    = 원본 슬라이스의 수용력 - 하위 슬라이스 시작 오프셋

  • 하위 슬라이스와 append를 아무 생각 없이 사용하면 혼란이 가중된다.

  • 의도치 않은 값 변경이 발생
  • 완전한 슬라이스 표현식(full slice expression)으로 해결
  • 완전한 슬라이스 표현식
    • 하위 슬라이스에 얼마나 많은 메모리를 공유할 것인지를 지정
    • 슬라이스 연산때 콜론을 한 번 더 사용하여 세번째 인자에 원본 슬라이스에서 하위 슬라이스의 마지막 요소의 위치 지정
    • 슬라이스 연산의 세 번째 인자의 값을 두 번째 인자의 값과 같도록 설정

3.2.7 배열을 슬라이스로 변환

  • 배열에 슬라이스 연산 적용
  • 메모리 공유 문제 존재

3.2.8 copy

  • copy() 내장 함수
  • copy의 첫 번째 인자는 슬라이스이어야 함

3.3 문자열과 룬 그리고 바이트

  • 문자열은 룬으로 이루어진 것은 아니다.
  • 문자열은 바이트의 시퀀스이다.
  • 문자열의 길이는 바이트 수
  • 문자열에 슬라이스 연산 사용 가능
  • 문자열은 수정불가(immutable)이므로 슬라이스의 메모리 공유 문제가 없음
  • 유니코드로 구성되므로 슬라이스를 했을 때 문자가 깨지는 우려가 있음
  • 문자열은 바이트 슬라이스 또는 룬 슬라이스로 변환 가능

3.4 맵

  • 순서 없는 데이터 처리 유용
  • (Key, Value) Pair
    [SYNTAX]
    var 변수명 map[키타입]값타입
  • 값: 어느 타입도 가능
  • 키: 몇 가지 제약
  • 생성
    1) nil 맵(제로 값 할당): map의 제로 값은 nil
    • nil 맵은 길이 0
    • nil 맵의 값을 읽으면 맵 값이 되는 타입의 제로 값
    • nil 맵에 값을 쓰려고 하면 패닉 발생
      2) 비어 있는 맵 리터럴: 비어 있는 맵 생성
    • nil 맵과 다르다
    • 길이는 0
    • 비어 있는 맵 리터럴이 할당된 맵을 읽고 쓸 수 있다.
      3) 값이 있는 맵 리터럴
    • 키와 값을 콜론으로 구분
    • 마지막 요소(키, 값)의 끝에 콤마(,)를 붙인다.

4) make() 함수로 생성

  • 맵의 요소 개수를 안다면
  • 길이는 0 (make()로 슬라이스 만드는 것과 다름)
  • 초기 지정 개수 이상으로 커질 수 있다.
  • 맵의 제로 값은 nil
  • 맵은 비교 불가능, 단 nil과 같은지 다른지는 비교 가능

3.4.1 맵 읽고 쓰기

  • := 연산자는 사용 불가
  • 아직 설정되지 않은 키에 할당된 값을 읽으면 값 타입의 제로 값이 반환된다.

3.4.2 콤마 OK 관용구(idiom)

  • 맵의 키에 대응되는 값이 없어도 제로 값이 리턴되지만
  • 맵에 키가 존재하는지 확인할 필요가 있을 때 주로 사용하는 패턴
  • 맵에 키가 없어서 제로 값이 반환되는 건지
  • 실제로 키가 있는데 해당 값이 제로인 건지

3.4.3 맵 (요소) 삭제

  • delete() 내장 함수 사용
  • 키가 존재하지 않거나 nil 맵인 경우 아무것도 일어나지 않음
  • delete() 함수는 반환값이 없음

3.4.4 맵을 셋(집합)으로 이용

  • 집합(셋)
  • Uniqueness, 순서 없음
  • Go는 집합형을 직접 지원하지 않고 맵을 통해 간접적으로 지원
  • 집합의 원소로 쓰고 싶은 타입을 맵의 키 타입으로
  • 맵의 값을 불리언으로 설정

3.5 구조체

  • 여러 데이터 타입을 한데 묶어서 다루고 싶을 때 사용
  • struct 키워드, type 키워드
  • 구조체는 사용자 정의 타입
  • 따라서 바로 사용할 순 없고
  • 1) 구조체 정의 -> 2) 구조체를 타입으로 하는 변수 선언
  • 구조체 항목들은 콤마로 구분하지 않는다
  • 구조체는 어떤 블록 레벨에서도 정의 가능
  • 구조체의 제로 값
  • 구조체를 구성하는 항목들의 제로 값
  • 구조체 리터럴
  • 첫 번째 방법
  • 구조체 항목 값은 구조체 정의한 순서대로 나열
  • 구조체 항목 값은 콤마로 구분하고 마지막 항목에서 콤마를 붙여야 함
  • 두 번째 방법
  • 맵 리터럴과 유사
  • 순서 무관
  • 생략할 경우 제로 값으로 설정
  • 위 두 방법을 혼용할 수 없음
  • 제로 구조체와 비어 있는 구조체는 차이점이 없다
  • 구조체의 멤버(항목)을 접근할 때는 인덱싱이 아니라 점 표기법을 사용

3.5.1 익명 구조체

  • 한 번만 사용할 구조체
  • type 생략 -> 구조체 이름이 없다.
  • 구조체 변수만 존재
  • 주요 용도
  • 마샬링, 언마샬링
  • 테스트 작성

3.5.2 구조체 비교와 변환

  • 구조체 비교는 항목에 따라 다름
  • 두 개의 구조체가 같은 이름, 순서, 타입으로 구성되어 있으면 구조체 간에 타입 변환 가능

chap. 04

블록, 섀도, 제어 구조

4.1 블록(block)

  • 선언문이 있는 각각의 공간
  • 관련 있는 문장을 묶은 것
  • 식별자의 스코프(scope)
  • 식별자의 Lifecycle(생명주기)
    : 블록 내에서 선언/정의될 때 식별자가 생성
    : 블록이 끝날때 식별자가 제거
  • 즉, 블록 내에서만 해당 식별자를 사용할 수 있고
    블록 밖에서는 해당 식별자를 접근할 수 없다
  • 패키지 블록
  • 함수 외부에 선언된 것들이 속함
  • :=은 사용 불가
  • 대문자로 시작하는 식별자는 패키지 외부에 노출(expose)됨
  • 노출된 식별자는 점 연산자를 통해 접근
  • .go 파일이 여러 개더라도 package 이름이 같으면 같은 패키지 블록에 포함된다
  • 파일 블록
  • .go 파일에 선언된 것들
  • import 문이 포함된 파일
  • 함수 블록
  • 로컬 변수: 매개변수(파라미터), 반환 변수도 포함
  • 모든 중괄호는 새로운(다른) 블록을 정의
  • 제어 구조도 자체의 블록을 가진다.
  • 유니버스 블록
  • 기본 타입, 내장 함수, true/false와 같은 미리 선언된 식별자가 정의된 블록
  • 다른 모든 블록을 포함하는 블록
  • 따라서 다른 블록에 의해 섀도잉될 가능성이 높다
  • 식별자 접근
  • 외부 블록에서는 내부 블록의 요소(식별자)에 접근 불가
  • 내부 블록에서는 자신과 외부 블록의 요소에 접근 가능

4.1.1 섀도잉 변수

  • 섀도잉(Shadowing)
  • 외부 블록과 내부 블록의 식별자가 같은 경우 내부 블록의 식별자로 처리
  • 내부 식별자에 의해 외부 식별자가 그림자에 가려 보이지 않음
  • := 연산자
  • 여러 변수를 := 연산자로 초기화할 때
    : 현재 블록에서 선언된 변수들만 재사용된다
  • 임포트된 패키지가 섀도잉되지 않도록 주의
  • 임포트한 패키지 이름과 같은 식별자를 선언하지 말 것

4.2 if 문

  • 조건을 괄호로 감쌀 순 있지만 감싸지 않는 것이 일반적
  • if 블록이나 else 블록내에서만 사용가능한 변수를 if 또는 else에서 바로 선언하는 방법

4.3 for 문

  • for 문의 4 가지 패턴
    1) C 언어와 동일한 방식의 for
  • 변수 초기화는 반드시 := 사용
  • for 에서 선언된 변수도 if 처럼 섀도잉 문제 발생 가능

4.3.1 완전한 for 구문
1) 선언부는 한 번만 실행
2) 조건부가 참일 경우에만 for 블록 실행, 거짓이면 for 문 종료
3) 증감부는 for 블록 실행이 끝나고 다시 2)으로 가기전에 실행

4.3.2 조건식만 사용하는 for 문

  • for 문을 while 문처럼 사용

4.3.3 for 문을 이용한 무한루프

  • for 문의 조건식을 생략한 형태

4.3.4 break와 continue

  • 파이썬과 같은 개념
  • 코드를 간결하게 보기좋게 만드는데 기여

4.3.5 for-range 문

  • 내장 타입의 요소(문자열, 배열, 슬라이스, 맵)를 순회하며 루프 수행
  • range가 두 개의 값을 반환
  • (index, value) 쌍

    cf.) Go에서는 반환되는 값을 사용할 의도나 필요가 없다면 _(underscore)로 받는다. (Python과 같은 개념)

  • 키 값만 순회하는 대표적인 예
  • 맵 타입을 집합 타입으로 사용한 경우
  • 맵의 값보다는 유니크한 키 값이 중요 ⇒ 집합(셋)
  • 맵을 for-range로 순회
  • 요소를 처리하는 순서가 일정하지 않다 (보안 이슈)
  • 문자열을 for-range 순회
  • 룬을 순회, 바이트 순회 아님
  • for-range의 값은 복사본
  • for-range 값으로 원본을 수정할 수 없다
  • 레이블링(labeling)
  • 레이블: 프로그램 코드 위치에 이름 붙인 것
  • break, continue, goto 문의 대상이 됨

4.3.7 4 가지 중 알맞은 for 문 선택

4.4 switch 문

  • switch, case, default, [break]
  • 다른 언어의 switch
  • fallthrough 기능 존재
  • Go는 fallthrough가 기본적으로 금지, fallthrough 키워드로 활성화
    [SYNTAX]
    switch [selector] {
    case value|condition:
    문장1
    문장2
    ...
    case

case

}

  • switch의 fallthrough
  • break 문을 만날때 까지 다른 case의 내용도 실행

4.5 공백 스위치

  • case에 불리언 결과(조건식, 불값을 반환하는 함수)를 사용합니다. selector 없음

4.6 if 문과 switch 문 중 선택

4.7 goto 문

  • Go에서 지원은 하지만 없다고 생각하세요

chap. 05

함수

5.1 함수 선언과 호출

  • 1급 객체
  • main 함수
  • 프로그램 진입점
  • 실행 파일을 만들려면 필요
  • 인자 X
  • 반환값 X
  • 함수 선언 = 함수 헤더(시그니처) + 함수 몸체(바디)
    1) func 키워드
    2) 함수 이름
    3) 입력 파라미터: 반드시 타입 명시 (Go는 정적 언어)
    4) 반환값의 타입
    ⇒ 함수의 시그니처

  • return 키워드

  • 여러 입력 파라미터가 같은 타입이라면 콤마로 파라미터를 구별하고 타입은 마지막에 기술할 수 있다.

5.1.1 이름이 지정된 파라미터(named paramters)와 선택적 파라미터 대응

  • Go는 키워드 파라미터(=named paramter)를 지원하지 않음
  • 선택적 파라미터: 파라미터 기본값 X
  • Go는 함수를 호출할 때 함수에서 정의한 파라미터를 생략할 수 없음
  • 구조체를 사용하면 named parameter 및 선택적 파라미터 방식을 간접적으로 실현

5.1.2 가변 입력 파라미터와 슬라이스

  • 임의 개수의 입력 파라미터 처리

  • 가변 파라미터는 반드시 함수의 입력 파라미터 목록에서 마지막에 위치

  • 타입 이름 앞에 ...

  • 가변 파라미터는 함수 내에서는 해당 타입의 슬라이스이다

  • 슬라이스를 파라미터로 Unpack하려면 슬라이스뒤에 ...을 붙여준다

5.1.3 다중 반환값

  • 함수 정의시 반환값의 타입을 콤마로 구분하고 괄호로 묶어준다

  • return 시 반환값을 괄호로 묶지 않는다

  • 일반적으로 다중 반환값을 받을 때는 := 사용

5.1.4 다중 반환값은 다중값

  • 다중 반환값은 개별 변수로 받아야 함
  • 하나의 변수로 받으면 에러

5.1.5 반환되는 값 무시

  • 함수의 반환값 개수와 받는 변수 개수가 다르면 컴파일 에러
  • 일부만 무시하려면 _(unserscore) 사용
  • 모든 반환값을 무시할 수 있다

5.1.6 이름이 지정된 반환값(naked return)

  • 함수 정의 시 반환값 타입뿐만 아니라 반환값 파라미터 선언
  • 해당 함수의 로컬 변수로 간주
  • 코너 케이스
    1) 섀도잉 문제
    2) 해당 변수들을 반환할 필요가 없다.

5.1.7 빈 반환(blank return)

  • return 문에 반환값을 사용하지 않은 것
    1) 반환값이 없는 함수
    2) 이름 지정된 반환값을 반환하고자 할 때
    – 이때 return을 생략하면 컴파일 에러

5.2 함수는 값이다

  • 함수는 일급 객체다

5.2.1 함수 타입 선언

  • Go는 강타입 정적 타입 언어이므로
  • 함수를 저장하는 변수는 해당 함수의 타입으로 선언되어야 함
  • 함수 타입: type 키워드로 정의
    [SYNTAX]
    type 함수타입명 함수시그니처(함수 이름과 파라미터 이름 제외)

5.2.2 익명 함수

  • 이름 없는 함수 ⇒ 재사용 하지 않겠다 = 한 번만 쓴다
  • 변수에 저장할 용도가 아니므로 인라인으로 작성한다.
  • 익명 함수를 호출해야 하므로 인라인 작성에서 함수 몸체 정의가 끝나고나서 괄호를 사용하여 인자를 넘겨줘야 한다

5.3 클로저(Closure)

  • 함수 내부에 선언된 함수 = 내부 함수(inner function)
  • 외부 함수에 의해 반환되는 내부 함수, 이 내부 함수는 외부 변수를 참조함
package main
 
func nextValue() func() int {
    i := 0
    return func() int {
        i++
        return i
    }
}
 
func main() {
    next := nextValue()
 
    println(next())  // 1
    println(next())  // 2
    println(next())  // 3
 
    anotherNext := nextValue()
    println(anotherNext()) // 1 다시 시작
    println(anotherNext()) // 2
}

5.3.1 파라미터로 함수를 전달

5.3.2 함수에서 함수 반환

5.4 defer

  • 정리작업을 수행하는 함수 지정
  • 현재 코드 블록이 끝날 때 자동으로 실행
  • defer는 여러번 사용될 수 있다 -> 나중에 defer된 함수가 먼저 실행됨(LIFO)
  • defer가 적용된 함수 중 반환값이 있는 함수의 반환값은 따로 읽을 방도가 없다.
  • defer와 이름 지정된 반환값
package main
 
import "os"
 
func main() {
    f, err := os.Open("1.txt")
    if err != nil {
        panic(err)
    }
 
    // main 마지막에 파일 close 실행
    defer f.Close()
 
    // 파일 읽기
    bytes := make([]byte, 1024)
    f.Read(bytes)
    println(len(bytes))
}

5.5 값에 의한 호출을 사용하는 Go

  • Call by Value: 인자나 반환값은 복사된다
  • cf.) Call by Reference: 주소가 복사된다
  • 기본 타입, 구조체는 call by value
  • 파라미터로 넘어온 맵에 대한 변경은 원본에도 적용
  • 슬라이스
  • 길이 조정 외의 모든 조작이 원본에도 적용
  • 맵과 슬라이스는 포인터로 구현이 되었기 때문에 원본도 변경되는 것

chap. 06

포인터

6.1 빠른 포인터 입문

  • 포인터란?
  • 값이 저장된 메모리의 위치 값(주소)를 저장하는 변수
  • 주소 연산자(&)

  • 간접 연산자(*, dereferencing operator)

  • 모든 포인터는 어떤 타입을 가리키던간에 항상 같은 크기

  • 포인터의 제로 값: nil

    • 슬라이스, 맵, 함수, 채널, 인터페이스의 제로 값: nil
  • nil은 숫자 0이 아닙니다.

    • nil을 숫자로 변경/변환하거니 숫자를 반대로 변환할 수는 없다.
  • 포인터 dereferencing 전에 nil인지 확인할 필요 있다.

  • 포인터 타입

  • 포인터가 가리키는 값의 타입
  • 포인터가 어떤 타입의 값을 가리키는지 나타냄
  • 포인터 변수를 선언할 때 주로 사용
  • 타입 이름 앞에 *
  • cf.) 포인터 역참조: 변수 이름 앞에
    x := 10
    var pointerToX
    int
    pointerToX = &x
  • 내장 함수 new()
  • 포인터 변수 생성
  • 해당 타입의 제로 값을 가리키는 포인터 반환
  • 기본 타입의 리터럴이나 상수는 주소가 없으므로 주소연산자를 사용할 수 없다.
  • 헬퍼 함수를 사용하여 이 문제를 해결하자

6.2 포인터를 두려워 말라

  • Java의 데이터 타입
  • 원시 타입(Primitive Type): 8개
    : byte, short, int, long, float, double, bool, char: 값 그 자체, 객체가 아님, call by value
  • 클래스: 객체를 참조 변수로 참조해서 작업, call by reference
    : 원시 타입의 클래스: Byte, Short, Integer, Long, Float, Double, Boolean, Character
  • Go는 변수를 1)call by value로 할지 2)call by reference로 할지 선택할 수 있다.
  • 1) 함수에 변수를 그냥 넘기는 것
  • 2) 함수에 변수의 포인터를 넘기는 것

6.3 포인터는 변경 가능한 파라미터를 가리킨다

  • 비 포인터 타입: 기본타입, 배열, 구조체 => 원본의 불변성
  • 포인터: 함수로 전달되면 포인터의 복사본이 생성 => 원본에 닿을 수 있다. 수정할 수 있다.

6.4 포인터는 최후의 수단

  • 포인터
  • 데이터 흐름 이해를 어렵게 한다
  • GC에게 추가 작업 부하를 건다 (포인터를 이용하면 주로 힙을 이용)
    : 가비지는 힙에 생성

6.5 포인터로 성능 개선

  • 포인터는 모든 데이터 타입을 함수로 전달할 때 상수 시간이 걸린다.
  • 주소를 함수에 전달하므로, 모든 데이터 타입의 주소 길이는 같다.
  • 대략 1메가 바이트 전달 기준으로
  • 이상이면 포인터로 전달
  • 이하면 값 전달

6.6 제로 값과 값없음의 차이

  • 0, ‘ ‘ vs. NULL, ‘’ ⇒ Go: nil
    값이 있음 vs. 값이 없음

  • 포인터를 이용하여 변수나 구조체의 항목의 값이 제로 값인지 없는 값인지 구분하는데 사용

  • 할당되지 않은 변수나 구조체 항목에 nil 포인터를 사용

  • 또한, 포인터는 변경가능함을 나타내므로 함수에서 nil 포인터를 직접 반환하는 것보다 콤마 OK 관용구를 사용하자

  • nil 포인터를 함수의 파라미터나 구조체의 항목의 값으로 담아서 함수의 인자로 넘기면 nil 포인터를 통해서는 값을 저장할 수 없으므로 함수 안에서 값을 설정(간접 연산자 *를 사용하는 것)할 수가 없음을 명심

6.7 맵과 슬라이스의 차이

6.8 버퍼 슬라이스

  • 메모리 용도에 따른 구분
  • 버퍼(buffer)
  • 캐시(cache)
  • 풀(pool)

6.9 가비지 컬렉션 작업량 줄이기

  • 버퍼를 이용하면 GC의 부하를 줄일 수 있다
  • 가비지(Garbage)
  • 더 이상 어떤 포인터도 가리키지 않는 데이터

chap. 07

타입, 메서드, 인터페이스

7.1 Go의 타입

  • 내장 타입
  • 기본 타입, 복합 타입
  • 구조체를 이용한 사용자 정의 타입
  • 구체적인 타입(구체 타입, concrete type)
    • cf.) abstract type
      : abstract = general, super, 미완성(상속)
      <-> concrete = specific, sub, 완성(상속)

7.2 메서드

  • 타입(type)을 위한 메서드
  • 반드시 패키지 블록 레벨에서 정의해야 함
  • 리시버(receiver)
  • func 키워드와 메서드 이름 사이에 리시버_이름 타입을 괄호 감싸서 정의
  • 리시버 식별자는 관례적으로 타입 이름의 짧은 약어인 첫 문자를 사용
package main
 
//Rect - struct 정의
type Rect struct {
    width, height int
}
 
//Rect의 area() 메소드
func (r Rect) area() int {
    return r.width * r.height   
}
 
func main() {
    rect := Rect{10, 20}
    area := rect.area() //메서드 호출
    println(area)
}

7.2.1 포인터 리시버와 값 리시버

  • 포인터 리시버: 리시버의 타입 앞에 *을 붙인다
package main

import (
	"fmt"
	"time"
)

type Counter struct {
	total       int
	lastUpdated time.Time
}

func (c *Counter) Increment() {
	c.total++
	c.lastUpdated = time.Now()
}

func (c Counter) String() string {
	return fmt.Sprintf("total: %d, last updated: %v", c.total, c.lastUpdated)
}

func main() {
	var c Counter
	fmt.Println(c.String())
	c.Increment()
	fmt.Println(c.String())
}
  • 결정하는 규칙 (p.183)
  • 메서드가 리시버를 수정 => 반드시 포인터 리시버 사용
  • 메서드가 nil 인스턴스를 처리할 필요 => 반드시 포인터 리시버 사용
  • 메서드가 리시버를 수정하지 않음 => 값 리시버 사용 가능
package main
import (
	"fmt"
	"time"
)
type Counter struct {
	total       int
	lastUpdated time.Time
}
func (c *Counter) Increment() {
	c.total++
	c.lastUpdated = time.Now()
}
func (c Counter) String() string {
	return fmt.Sprintf("total: %d, last updated: %v", c.total, c.lastUpdated)
}
func doUpdateWrong(c Counter) {
	c.Increment()
	fmt.Println("in doUpdateWrong:", c.String())
}
func doUpdateRight(c *Counter) {
	c.Increment()
	fmt.Println("in doUpdateRight:", c.String())
}
func main() {
	var c Counter
	doUpdateWrong(c)
	fmt.Println("in main:", c.String())
	doUpdateRight(&c)
	fmt.Println("in main:", c.String())
}
  • 이것은 타입에 선언된 다른 메서드에 따라 결정
  • 같은 타입에 다른 리시버가 포인터 리시버라면 리시버를 수정하지 않는 메서드라도 포인터 리시버 사용 <= 일관성을 위해
// 포인터 Receiver
func (r *Rect) area2() int {
    r.width++
    return r.width * r.height
}
 
func main() {
    rect := Rect{10, 20}
    area := rect.area2() //메서드 호출
    println(rect.width, area) // 11 220 출력
}

7.2.2 nil 인스턴스를 위한 메서드 작성

  • 포인터 메서드 사용
  • nil 인스턴스: 슬라이스, 맵, 포인터 등
  • 위 타입의 제로 값이 nil

package main

import (
	"fmt"
)

type IntTree struct {
	val         int
	left, right *IntTree
}

func (it *IntTree) Insert(val int) *IntTree {
	if it == nil {
		return &IntTree{val: val}
	}
	if val < it.val {
		it.left = it.left.Insert(val)
	} else if val > it.val {
		it.right = it.right.Insert(val)
	}
	return it
}

func (it *IntTree) Contains(val int) bool {
	switch {
	case it == nil:
		return false
	case val < it.val:
		return it.left.Contains(val)
	case val > it.val:
		return it.right.Contains(val)
	default:
		return true
	}
}

func main() {
	var it *IntTree
	it = it.Insert(5)
	it = it.Insert(3)
	it = it.Insert(10)
	it = it.Insert(2)
	fmt.Println(it.Contains(2))
	fmt.Println(it.Contains(12))
}

7.2.3 메서드도 함수이다

  • 메서드를 변수에 할당하거나 함수의 입력으로 반환값으로 사용 가능
    ⇒ 메서드 값(method value)
  • 메서드 (표현)식
    ⇒ 타입 자체로 함수 생성
  • 메서드 표현식을 이용하여 메서드를 호출할 때는 첫 번째 인자로 메서드 리시버를 사용
public class Sample {
    int sum(int a, int b) {
        return a + b;
    }

    public static void main(String[] args) {
        int a = 3;
        int b = 4;

        Sample sample = new Sample();
        int c = sample.sum(a, b);

        System.out.println(c);  // 7 출력
    }
}

7.2.4 함수와 메서드 비교

7.2.5 타입 선언은 상속되지 않는다

  • 다른 타입 기반으로 (사용자 정의) 타입 선언
  • 상속이 아니다.
  • 계층이라는 개념이 없다.
  • 타입들은 서로 동등한 레벨이다.
  • 기본 타입이 내장 타입인 사용자 정의 타입의 경우 해당 기본 타입의 연산자와 함께 사용할 수 있다.
  • 또한 기본 타입과 호환되는 리터럴 및 상수를 대입할 수 있다.

7.2.6 타입은 실행가능한 문서

  • 사용자 타입을 선언하는 시점
    1) 구조체의 경우 우리가 필요한 시점
    2) 기존 타입을 기반으로 사용자 정의 타입을 선언하는 것은
    • “타입은 문서이다”를 기준으로 삼자
    • 개념을 위한 이름을 타입 이름으로 지정하여 코드를 더 명확하게 하고 기대되는 데이터의 종류를 기술할 목적으로

7.2.7 열거형을 위한 iota

  • 열거형(enumeration, enum)
  • 카테고리성 데이터, 명목형 데이터를 나타내기 위함
    : 도시 - 서울, 부산, 인천, ...
    이메일 종류 - 미분류, 스팸, 소셜, 비즈니스, ...
package main

import "fmt"

type State uint

const (
	StateCreated State = iota  // 0
	StateInitialized           // 1
	StateRunning               // 2
	StateStopped               // 3
)

func main() {
	var state = StateRunning
	fmt.Printf("State: %v\n", state) // prints State: 2
}
  • 기본 값은 정수이지만 그리고 정수를 사용(연산)할 수는 있지만 권장하지 않음
package main

import "a"

// func from the outside that handles a.base via a.Baser
// since a.base is not exported, only exported bases that are created within package a may be used, like a.A, a.C, a.T. and a.G
func HandleBasers(b a.Baser) {
    base := b.Base()
    base.OtherMethod()
}


// func from the outside that returns a.A or a.C, depending of condition
func AorC(condition bool) a.Baser {
    if condition {
       return a.A
    }
    return a.C
}
  • Go에서 열거형 만들기
    1) 모든 유효한 값을 나타내는 정수 기반의 타입 정의
    2) 값의 집합을 만들기 위해 const 블록 사용
    3) const 블록에서 1)에서 선언한 타입의 첫 번째 상수에 iota 지정

7.3 구성(composition)을 위한 임베딩 사용

  • composition over inheritance
    : 클래스 상속보다는 객체 구성 선호

7.4 임베딩은 상속이 아니다

  • 명시적으로 접근
package main

import (
	"fmt"
)

type Employee struct {
	Name string
	ID   string
}

func (e Employee) Description() string {
	return fmt.Sprintf("%s (%s)", e.Name, e.ID)
}

type Manager struct {
	Employee
	Reports []Employee
}

func (m Manager) FindNewEmployees() []Employee {
	// do business logic
	return nil
}

func main() {
	m := Manager{
		Employee: Employee{
			Name: "Bob Bobson",
			ID:   "12345",
		},
		Reports: []Employee{},
	}
	var eFail Employee = m        // compilation error!
	var eOK Employee = m.Employee // ok!
}
  • 동적 디스패치
package main

import "fmt"

type Inner struct {
	A int
}

func (i Inner) IntPrinter(val int) string {
	return fmt.Sprintf("Inner: %d", val)
}

func (i Inner) Double() string {
	result := i.A * 2
	return i.IntPrinter(result)
}

type Outer struct {
	Inner
	S string
}

func (o Outer) IntPrinter(val int) string {
	return fmt.Sprintf("Outer: %d", val)
}

func main() {
	o := Outer{
		Inner: Inner{
			A: 10,
		},
		S: "Hello",
	}
	fmt.Println(o.Double())
}

7.5 인터페이스

  • 암묵적 인터페이스
    : Go의 유일한 추상 타입

  • 인터페이스의 목적/용도

  • 스펙을 정의
  • 인터페이스를 사용하려면 인터페이스의 요소를 완성/구현(implement)해야 함
  • 이를 통해 타입의 안정성을 높이고 디커플링을 가능(솔로천국 커플지옥)
  • interface 키워드

  • 인터페이스 리터럴

  • interface {
    ...
    }
  • 위 ...에는 인터페이스를 만족시키기 위한 구체적 타입에서 반드시 구현(implement)해야 할 메서드 시그니처 나열

  • Go에서 관례적으로 인터페이스 이름은 끝에 ‘er’을 붙인다

  • 인터페이스는 모든 블록에서 선언 가능

7.6 인터페이스는 타입에 안정적인 덕 타이핑이다.

  • 덕 타이핑
  • 컴퓨터 프로그래밍 분야에서 덕 타이핑(duck typing)은 동적 타이핑의 한 종류
  • 객체의 변수 및 메소드의 집합이 객체의 타입을 결정하는 것
  • 클래스 상속이나 인터페이스 구현으로 타입을 구분하는 대신, 덕 타이핑은 객체가 어떤 타입에 걸맞은 변수와 메소드를 지니면 객체를 해당 타입에 속하는 것으로 간주
function calculate(a, b, c) => return (a+b)*c
a = calculate (1, 2, 3)
b = calculate ([1, 2, 3], [4, 5, 6], 2)
c = calculate ('apples ', 'and oranges, ', 3)
print to_string a
print to_string b
print to_string c

7.7 임베딩과 인터페이스

7.8 인터페이스를 받고 구조체 반환하기

  • 디자인 패턴: program to interface not to implementation(함수)
  • Go: 인터페이스를 받고 구조체를 반환해라
    => 함수로 실행되는 비즈니스 로직은 인터페이스를 통해 실행
    => 함수의 출력은 구체 타입이어야 함

7.9 인터페이스와 nil

7.11 타입 단언과 타입 스위치

  • 인터페이스 변수에 할당된 구체 타입 확인

  • 구체 타입이 다른 인터페이스를 구현했는지 확인

  • 타입 단언(Type Assertion)

  • 인터페이스 변수.(타입)
  • 타입 스위치 = 인터페이스.(type) + switch 문

chap. 10

Go의 동시성(Concurrency)

10.1 동시성 사용 시점

  • 동시성은 병렬성이 아니다
  • 동시성 코드가 병렬적(동시에)으로 실행되는지는 하드웨어와 알고리즘에 의해 결정된다
  • 동시성은 동시에 실행되는 것이 시간이 얼마 걸리지 않을 때 사용하는 것은 좋지 않다.
  • 동시성은 공짜가 아니다

10.2 고루틴

  • 프로세스
  • 스레드
  • OS의 스케줄러 및 Go 런타임의 스케줄러
  • Go 런타임 스케줄러의 장점(p.279~280)
  • 고루틴: go 키워드
  • Go 런타임에서 관리하는 가벼운 프로세스
  • 비즈니스 로직을 구현한 클로저를 고루틴으로 실행하는 것이 관례

cf) 동기(synchronous) vs. 비동기(asynchronous)
ex) 동기 통신 vs 비동기 통신

10.3 채널

  • 고루틴(스레드)은 채널을 통해 통신한다.
  • make 함수로 생성
  • 내장 타입
  • 채널의 제로 값은 슬라이스, 맵, 포인터 처럼 nil

0개의 댓글