본문 바로가기
Programming/GO

Effective Go에 대해 - 2

by 코인선물로부자된다 2023. 1. 2.
반응형

https://go.dev/doc/effective_go

 

Effective Go - The Go Programming Language

Documentation Effective Go 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 straigh

go.dev

Data

Allocation with new

new는 메모리를 할당하며 초기화하지 않고 0으로 할당
new(T)는 타입 T에 대해 0으로 초기화된 메모리 공간의 주소(*T)를 리턴
type SyncedBuffer struct {
lock sync.Mutex
buffer bytes.Buffer
}

p := new(SyncedBuffer) // type *SyncedBuffer
var v SyncedBuffer // SyncedBuffer 유형

Constructors and Composite Literals

new처럼 메모리를 할당하면서 0할당 외에 초기화 되어야 하는 것이 필요할 수 있음

func NewFile(fd int, name string) *File {
    if fd < 0 {
        return nil
    }
    f := new(File)
    f.fd = fd
    f.name = name
    f.dirinfo = nil
    f.nepipe = 0
    return f
}
func NewFile(fd int, name string) *File {
    if fd < 0 {
        return nil
    }
    f := File{fd, name, nil, 0}
    return &f
}

지역 변수의 주소를 반환가능

return &File{fd, name, nil, 0}
return &File{fd: fd, name : name} //return &File{fd: fd, name : name, _0 , _0 }
복합리터럴의 빈 값은 0으로 대체

표현식
new(File) == &File(){}

복합 리터럴은 arrays, slices, maps 가 유용
a := [...]string {"a":"a", "b":"b"}
b := []string {"a":"a", "b":"b"}
c := map[int]string {"a":"a", "b":"b"}

Allocation with make

new와 다른 점은 slice, map, channel만 생성 가능
make는 메모리를 할당한 후, 0으로 초기화 하지 않고 별도의 초기화 과정을 거친 뒤
*T 가 아닌 T 타입의 값을 리턴
slice, map, channel은 사용 전에 초기화되어야 하는 데이터 구조에 대한 참조를 하기 떄문에 new와는 차별을 둠

var p *[]int = new([]int)       // 슬라이스를 할당하지만 *p는 nil이다, 유용하지 않음
var v  []int = make([]int, 100) // int 100칸을 가지는 슬라이스
추천하는 형식 
v := make([]int, 100)

Arrays

배열은 값, 하나의 배열을 다른 배열에 할당하면 모든 요소가 복사
특히, 배열을 함수에 전달하면 배열에 대한 포인터가 아닌 배열의 복사본을 전달 받음
Arrays 보다 Slices를 추천

Slices

배열을 래핑하여 데이터의 순서보다 일반적이고 강력
슬라이스의 경우에 기본 배열에 대한 참조를 보유하고 동일한 배열을 참조함
슬라이스 요소에 대한 변경 사항은 기본 배열에 대한 포인터를 전달하는 것과 유사하게 표시

func (f *File) Read(buf []byte) (n int, err error)
n, err := f.Read(buf[0:32])

슬라이스 길의의 경우, 배열의 길이내라면 변경 가능하며 최대 길이를 초과할 경우,
슬라이스를 재할당해서 사용함

func Append(slice, data []byte) []byte {
    l := len(slice)
    if l + len(data) > cap(slice) {  // reallocate
        // Allocate double what's needed, for future growth.
        newSlice := make([]byte, (l+len(data))*2)
        // The copy function is predeclared and works for any slice type.
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0:l+len(data)]
    copy(slice[l:], data)
    return slice
}

Two-dimensional slices

Slice는 가변길이인데, 가변길이의 배열에 가변길이이므로 최상위 Slice에 각 내부 Slice는 길이가 다를 수 있고, 독립적인 길이를 가짐

type Transform [3][3]float64  // A 3x3 array, really an array of arrays.
type LinesOfText [][]byte     // A slice of byte slices.

