본문 바로가기
Language/Go

Effective Go - New 와 Make

by ocwokocw 2022. 5. 7.

- 출처: https://go.dev/doc/effective_go#data

- New

Go에서 할당에 관한 primitive에는 new와 make 2 가지 built-in 함수가 존재한다. 
 
new는 메모리를 할당한다. 다른 언어의 new와는 다르게 메모리를 초기화하지 않고, 단순히 "zero"화 한다. new(T) 는 T형의 새로운 요소를 위해 zero화된 공간을 할당하고, type *T 값인 해당 주소를 반환한다. Go 용어로 type T의 새롭게 할당된 zero 값에 대한 pointer를 반환한다.
 
new에 의해 반환된 메모리가 zero화 되기 때문에, 추가적인 초기화 없이 각 type의 zero값이 사용될 수 있는 data 구조를 설계할 때 유용하다. 이를 잘 사용할 경우 해당 data 구조를 사용하는 유저가 new 한번으로 생성하고 난 후, 추가 작업 없이 곧바로 사용할 수 있는 편의성을 제공할 수 있다. 예를 들면 bytes.Buffer 의 문서에는 "Buffer의 zero 값은 사용할 준비가 완료된 빈 buffer 이다."라고 명시되어 있다. 비슷하게 sync.Mutex는 명시적인 생성자나 Init 메소드가 없다. sync.Mutex의 zero 값은 unlock된 상태의 Mutex로 간주된다.
 
아래의 struct type을 선언했다고 가정해보자.
 
type SyncedBuffer struct {
    lock    sync.Mutex
    buffer  bytes.Buffer
}
 
SyncedBuffer 형의 값들은 new를 이용한 할당이나 단순한 선언을 한 경우에도 즉시 사용이 가능한 상태가 된다. 아래 코드에서 p나 v 둘 다 추가적인 작업없이도 사용이 가능하다.
 
p := new(SyncedBuffer)  // type *SyncedBuffer
var v SyncedBuffer      // type  SyncedBuffer
 

- Constructors and composite literals

가끔은 zero 값으로 충분하지 않아서 초기화 생성자가 필요한 경우가 있다. 아래 코드는 os package 코드에서 차용한것이다.
 
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
}
 
위의 코드는 boiler plate 가 많다. 이런 경우 평가할때마다 새로운 instance를 생성하는 표현식인 composite literal을 사용해서 단순화 시킬 수 있다.
 
func NewFile(fd int, name string) *File {
    if fd < 0 {
        return nil
    }
    f := File{fd, name, nil, 0}
    return &f
}
 
C 언어와 다르게 변수와 관련된 저장공간이 함수가 반환된 뒤에도 살아남기 때문에 local 변수의 주소를 완벽하게 반환할 수 있다. 사실 composite literal 의 주소를 받으면 평가될때마다 새로운 instance를 할당하므로 마지막 2 라인은 아래와 같이 작성할 수 있다.
 
return &File{fd, name, nil, 0}
 
composite literal의 field들은 순서대로 배치되며 모두 존재해야 한다. 하지만 명시적으로 field:value 쌍 요소들을 기술하면 순서가 변경되어도 무방하며 누락된 항목은 zero 값이 되므로 아래와 같이 사용이 가능하다.
 
return &File{fd: fd, name: name}
 
composite literal 에 아무런 field도 포함하지 않으면 해당 type의 zero 값을 만든다. 따라서 new(File)과 &Field{}은 동치이다. Composite lieral은 array, slice, map에 대해 생성될 수도 있으며 field 라벨은 index 또는 map key가 된다. 

- Allocation with make

built-in 함수 make(T, args)는 new(T)와 다른 목적을 지니고 있다. make는 slice, map, channel 만 생성하며, type T(*T가 아닌)의 초기화된 값(zero는 아닌)을 반환한다.
 
이런 차별점이 있는 이유는 3가지 유형이 사용전에 초기화되어야 하는 data 구조에 대한 참조를 다루기 때문이다. 예를 들어 slice는 data에 대한 pointer, 길이, 용량에 대한 3 가지 기술자를 포함하고 있으며 해당 요소가 초기화될때까지 slice는 nil이다. slice, map, channel의 경우 make 함수를 사용해서 내부 data 구조를 초기화하여 사용을 위한 값을 준비한다.
 
make([]int, 10, 100)
 
예를 들어 위의 코드는 100 개의 int 배열을 할당한다음 배열의 처음 10개 요소를 가리키는 길이가 10이고, 용량이 100인 slice 구조체를 생성한다. (slice를 만들 때 용량(capacity)는 생략이 가능하다.) 반대로 new([]int)는 nil값을 가리키는 pointer인 새롭게 할당된 zero화된 slice 구조체  pointer를 반환한다.
 
아래 예제는 new와 make의 차이를 설명해주는 예제이다.
 
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는 map, slice, channel에만 사용이 가능하며 pointer를 반환하지 않는다. 명시적으로 pointer를 얻고 싶다면 new를 이용해서 할당하거나 변수의 주소값을 취해야 한다.
 

 

 

'Language > Go' 카테고리의 다른 글

Effective Go - Methods  (0) 2022.05.17
Effective Go - Array and Slice  (0) 2022.05.08
Effective Go - Functions  (0) 2022.05.07
Go - Reading files  (0) 2022.03.13
Go - Context  (0) 2022.02.27

댓글