-
🏃♂️ [Effective Go] Go를 Go답게 사용하는 방법| 프로그래밍 분야/Go 2021. 10. 25. 19:08
#Go #GoLang
출처 : https://golang.org/doc/effective_go
본 포스팅은 Go의 기본적인 문법사항이 아닌, Go를 Go답게 사용하는 코드 컨벤션에 대해 다루고 있어요.
포맷팅(Formatting)
모든 언어에서 그렇듯 포맷팅은 아주 중요한 이슈는 아니지만, 협업과 가독성의 측면에서 은근히 신경쓰이는 요소가 아닐 수 없어요
Go는 언어 자체에서 지원하는 Formatter인 gofmt를 통해 일관된 포맷을 유지할 수 있어요
type T struct { name string // name of the object value int // its value }
예를 들어, gofmt는 위와 같은 코드를 다음과 같이 자동으로 정렬할 거에요
type T struct { name string // name of the object value int // its value }
Go의 포맷팅 컨벤션에 관한 요약은 다음과 같아요
- 들여쓰기
기본값으로 탭(tabs)을 사용하며, 꼭 써야 하는 경우에만 스페이스(spaces) 사용 - 한 줄 길이
한 줄 길이에 제한이 없으나, 너무 길게 느껴진다면 별도의 탭을 들여써서 감싸기 - 괄호
C와 Java에 비해 적은 수의 괄호가 필요하고, 그렇게 설계하기
주석
Go 언어는 C style의 /* */ 블럭주석과 // 한줄주석을 제공해요
일반적으로 한줄주석이 많이 사용돼요
블럭주석은 대부분의 패키지 주석에 사용되며, 표현식 안이나 많은 코드를 주석처리 할 때에도 유용해요
Go의 기본 프로그램인 godoc은 패키지의 내용에 대한 문서를 추출하도록 소스 파일을 처리해요
최상위 선언문 이전에 여백 없이 주석이 나타나면 해당 항목의 설명으로 추출되어 문서화돼요
이러한 주석의 스타일과 유형은 godoc이 만들어내는 문서의 질을 결정하게 돼요
※ 주의 : godoc은 기본적으로 public 요소(대문자로 시작하는 타입, 함수, 상수, 변수)만 보여줘요
패키지 주석
모든 패키지(package)에는 패키지 구문 이전에 블럭주석의 형태로 패키지 주석이 있어야 해요
단, 여러 파일로 구성된 패키지의 경우 하나의 파일에만 있으면 돼요
패키지 주석은 패키지 소개 및 상세를 작성해야 해요
/* Package regexp implements a simple library for regular expressions. The syntax of the regular expressions accepted is: regexp: concatenation { '|' concatenation } concatenation: { closure } closure: term [ '*' | '+' | '?' ] term: '^' '$' '.' character '[' [ '^' ] character-ranges ']' '(' regexp ')' */ package regexp
패키지 주석의 좋은 예시
간혹 C나 Java의 api-doc를 보면 특수기호(-, |, *)를 이용한 decoration이 있는데, godoc문서의 경우 지양하는 것이 좋아요. 문서의 출력이 고정폭 폰트로 주어지지 않을 수도 있으므로, 스페이스나 정렬 등에 크게 의존하지 않기로 해요.
함수 주석
주석은 해석되지 않는 일반 텍스트에요. HTML이나 _this_같은 주석은 작성된 그대로 표현되므로 지양하기로 해요
주석에서 신경써야 할 부분은 다음과 같아요.
- 정확한 철자
- 구두법
- 문장구조
- 문장 길이의 간소화
- 완전한 문장
또한, 첫 문장은 선언된 이름으로 시작하는 한 줄짜리 문장으로 요약됨이 좋아요
// Compile parses a regular expression and returns, if successful, // a Regexp that can be used to match against text. func Compile(str string) (*Regexp, error) {
함수 주석의 좋은 예시
그룹 주석
Go언어의 선언구문은 그룹화가 가능해요(import, var, ...)
이를 활용하여 관련된 상수 또는 변수의 그룹 등에 대해 설명할 수 있어요
// Error codes returned by failures to parse an expression. var ( ErrInternal = errors.New("regexp: internal error") ErrUnmatchedLpar = errors.New("regexp: unmatched '('") ErrUnmatchedRpar = errors.New("regexp: unmatched ')'") ... )
그룹 주석의 좋은 예시
명명법
다른 언어들과 마찬가지로(그 이상으로) Go에서 "이름"은 중요해요. 위에서 명시했듯, 이름의 첫 문자가 대문자인지 아닌지에 따라 public 여부가 결정되기도 해요.
패키지명
- 패키지가 임포트되면, 패키지명은 패키지 내용들에 대한 접근자가 돼요.
import "bytes"
위와 같은 경우, bytes.Buffer 를 사용할 수 있어요.
접근자로 이용되는 만큼, 패키지명은 짧고, 간결하고, 연상하기 쉬운 단어로 작성되어야 해요
또한 접근자로 사용되므로, 패키지 내부의 함수는 패키지명과 중복되지 않는 것이 사용자 경험을 높여줄 수 있어요
ex) bufio.BufReader(x) bufio.Reader(o)
- 관례적으로, 패키지명은 소문자, 한 단어로만 부여해요. 즉, 언더바(_)나 대소문자 혼용에 대한 필요가 없어야해요
- 패키지명은 소스 디렉토리 이름 기반으로 작성돼요
ex) src/encoding/base64
=> import "encoding/base64"
게터와 세터(Getters & Setters)
Go는 getters와 setters를 자체적으로 생성해주지 않고, 직접 선언해야 해요. 하지만 getter의 이름에 Get을 넣는건 Go언어답지 못한 방법이에요.
owner라는 private 필드(첫 문자가 소문자)를 가지고 있다면 getter 메서드는 GetOwner가 아닌 Owner(첫 문자가 대문자, public)라고 불러야 해요. 단, setter 함수는 SetOwner와 같이 명명해도 좋아요.
owner := obj.Owner() if owner != user { obj.SetOwner(user) }
인터페이스명
관례적으로, 하나의 메서드를 갖는 인터페이스는 메서드 이름에 -er 접미사를 붙이거나 에이전트 명사를 구성하는 유사한 변형에 의해 지정돼요.
ex) Reader, Writer, Formater, CloseNotifier 등
여러 단어로 된 이름의 명명
Go의 네이밍 규칙은 snake case가 아닌 camel case를 따르고 있어요
ex) close_notifier (x) CloseNotifier (o)
세미콜론
C언어와 같이, Go의 정식문법은 구문 종료를 위해 세미콜론을 사용한다. 하지만 구문분석기(lexer)가 소스 코드를 스캔하며 자동으로 세미콜론을 삽입해주기 때문에, 개발자는 다음 두 경우를 제외하고 세미콜론을 신경쓰지 않아도 돼요.
- for loop 구문에서 변수 초기화와 조건, 진행 변수를 구분할 때
- 한 라인에서 여러 문장을 사용할 경우
switch
Go 언어에서 스위치는 C언어에서 보다 더 일반적인 표현이 가능해요. 표현식은 상수이거나 정수일 필요가 없고, case 구문은 위에서부터 바닥까지 해당 구문이 true가 아닌 동안에 일치하는 값을 찾을 때까지 계속 값을 비교해요
따라서 if-else-if-else 형태로 작성하는 것 대신 switch를 사용하는 것이 더 Go 다워요
변수 type 체크
동적으로 할당된 변수의 type에 따라 다르게 처리해줘야 하는 경우가 있을 수 있어요
다음과 같이 switch문을 이용해서 type check를 해줄 수 있어요
func main() { var t interface{} t = functionOfSomeType() switch t := t.(type) { default: fmt.Printf("unexpected type %T\n", t) // %T prints whatever type t has case bool: fmt.Printf("boolean %t\n", t) // t has type bool case int: fmt.Printf("integer %d\n", t) // t has type int case *bool: fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool case *int: fmt.Printf("pointer to integer %d\n", *t) // t has type *int } } func functionOfSomeType() (ret int) { ret = 4 return }
[출력]
integer 4이차원 slice
Go의 배열과 slice는 일차원적이에요. 따라서 이차원의 배열이나 slice를 만들기 위해서는 배열의 배열 혹은 slice의 slice를 다음과 같이 정의해야 해요
type Transform [3][3]float64 // A 3x3 array, really an array of arrays. type LinesOfText [][]byte // A slice of byte slices.
이 때, 이차원 slice의 각 row를 할당해주기 위한 방법에는 두 가지가 있어요
각각의 장단점이 있으니 상황에 따라 취사선택하는것이 좋아요
- 반복문을 돌며 각 row에 메모리 할당
- (row의 total size를 아는 경우) total size의 slice 한 번 할당 후, 각 row마다 잘라서 주소 부여
1번의 예시는 다음과 같아요
picture := make([][]uint8, YSize) for i := range picture { picture[i] = make([]uint8, XSize) }
장점 : 각 row의 길이가 가변일 때, 다음 row를 덮어쓰는 일을 방지해요
단점 : 메모리 할당은 system call 함수이기 때문에, 호출이 많을수록 효율이 나빠요
2번의 예시는 다음과 같아요
picture := make([][]uint8, YSize) pixels := make([]uint8, XSize * YSize) for i := range picture { picture[i], pixels = pixels[:XSize], pixels[XSize:] }
장점 : system call이 두 번으로 끝나므로 효율이 좋아요
단점 : row의 길이가 가변인 경우에 대한 대처가 어려워요
- 들여쓰기