좋은예 2개가있는데,

  1. 최상위 슬레이스를 먼저 할당하고 내부 슬라이스를 할당하는 방법
  2. picture := make([][]uint8, YSize) for i := range picture { picture[i] = make([]uint8, XSize) }
  3. 한번에 할당하는 방법
  4. picture := make([][]uint8, YSize) pixels := make([]uint8, XSize*YSize) for i := range picture { picture[i], pixels = pixels[:XSize], pixels[XSize:] }

Maps

Key - Value의 데이터 구조 형식으로 일반적인 복합 리터럴 구문을 사용하여 초기화중에 구성

var timeZone = map[string]int{ 
  "UTC": 0*60*60, 
  "EST": -5*60*60, 
  "CST": -6*60*60, 
  "MST": -7*60* 60, 
  "PST": -8*60*60, 
}

Map에 없는 값을 참조하려고하면 Return Type에 맞춰서 0, false 등등 반환

attended := map[string]bool{
    "Ann": true,
    "Joe": true,
    ...
}

if attended[person] { // will be false if person is not in the map
    fmt.Println(person, "was at the meeting")
}

[예외 처리가 필요한 부분] 시간, 돈값 등등 0값과 구분이 필요한 경우, OK 관용구

func offset(tz string) int {
    if seconds, ok := timeZone[tz]; ok {
        return seconds
    }
    log.Println("unknown time zone:", tz)
    return 0
}

Blank Identifier ( _ )

_, preset := timeZone[tz]

Delete 시,

delete(timeZone, "PDT")

