공대생 정리노트

Effective Go (1) 요약 본문

언어/Go

Effective Go (1) 요약

woojinger 2021. 10. 18. 09:54

원글

https://golang.org/doc/effective_go

 

Effective Go - The Go Programming Language

Effective Go Introduction Go is a new language. Although it borrows ideas from existing languages, it has unusual properties that make effective Go programs different in character from programs written in its relatives. A straightforward translation of a C

golang.org


Formatting

gofmt가 Go 프로그램을 들여쓰기, 수직 줄맞춤, 주석 reformatting 등을 맞춰준다.

대부분의 개발용 에디터는 gofmt로 스타일을 정돈해주는 플러그인이 존재한다.

터미널로 gofmt를 사용할 경우 gofmt -w prog.go로 정돈을 할 수 있다

 

포맷팅 세부사항을 간단하게 쓰면 다음과 같다

Indentation

tab을 default로 사용한다.

Line length

Go는 line limit이 없다. 길다고 느낀다면 감싸고 추가탭을 이용하면 된다

Parentheses

C나 Java에 비해 괄호 사용이 적다. 

 

Commentary

// Line Comments
/*
	block comments
*/

godoc는 Go 소스파일에서 패키지의 주석을 바탕으로 documentation을 뽑아낸다

주석은 top-level declaration 전에 와야 하고, 줄바꿈이 그 사이에 있으면 안된다.

 

모든 패키지는 package 구문 이전에 package comment를 가지는 것이 조다.

package가 여러 파일로 이루어져 있다면, package comment는 한 파일에만 있으면 된다.

/*
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

gofmt처럼 godoc가 포맷팅해서 보여주므로 폰트나 spacing에 신경을 쓸 필요가 없다.

 

패키지안에서 top-level declaration 전에 나오는 주석은 doc 주석이 된다.

프로그램에서 외부로 공개되는(대문자로 쓰여진) 이름들은 doc 주석을 작성해야 한다.

 

doc 주석은 완성된 문장으로 쓰는 것이 좋고, 첫 문장의 첫 단어는 선언되는 이름으로 시작해야 한다

// 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 doc -all regexp | grep -i parse
    Compile parses a regular expression and returns, if successful, a Regexp
    MustCompile is like Compile but panics if the expression cannot be parsed.
    parsed. It simplifies safe initialization of global variables holding

 

Names

첫 글자가 대문자이면 외부에서 볼 수 있음

 

Package names

package가 import될 때 package 이름은 accessor가 된다.

import "bytes"

이 이후에는 bytes.Buffer로 사용할 수 있다.

 

package 이름은 소문자, 한단어로 쓰여져야 한다. underscore나 mixedCap이 있어선 안된다.

package 이름 충돌은 걱정을 안해도 된다. package 이름은 import를 위한 default 이름이다.

모든 소스 코드에서 유일할 필요는 없다. 드물게 충돌이 발생해도 로컬에서 다른 이름으로 선택해 쓸 수 있다.

 

(찾아본 내용 - 아래처럼 다른 이름으로 import 할 수 있음)

https://golang.org/ref/spec#Import_declarations

import (
    "text/template"
    htemplate "html/template" // this is now imported as htemplate
)

 

pacakge 이름은 source directory의 베이스 이름이다. src/encoding/base64의 package는 "encoding/base64"로 import 되지만 base64라는 이름을 가진다.

 

package를 import할 때는 package 이름을 이용하기 때문에 반복을 피할 수 있다. 예를 들어 bufio package의 Reader는 BufReader로 이름이 지어지지 않았다. bufio.Reader로 사용자가 쓰기 때문에 Reader로 이름을 짓는 것이 깔끔하다

 

다른 예로는 once.Do(setup)이 once.DoOrWaitUntilDone(setup)으로 짓는 것 보다 나은 것을 들 수 있다. 긴 이름이 자동으로 readable하게 만들지 않는다. 긴 이름보다 helpful한 doc 주석을 작성하는 것이 낫다

 

Getters

Go는 autmatic하게 getter와 setter를 제공하지 않는다.

스스로 getter와 setter를 만들어도 괜찮고, 그게 나을 수도 있다.

그러나 getter의 이름에 get을 넣는 것은 자연스럽거나 필요하지 않다.

owner(소문자, 노출 X)라는 field를 가지려면 geter method는 Owner(대문자, 노출 O)로 되어야 한다.

setter의 경우 필요하다면 SetOwner로 짓는 것이 낫다

owner := obj.Owner()
if owner != user {
    obj.SetOwner(user)
}

Semicolons

C처럼 statement를 종료할 때 semicolon을 사용하나 lexer가 스캔할 때 자동으로 넣어주기 때문에 소스 코드에 넣어주지 않아도 된다.

statement를 끝낼 수 있는 토큰 뒤에 newline이 온다면 semicolon을 집어넣는다.

 

보통 Go에서는 for loop 구문에서 initializer, condition, continuation을 구분하기 위해 semicolon을 사용한다.

한 라인에서 여러 statement를 구분할 때도 사용한다.

 

semicolon 규칙에 따라 if, for, switch, select와 같은 control structure의 여는 괄호를 다음 라인에 사용하면 안된다.

만약 그렇게 한다면 semicolon이 괄호 뒤에 들어가게 되기 때문이다

if i < f() {
    g()
}

if i < f()  // wrong!
{           // wrong!
    g()
}

Control structures

Go는 C와 다르게 do나 while 문이 없다.

switch문은 좀 더 유연한데, if와 switch문은 for문 처럼 initialization을 사용할 수 있다.

 

If

if와 switch문은 initialization statement를 사용할 수 있기에 local 변수를 다음과 같이 셋팅하는 것은 흔하다

if err := file.Chmod(0664); err != nil {
    log.Print(err)
    return err
}

else statement가 없는 경우 생략해도 된다

f, err := os.Open(name)
if err != nil {
    return err
}
d, err := f.Stat()
if err != nil {
    f.Close()
    return err
}
codeUsing(f, d)

 

Redeclaration and reassignment

위의 예를 보면 err가 두 번 할당이 되는 것을 알 수 있음.

d, err := f.Stat() 에서 err는 reassign 되었다고 한다. 이 뜻은 f.Stat은 존재하는 err 변수를 사용하고 값을 새로 주는 것이다.

다음의 경우 변수 v가 선언이 되었더라도 v에 대해 := 를 사용할 수 있다

(밑의 3가지 조건을 모두 만족해야 하는 것 같다)

  • 이전의 선언과 현재 선언이 같은 스코프에 있는 경우. 만약 이전 선언이 바깥 스코프에 있으면 이 선언은 새로운 변수를 만든다
  • 선언하려는 값이 v에 할당 가능한 경우.
  • 현재 선언을 통해 만들어지는 변수가 v 이외에 최소 하나 이상인 경우

§ Go에서는 함수 파라미터와 return 값이 body 괄호 바깥에 있지만 body와 같은 스코프로 생각해야 한다

 

For

C의 for와 while을 합쳐놓았다. 3가지 형태로 사용할 수 있다

// Like a C for
for init; condition; post { }

// Like a C while
for condition { }

// Like a C for(;;)
for { }

array, slice, map, channel 등을 읽을 때면 range를 loop에 사용할 수 있다

for key, value := range oldMap {
    newMap[key] = value
}

for key := range m {
    if key.expired() {
        delete(m, key)
    }
}

sum := 0
for _, value := range array {
    sum += value
}

range과 string과 쓰이면 각각을 UTF-8으로 파싱해 유니코드로 쪼개준다.

 잘못 인코딩된 문자는 첫 바이트는 무시하고 U+FFFD로 바꾸어준다.

U+FFFD는 rune이라고 하고 Go에서 single Unicode point로 사용된다

for pos, char := range "日本\x80語" { // \x80 is an illegal UTF-8 encoding
    fmt.Printf("character %#U starts at byte position %d\n", char, pos)
}

/*
character U+65E5 '日' starts at byte position 0
character U+672C '本' starts at byte position 3
character U+FFFD '�' starts at byte position 6
character U+8A9E '語' starts at byte position 7
*/

