Go 언어를 배우게 되면서 홈페이지에서 읽은 내용을 정리해 볼까 한다.
(https://go.dev/doc/effective_go)
Redeclaration and reassignment
- 보통 := 이 형태 의 축약형태로 특정 값을 정의하곤 한다.
func test01(a int) (int,error) {
return a,errors.New("에러 발생")
}
func main(){
a,err := test01(1)
fmt.Println(a,err)
}
Go의 다양항 기본 라이브러리에서도 위와 같은 방식의 형태를 취하곤 한다. 다만 여기서 b, err의 같은 값을 새로운 형태로 받아서 정의하고자 한다면 어떻게 해야 할까? := 형태를 이용해서 정의한다는 것은 해당 벨류에 메모리 주소를 부여하고 초기화까지 진행한다는 의미이다.
통상 일반적인 언어에서는 = 를 이요해 재정의 를 할 것이다. 물론 Go에서도 가능하다. 다만 하나의 벨류값이 아닌 리턴을 받으면서 재정의하려고 한다면 컴파일 에러 undefined value 가 발생한다.
func test01(a int) (int,error){
return a,errors.New("에러발생")
}
fucn main(){
a,err := test01(1)
b,err := test01(2)
c,err = test01(3) // error
}
= 방식을 이용해서 재정의 한다면 컴파일 에러가 발생한다. 위와같이 초기화와 같이 동시에 진행할 때는 같은 블록 스코프 안에 있다면 := 이와 같은 형태를 이용한다면 기존값에는 대체, 새로운 값에는 초기화 가 진행된다.
이러한 형태를 go 라이브러리 에서 자주 볼 수 있는데 if else 체인에서 err를 핸들링할 때 자주 사용되는 방법 중하나이다.
또한 이런 형태는 {} 스코프 밖에서 진행한 변수에 대해서도 영향을 받는다
Named result parameter
리턴 벨류 의 값 자체를 정의해서 반환하는 형태를 종종 볼 수 있다.
꼭 이렇게 리턴벨류 값 자체 의 정의를 하는 것이 필수적인 것이 아니지만, 이것은 보다 짧고 명확한 코드를 의미한다. 예를 들어보자.
//return 을 벨류값으로 정해서 사용할때
func ReadFull(r Reader, buf []byte) (n int, err error) {
for len(buf) > 0 && err == nil {
var nr int
nr, err = r.Read(buf)
n += nr
buf = buf[nr:]
}
return
}
// 그냥 사용할때
func ReadFull(r Reader, buf []byte) (int, error) {
var (
n int
err error
)
for len(buf) > 0 && err == nil {
var nr int
nr, err = r.Read(buf)
n += nr
buf = buf[nr:]
}
return n,err
}
편한 걸 골라서 사용하면 된다. java에서는 통상 아래와 같은 방식으로 사용하고 return으로 끝내고 싶다면 static을 이용한 변수를 사용했던 것 같다.
Defer
Go를 배우면서 가장 인상 깊었던 키워드이지 않을까 싶다. defer 키워드 가 달린다면 함수의 로직이 전부 실행되고 defer 키워드 로직을 마무리 지으면서 defer 함수가 종료된다.
Doc에서 는 주로 리소스 반환을 할 때 사용하는 것을 예로 들고 또 권장한다고 한다.
// Contents returns the file's contents as a string.
func Contents(filename string) (string, error) {
f, err := os.Open(filename)
if err != nil {
return "", err
}
defer f.Close() // f.Close will run when we're finished.
var result []byte
buf := make([]byte, 100)
for {
n, err := f.Read(buf[0:])
result = append(result, buf[0:n]...) // append is discussed later.
if err != nil {
if err == io.EOF {
break
}
return "", err // f will be closed if we return here.
}
}
return string(result), nil // f will be closed if we return here.
}
파일을 열고, 그 결괏값을 버퍼에 넣어 string으로 반환하는 함수이다.
위와 같이 defer를 이용해 작성했을 때의 이점은 무엇일까? 그냥 함수 리턴전에 닿아주면 되는 거 아닌가?
1. 리소스 반환을 까먹지 않고 할 수 있다.
2. 코드를 보다 명확하게 읽을 수 있다.
위와 같은 짧은 코드 라면 그렇게 가치 없는 키워드라고 생각할 수 있으나 함수의 크기가 커진다면? 충분히 고려할 사항이다.
defer의 특징으로는 Lifo 구조이다. 스택이다.
아래 예제를 확인해 보자.
func trace(s string) string {
fmt.Println("entering:", s)
return s
}
func un(s string) {
fmt.Println("leaving:", s)
}
func a() {
defer un(trace("a"))
fmt.Println("in a")
}
func b() {
defer un(trace("b"))
fmt.Println("in b")
a()
}
func main() {
b()
}
이렇게 출력했을 때 우리 함수의 콜스택에 어떻게 쌓이는가?
trace("b")는 먼저 실행되고 이거에 대한결과 값에 대한 로직이 defer로 구분된다. 다시 말해 구분해서 적어보자면
- fucntion = trace("b") / fmt.Println("in b") / trace("a) / fmt.Println("in a )
- defer = un(결괏값(trace("a")) / un(결괏값(trace("b"))
이와 같이 function 이 실행되고 이후 defer 라인이 실행된다. 실제로 그려보면서 따라가면 보다 이해가 쉬워진다.
Allocation with new
Go에서는 메모리 할당 을 하는 두 가지 빌트인 함수가 있다. make, new인데 new부터 알아보자.
다른 언어와 달리 new 키워드를 사용한다면 단순 메모리 할당만 있을 뿐 초기화를 진행해 주지 않는다. 즉 0 이란 의미이다.
또한 new 함수를 이용해서 만든 반환값은 메모리 주소를 반환해 준다.
var a = new(customType) => *customType의 메모리주소 타입이다.
다르게 생각해 본다면? 0의 값이니깐 이 인스턴스 값을 채우기 위한 코드도 필요할텐데 어떻게 핸들링 하는지 마저 읽어보자.
이렇게 0의 값을 리턴 하기 때문에 사용자가 직접 빌드해서 사용하기 좋다고 한다. 데이터를 0 부터 구조화시켜 나가는 이점에 대해서 말해준다.
예시로 들어준 SyncedBuffer 타입을 보면 안에 buffer 필드가 있는데 이 또한 0 의 벨류 이기 때문에 즉각적으로 다음 코드를 빌드해갈수 있다.
p := new(SyncedBuffer) // type *SyncedBuffer
var v SyncedBuffer // type SyncedBuffer
위의 코드 들은 추가적인 조정 없이 바로 사용이 가능하다. 다시 생각해 보면 위와 같이 타입으로 변수를 선언해도 0 값 과 메모리가 할당된다는 의미로 받아들여진다. 다만 포인터 값 과 그냥 값이 주어진다면 함수를 이용할때 복사 가 일어나는 부분에서 주의가 필요하다.
Constructors and composite literals
위에서 제시한 문제점을 해결하기 위한 방법으로 제공된다.
return &File{fd, name, nil, 0}
자바의 인스턴스 생성과 유사하다. 다만 이렇게 작성한다면 메모리 주소 타입을 반환하지 않기에 따로 위와같이 붙여주어 new의 기능을 상실하지 않게 만든다.
추가적으로 file struct의 필드 값이 모두 정해진 값이 아니라면 생략 가능하다.
return &File{fd: fd, name: name}
다시 말해 new(File)과 &File {}의 타입 은 같으며 동일하게 동작한다.
또한 위와 같은 형태로 초기화 하는 것 은 고에서 제공하는 원시 데이터 타입들 또한 받아서 작성가능하다
a := [...]string{0: "no error", 1: "Eio", 10: "invalid argument"}
s := []string{0: "no error", 1: "Eio", 100: "invalid argument"}
m := map[int]string{1: "no error", 4: "Eio", 5: "invalid argument"}
위와 같은 방식으로 작성할 수 있다. 슬라이스 와 배열에서 위와 같이 숫자를 적어주면 해당 인덱스에 값을 부여하는 방식으로
3개 의 값을 적었지만 cap는 11 이 된다.
Allocation with make
new 와는 달리 make 함수는 slice, chanel, map 만 만들 수 있으며, 메모리 주소 타입 이 아닌 진짜 값을 반환하는 것이 가장 큰 차이점이다. 이와 같은 구조의 이유는 위의 3가지 타입은 데이터의 구조를 레퍼런스 하기 때문에 사용 전 반드시 초기화 작업이 필요하기 때문이다.
var p *[]int = new([]int) // allocates slice structure; *p == nil; rarely useful
var v []int = make([]int, 100) // the slice v now refers to a new array of 100 ints
// Unnecessarily complex:
var p *[]int = new([]int)
*p = make([]int, 100, 100)
// Idiomatic:
v := make([]int, 100)
통상적으로 맨 아래 방식을 가장 많이 사용한다.
make 함수를 이용해서 작성하는 것은 항상 값타입을 리턴하는 것 을 알고 사용하자.
'Go > Go Basic' 카테고리의 다른 글
Ulitmate-Go-01 (string,메모리 패딩) (3) | 2023.08.19 |
---|---|
Go Interface, embedded (0) | 2023.02.28 |
Effective Go 04 (0) | 2023.02.12 |
Effective Go 03 (0) | 2023.02.10 |
Effective Go 02 (0) | 2023.02.09 |