Printing

  1. C의 printf와 유사하게 동작 - 더 풍부한 기능
    1. Printf - 기본 프린트, 포맷을 지정
    2. Fprint - fprintf 데이터를 형식에 맞춰서 스트림에 작성 (스트림에 쓴 문자의 개수 반환)
    3. Println - println 라인을 추가
    4. Sprint - sprintf 버퍼에 문자와 값의 형식을 지정 (문자 총 개수 반환)
  2. 확장 기능
    1. long, unsigned와 같은 리터럴 접미사를 사용하지 않아도 됨 → %d로 모든걸 처리
    2. %x, %X가 integer → 16진수 출력이 아닌 string, array, byte slice에도 사용 가능
  3. 새로운 기능
    1. %v를 통해 slice, struct, map등의 값을 편하게 출력 → for문과 &을 통해 요소 하나하나를 출력할 필요가 없음
    2. %q와 `을 사용해서 인용 문자열 포맷 출력이 가능
    3. %T를 사용해 타입 출력이 가능
  4. 주의 점
  5. 타입을 명확하게 해줘야 함 (%s는 모든 걸 다 받을 수 있기 때문에)
// 무한루프 발생
type MyString string
func (m MyString) String() string {
    return fmt.Sprintf("MyString=%s", m) // Error: will recur forever.
}

type MyString string
func (m MyString) String() string {
    return fmt.Sprintf("MyString=%s", string(m)) // OK: note conversion.
}

Initialization

  1. Constants
    1. https://go.dev/ref/spec#Constants
    2. 컴파일 시 생성
      1. 컴파일러가 인지할 수 있는 형식이여야 함
      2. 런타임에 실행되는 함수와 같은 것들은 상수가 될 수 없음
      3. import "math" func main(){ Sin := math.Sin(math.Pi/4) }
    3. 숫자, 문자, 문자열, boolean만이 상수가 가능 (sturct, map 불가능)
    4. itoa를 통해 enum 처럼 사용이 가능 (열거형 상수)
func main() { 
    const ( 
        c0 = iota 
        c1 = iota 
        c2 = iota 
    ) 
    fmt.Println(c0, c1, c2) 
    //Output: 
    //0 1 2 
}

2. Variables

  1. https://go.dev/ref/spec#Variables
  2. 런타임에 계산되는 표현식

https://google.github.io/styleguide/go/

Go Style Guide 배경과 목적

  1. 언어를 처음 접하는 사람들이 실수를 피할 수 있도록 쉬운 Go를 작성해서 추측을 최소화 하기 위한 것
  2. 팀의 코드 베이스는 차이가 있기마련이고, 가능한 일관성을 유지하는 것이 좋음

Style Guide는 기초, 규범적이고 정식이므로 봐야하는 부분이지만
Style Decisions, Best Practices는 정식까지는 아니므로
단순히 Go를 도구로 이용하는데만 목적성을 둔다면 Style Guide만 봐도 충분

Go Style Convension를 만든 배경과 목적

  • Go 스타일의 문제를 코드화합니다.
  • Go 관용구에 대한 표준 예제 문서화 및 제공
  • 다양한 스타일 장단점 문서화
  • Go 가독성 리뷰
  • 멘토가 일관된 용어 및 지침을 사용하도록 지원

얻고자하는 목표

표준화하여 규범적이고 일관성을 확보하면서 일반적이면서 친숙하게 만들자

추가적인 참조사항

Go Programming Language Specification

https://go.dev/ref/spec

  • 밑줄 문자 _(U+005F)는 소문자로 간주

Go Memory Model

https://go.dev/ref/mem

Go Data Structures

https://research.swtch.com/godata

Go Interfaces

https://research.swtch.com/interfaces

Go Proverbs

https://go-proverbs.github.io/

Go Style Guide
자료 : https://google.github.io/styleguide/go/guide

Go Style Decisions
자료 : https://google.github.io/styleguide/go/decisions

Go Best Practices
자료 : https://google.github.io/styleguide/go/decisions

Go Style Guide

golang style의 5가지 원칙

1. clarity - 명확하게 전달
2. simplicity - 간단해야 된다.
3. concision - 간결해야 한다. high signal to noise - 필요한 부분만
4. maintainable -  유지보수가 쉬워야된다.
5. consistency - 코드 작성의 일관성

clarity (명확하게 전달)

  • Naming / 효율적인 주석 / 효율적인 코드 구성이 clarity에 영향을 끼친다.
  • 코드는 작성보다 읽기 쉬운게 중요

명확하게 전달하는데 중요한 2가지

  1. 코드가 실제로 무엇을 하고있는가?
    1. 서술적인 naming
    2. 추가 주석
    3. 공백과 주석으로 코드를 분할
    4. 별도의 함수 / 메서드로 모듈화
    5. 코드를 처음보는 사람은 코드의 목적과 활용방안을 처음으로 생각할 것이고, 코드의 행위가 명확해야지 이해가 쉬워진다.
  2. 코드가 수행되는 이유는?
    1. 너무 중요한 주석 중 하나라고 생각됨
    2. 언어상의 이유
    3. 비지니스관점에서의 이유
    4. 주석은 코드에대한 동작설명보다 왜 실행해야되는지 설명하는게 좋을때가 많음

simplicity (간단해야 한다.)

  1. 사용, 읽기 유지보수가 간단해야된다.

고려해야될 사항

  1. 순서대로 읽기 편해야됨
  2. 무엇을 하는지 알고있다고 가정x
  3. 모든과정의 코드를 알고있다고 가정x
  4. 불필요한 추상화 수준이 없음
  5. 불필요한 특별한 네이밍x
  6. 전달하고자하는걸 독자에게 명확하게 전달
  7. 무엇을 수행하는지가 아닌, 왜 수행하는지에대한 주석
  8. 코드에 대한 문서화
  9. useful error & test code
  10. clever한 코드랑 반대될수있음, 최적화만이 좋은코드를 아님을 의미

api의 사용성과 code의 간편성은 서로 트레이드오프 가능성이있음 단순한 사용방법과 복잡한 사용방법을 제공함에 따라서 복잡한 구조를 이해할수있는 무언가를 제공해야된다.

내부 로직(API)이 복잡해지는 것과 외부에서 처리하는 것의 차이

최소한 지켜야될 사항들

  1. 여러가지 표현이 있을때는 표준의 방법을 선택
  2. 사용 사례가 없을경우, 최대한 언어의 핵심적인 구성을 사용(채널, 슬라이스, 맵 등등)
  3. 없을경우 표준 라이브러리
  4. google 코드베이스에 사용하고있는 라이브러리가 있는지 확인
    1. 값을 비교하는 bool을 판별할때는 map string[bool]이면 충분한데, 값을 비교하는 패키지는 더욱 복잡한 경우에 사용해야한다.

concision (간결해야 한다.)

  1. 전달하고자하는 것만 간결하게 코드에 넣는것. signal to noise ratio

방해요소

  1. 반복되는 코드 - 각 코드의 의미 파악이 어려워지며, 코드간의 차이점을 일일히 찾아봐야되는 수고 (좋은 예시는 테스트 테이블을 이용한 테스트)
코드를 구성하는 여러가지 방식이 있을때, 코드들의 의미가 분명해지는 방식으로 구현해야된다.

if err := doSomething(); err != nil {
    // ...
}
if err := doSomething(); err == nil { // if NO error
    // ...
}

f, err := os.Open(name)
if err != nil {
    return err
}
d, err := f.Stat()
if err != nil {
    f.Close()
    return err
}
codeUsing(f, d)
  1. 관련없는 신택스
  2. 불명확한 이름
  3. 불필요한 추상화
  4. whitespace

와같이 약간의 변화가 있을때 추가적인 주의를 줌으로 코드를 명확히 할 수 있다.

maintainablility (유지보수가 쉬워야한다.)

  1. 작성되는 회수보다 읽혀지는 회수가 더 많기 때문에 일기가 쉬워야된다, 그리고 그것은 유지보수에 직접적 영향을 준다.

유지보수가 쉬워지는 코드란?

  1. 미래의 프로그래머가 수정하기 쉽다.
  2. 정상적으로 올바른 api (각종 패턴들이 적용된)
  3. 코드의 구조가 아닌 문제의 구조에 매핑되는 추상화를 진행한다.
  4. 불필요한 결합x, 사용 되지 않는 기능x
  5. 약속된 기능을 할 수 있도록 단위 Test Code를 작성한다.

interface/ type으로 추상화할때 이점이 명확한가 확인해야된다. 인터페이스는 강력하지만, 세부사항을 이해가 필요한 부분 코드의 이해에 load가 생김.

// Bad  
if user, err = db.UserByID(userID); err != nil {  
// ...  
}
// Good:  
u, err := db.UserByID(userID)  
if err != nil {  
return fmt.Errorf("invalid origin user: %s", err)  
}  
user = u
// Bad:  
// The ! in the middle of this line is very easy to miss.  
leap := (year%4 == 0) && (!(year%100 == 0) || (year%400 == 0))
// Good:  
// Gregorian leap years aren't just year%4 == 0.  
// See [https://en.wikipedia.org/wiki/Leap\_year#Algorithm](https://en.wikipedia.org/wiki/Leap_year#Algorithm).  
var (  
leap4 = year%4 == 0  
leap100 = year%100 == 0  
leap400 = year%400 == 0  
)  
leap := leap4 && (!leap100 || leap400)

좀더 중요한 부분을 떼어내거나, 변경가능성의 여지를 열어주는 것이 중요하다.

  • 핵심로직이나 엣지케이스를 helper함수로 랩핑하게된다면, 추후 수정이 될때 놓칠 가능성이 있다.
  • 이름이 역할을 예측하는데 도움을 주는 이름. -> 동일한 개념을 가진 함수들은 동일한 이름을 갖도록 코딩해야된다.
  • 너무 많은 종속성은 유지보수의 난이도를 향상시키다.
  • 또한 유지보수성을 높이는 변경의 유연성을 증가시키는 복잡한 구조는 오히려 좋다.

consistency (일관성이 있어야 한다.)

  • 일관성
  • 유사한 코드의 형태- 코드의 파악이 매우 쉬워진다.
  • Effective Go 를 참조해라

core guidance (핵심 지침 사항)

  1. formating = go.fmt
  2. camelCase vs snakeCase
  3. tap으로 시작줄이 바뀔때는 여러줄로 쓰지말고 한줄로 써라
  4. naming을 반복적이지 않고, context를 고려하고, 명확한 것을 반복x -> 추가페이지 존재