Go는 comma operator를 지원하지 않고 ++와 --는 statement이지 expression이 아니다.

그래서 for에서 다중 변수를 사용하고 싶다면 parallel assignment를 사용해야 한다.

// Reverse a
for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {
    a[i], a[j] = a[j], a[i]
}

comma operator : 두 개의 표현식을 하나로 결합하는 연산자

ex) i=20, j = 2 * i;

 

Switch

switch의 표현식이 상수이거나 정수일 필요는 없다.

매치가 되는 케이스가 나올 때까지 위에서부터 아래로 차례대로 비교한다

switch 문이 표현식이 없으면 true로 처리되어 if-else문을 switch 문으로 바꾸는 것이 가능하다

func unhex(c byte) byte {
    switch {
    case '0' <= c && c <= '9':
        return c - '0'
    case 'a' <= c && c <= 'f':
        return c - 'a' + 10
    case 'A' <= c && c <= 'F':
        return c - 'A' + 10
    }
    return 0
}

case들을 콤마로 묶어서 분리된 리스트로 표현하는 것은 가능하다

func shouldEscape(c byte) bool {
    switch c {
    case ' ', '?', '&', '=', '#', '+', '%':
        return true
    }
    return false
}

Go에서도 break 문을 사용하여 switch를 일찍 끝낼 수 있다.

어떤 경우에는 switch 뿐만 아니라 둘러싸고 있는 루프를 끝낼 필요가 있을 때가 있다.

Go에서는 이 경우 loop에 라벨을 붙여 그 라벨로 break 할 수 있다

Loop:
	for n := 0; n < len(src); n += size {
		switch {
		case src[n] < sizeOne:
			if validateOnly {
				break
			}
			size = 1
			update(src[n])

		case src[n] < sizeTwo:
			if n+1 >= len(src) {
				err = errShortInput
				break Loop
			}
			if validateOnly {
				break
			}
			size = 2
			update(src[n] + src[n+1]<<shift)
		}
	}

continue도 label과 함께 사용될 수 있는데 loop일 때만 사용 가능하다

 

Type switch

switch문은 interface 변수의 동적 타입을 밝히기 위해 사용될 수 있다.

type switch는 괄호안에 type 키워드를 사용해 type assertion을 수행한다

아래 예처럼 switch 문에서 변수 이름을 재사용하면 이름은 같지만 각각 다른 타입으로 변수를 선언하는 것과 같은 효과이다

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
}

'언어 > Go' 카테고리의 다른 글

파일 I/O - write과 read시 파일 lock이 있을까?  (0) 2021.12.08
Effective Go (2) 요약  (0) 2021.11.01
동시성 패턴  (0) 2021.05.05
고루틴  (0) 2021.05.03
Server 함수 정리  (0) 2020.12.29
Comments