출처 : https://go.dev/doc/effective_go#append
Methods
Pointers vs. Values
Go 언어를 공부하면서 제일 고민이 많이 되었던 부분이다. 어떤 예제는 메서드로 어떤 예제는 그냥 함수로 설정해 리턴한다.
두 가지 방법을 모두 확인해 보자.
type ByteSlice []byte
func (slice ByteSlice) Append(data []byte) []byte {
// Body exactly the same as the Append function defined above.
}
func (p *ByteSlice) Append(data []byte) {
slice := *p
// Body as above, without the return.
*p = slice
}
둘 다 동일한 로직이다. 다만 표현의 차이인데 이에 대해 규칙을 아래와 같이 설명하고 있다.
1. 두 번째 함수는 포인터 그리고 값 모두에서 작동되지만, 값 메서드는 오직 값 타입에서만 함수를 호출할 수 있다 는 차이점이다. ChatGpt 가 준 예시를 보자.
type MyInt int
func (m MyInt) add(x int) MyInt {
return m + MyInt(x)
}
func (m *MyInt) addPointer(x int) MyInt {
*m += MyInt(x)
return *m
}
a := MyInt(5)
b := &a
// Value method can be invoked on a value
fmt.Println(a.add(10)) // 15
// Value method can also be invoked on a pointer
fmt.Println(b.add(10)) // 15
// Pointer method can only be invoked on a pointer
fmt.Println(b.addPointer(10)) // 25
// Pointer method cannot be invoked on a value
// This will result in a compile error: "cannot call pointer method on MyInt value"
// fmt.Println(a.addPointer(10))
여기서 주목할 점은 a , b의 타입이다 a는 값타입 이고, b 는 포인터 타입이다. b 는 값,포인트 메서드 각각을 호출 가능하지만
a 는 포인터 메서드 호출 시에 에러가 발생한다.
이러한 규칙에 따라 포인터메서드는 호출 값을 수정할 수 있다는 점 과 값메서드는 호출 시 새로운 값을 받는 차이점 이 존재한다.
하나 고에서는 편리한 예외를 적용해 주는데 주소를 지정할 수 있는 변수라면 포인터 타입의 메서드 또한 호출할 수 있다는 점이다.
즉 컴파일러가 이를 도와준다고 한다.
그러면 다시 생각해 보면 위에 예는 틀린 예제이다. chatgpt 한테 다시 물어봤다.
Okay thanks i see However in view of case on Effective go documentation they said "When the value is addressable, the language takes care of the common case of invoking a pointer method on a value by inserting the address operator automatically. In our example, the variable b is addressable, so we can call its Write method with just b.Write." if this is right your example is wrong, you would be able to call a.addPointer(10) as well.
Would you give me some more details?
Answer is :
Yes, I apologize for that mistake. You're correct, in Go, when the value is addressable (i.e., it has an address), you can call a pointer method on that value and Go will automatically insert the address operator.
In the example from the Effective Go documentation, b is a pointer to an addressable value, so you can call its Write method as b.Write. When you call a pointer method on a value, Go will automatically insert the address operator and convert the value to a pointer.
So, in our example, you could also call a.addPointer(10), and Go would automatically convert the value a to a pointer and call the addPointer method on that pointer. Here's an updated example to show this:
In this example, you can see that both b.addPointer(10) and a.addPointer(10) produce the same result.
잘못된 예시에 대해 사과를 해주는 게 정말 친절하다 ㅋㅋ
다만 나의 궁금증 은 해결되지 않았다. 어떤 상황에 대해 어떤 메서드를 써야 할지 에 대한 특별한 룰이나 나만의 기준을 정하고 싶어 추가적으로 좀 더 자료를 찾아보았다.
대체적으로 값은 변경을 원한다면 포인터 리시버를 그게 아니라면 벨류 리시버 를 사용한다고 한다. 추가적으로 포인터 리시버에 대한 고려사항으로 리시버 자체 의 크기가 너무 크다면? 이건 고려 의 여지없이 포인터 리시버를 사용해야 메모리 이슈가 없을 것이다.
팀장님 께 여쭤보니 글로벌한 함수에 대해서는 벨류를 보편적으로 사용하는 편이며 일관적으로 사용하는 것을 주로 한다고 하신다.
Interfaces and other types
Interfaces
인터페이스이다 자바에서 도 많이 보던 친숙한 키워드인데 고에서 는 어떻게 정의하는지 확인해 보자.
객체의 특정 행동 규약을 정의할 때 제공한다고 한다.
주로 1~2 개의 메서드를 정의하고, io.Writer의 Write를 예시로 들 수 있다.
(포스팅 은 안 했지만 주로 인터페이스 이름 규약으로는 뒤에 er을 붙인다.)
자바와 똑같이 하나의 타입은 다양한 인터페이스를 구현할 수 있다. 컬렉션 은 정렬될 수 있는데 sort의 구현체를 구현해야 한다.
Len() Less(i, j int) bool Swap(i, j int) 바로 예시를 들어 정렬을 해보자.
type twitter struct {
like int
title string
}
type twit []twitter
func (t twit) Len() int {
return len(t)
}
func (t twit) Less(i, j int) bool {
return t[i].like < t[j].like
}
func (t twit) Swap(i, j int) {
t[i], t[j] = t[j], t[i]
}
func main() {
a := twit{}
a = append(a,
twitter{249249, "Hi"},
twitter{123123, "Elon"},
twitter{1, "Musk"},
twitter{-1, "Paid for twit"},
)
fmt.Println(a)
sort.Sort(a)
fmt.Println(a)
}
[{249249 Hi} {123123 Elon} {1 Musk} {-1 Paid for twit}]
[{-1 Paid for twit} {1 Musk} {123123 Elon} {249249 Hi}]
프린트된 값을 보면 정렬된 것이 보이는가? 저기서 레스 함수를 반대로 꺾어준다면 역으로 정렬된다.
나름 직관적이라고 생각한다. 자바의 Comparable or Compartor 보다 마음에 든다.
Conversions
type Sequence [] int와 변환에 대해 설명하고 있다.
이 부분에서는 자바 인터페이스 Map a = HashMap <> 이런 식의 연관관계 가 생각난다. Map을 구현하고 있는 hashmap에 대해 타입으로 받을 수 있다?
Go에서는 위와 유사하게 형변환이 가능하다. 그래서 예제에서 들어준 sort에 대해 sequence의 len less 기타 등등 구현 없이 바로 가는 거를 보여준다.
type Sequence []int
// Method for printing - sorts the elements before printing
func (s Sequence) String() string {
s = s.Copy()
sort.IntSlice(s).Sort()
return fmt.Sprint([]int(s))
}
ㅋㅋㅋㅋ 별다른 구현 없이 이렇게 형변환 해서 들고 갈 수 있다.
저 함수 안으로 들어가면 이미 빌트인으로 다구현되어 있다 , 인트에 대해서
기존 함수 예제에서 String으로 리턴하기 위해 for를 한 번 더 돌리면서 sprint를 해 O(n) 2의 시간복잡도를 가지며
string을 만들어 갔지만 위와 같이 작성한다면?
o(nlogn)까지 줄어들 수 있다. s의 크기만큼 함수를 돌면서 정렬(고에서는 퀵정렬을 사용) 하기 때문에 o(nlogn) 이 된다.
Interface conversions and type assertions
타입의 변환에 대한 섹션이다. 문서에서 Printf에서 % v를 받을 때 어떻게 핸들링하는지에 대해 설명하고 있다.
type Stringer interface {
String() string
}
var value interface{} // Value provided by caller.
switch str := value.(type) {
case string:
return str
case Stringer:
return str.String()
}
value.(type)을 통해 가능한 타입을 케이스 별로 분기를 나눈다. 이는 다시 말해 혼합된 타입을 사용할 수 있는 걸 의미한다.
이렇게 해서 원하는 타입으로 뽑아낼 수 있다. 예를 들어
str := value.(string)
이렇게 스트링 타입으로 변환 후 변수 설정이 가능하다 이렇게 되면 str의 타입은 string 이 된다.
만약 이런 벨류에서 뽑아낼 타입이 없다면? 컴파일 에러가 아닌 런타임 에러가 발생하게 된다. 이에 따라 go에서는 아래와 같은 방법 이 관용적으로 사용된다고 한다.
func main(){
if str, ok := value.(string); ok {
return str
} else if str, ok := value.(Stringer); ok {
return str.String()
}
}
이 섹션에 대해서 잘 이해가 가질 않아서 검색을 해보다 보니 assertion과 conversion에 대한 개념을 먼저 잡아야 했다.
assertion => 벨류 의 인터페이스 안에서 하나의 타입을 끄집어내는 것이 assertion
conversion => 하나의 타입을 다른 타입으로 변환하는 것을 conversion이라고 한다.
아래 예시를 보자.
type MyType interface {
What() string
}
func PrintHolyMoly(t MyType) {
fmt.Println("HOlYMOLY", t.What())
}
type A struct {
a int
}
func (a A) What() string {
return fmt.Sprintf("This is A %v", a.a)
}
type B struct {
a int
}
func (b B) What() string {
return fmt.Sprintf("This is B %v", b.a)
}
func main() {
var a MyType
a = A{1} // Type asseriton worked so a is MyType right now
if b, ok := a.(B); ok { // Type assertion do one more for type conversion to a => b
a = b
} else {
fmt.Printf("Conversion type failed from MyType(A) to B))\n")
}
PrintHolyMoly(a)
}
interface MyType을 구현하는 A, B 가 있다. A를 B 타입으로 바꾸는 과정을 작성한 코드이다.
이렇게 타입을 바꾸기 위해서는 우선 타입추론을 통해 인터페이스 타입으로 뽑아 온다음 인터페이스 타입에서 다시 뽑아서 B로 바꾸는 것이 가능한지 확인 후 타입을 변환하는 과정이다.
다형성 때문에 내가 많이 헷갈린 거 같은데 다형성은 저렇게 interface 타입으로 생성한 함수(PrintHolyMoly(a))에서
저렇게 사용이 가능한 거지
A, B 가 동일한 인터페이스를 구현한다고 하더라도 A,B 간의 타입전환은 가능하나 중간 점검이 필요하다.
Interfaces and methods
인터페이스 매머드의 예로 핸들러 인터페이스를 든다 핸들러 인터페이스를 구현하고 있다면 http 요청을 처리할 수 있는 함수가 된다.
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
ResponseWriter는 Wirte 메서드가 있는데 이 메서드 덕분에 io.Wirter 가 들어가는 모든 값에 responseWriter 벨류를 집어넣을 수 있다 왜? 인터페이스 구현을 하고 있으니깐
serveHttp에 관련해 HandlerFunc을 조금 길게 설명하고 있다 그중 아래와 같이 타입을 시그니쳐 함수로 받을 수 있다. 예를 보자.
// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler object that calls f.
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, req).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) {
f(w, req)
}
The blank identifier
go에서는 선언한 값에 대해서는 무조건 적인 사용이 필수적이다 그렇지 않다면 컴파일 에러가 발생하는데 이를 해결하기 위해 _선언을 허용한다.
1. 사용되지 않는 임포트 => 이 임포트를 함으로써 init 함수가 실행되어야 프로그램 이 실행될 때
2. for, map을 loop 돌때 => 인덱스 값에 대해 이용하지 않을 때
1번 케이스를 임포트 사이드 이펙트라고 하는데 고의 기본 정책이니 다른 임포트 할 때 도 주의할 필요가 있다.
Interface checks
자바 같은 언어와 달리 고에서는 인터페이스의 함수만 구현하면 그 자체로 인터페이스 타입을 받을 수 있게 된다. 이에 따라 런타임 시점에 인터페이스에 서 타입을 끄집어내는 type assertion 이 가능하다. 예시로 제시하는 marshaler에 대해 알아보자.
우선 Go에서 엔코딩/디코딩, 마샬러 모두 비슷한 역할을 하는데 이에 대해 명확히 인지하고 사용해야 한다.
Encoding/Decoding 은 json의 문자를 스트링 또는 바이너리 데이터로 읽고 쓰기 하는 것을 의미한다.
Marshaling/UnMarshaling 은 JsonType으로부터 고 의 원시타입으로 맵핑하는 것 을 의미한다.
func main() {
var data Singer
var data2 Singer
jsonData := []byte(`
{
"Name" : "NewJeans",
"Title" : "Attention",
"DebutDate" : 2022
}
`)
verify := json.Valid(jsonData)
if verify {
json.Unmarshal(jsonData, &data)
fmt.Println(data)
}
json.NewDecoder(strings.NewReader(string(jsonData))).Decode(&data2)
fmt.Println(data2)
}
이에 동일한 결괏값을 받을 수 있다. 내부적으로 까보면 Decode 내에서 Unmarshal을 호출하고 있는 모습을 볼 수 있다. 즉 다시 말해
디코딩 은 버퍼에 들어있는 값을 들고 와 얼마샬 을 한다고 이해하면 될 거 같다.
349 업 버튼을 받은 답변 중에(https://stackoverflow.com/questions/21197239/decoding-json-using-json-unmarshal-vs-json-newdecoder-decode)
json.Decoder 는 나의 데이터가 io.Reader 에서 부터 온다면 혹은 스트림 데이터로부터 다양한 값을 디코딩해야 한다면 사용하고,
json.Unmarshal 은 이미 json 데이터가 메모리에 존재한다면 사용하라고 한다.
var _ json.Marshaler = (*RawMessage)(nil)
예시에서 이렇게 빈칸지시자를 활용해서 오로지 타입 체크만 할 때도 사용된다고 한다. 이는 type conversion 이기 때문에 컴파일 시점에 잡을 수 있다.
Embedding
고에서는 서브클래싱 의 타입 핸들? 드리븐? 개념을 제공하지 않는다? 무슨 의미인지 모르겠으나 고에서는 클래스 개념과 상속의 개념이 없어서 위와 같은 말을 한다고 생각된다. 이 문장 이후 제시된 대안으로
구조체 혹은 인터페이스의 임베딩 타입에 의해 구현된 일부분을 가져올 수 있다라고 강조한다. brrow라는 표현을 강조하는데 예시를 확인해 보자.
글을 읽어보면 자바의 class extend 가 생각난다.
subclass에서 superclass를 접근하는 개념과 유사하게 생각하면 될 거 같다 단지 구현의 차이만 있지 의미하는 바가 매우 유사하다.
글에서 제시된 io reader, writer 그리고 이걸 랩핑 하는 readwriter 가 있다면 reader와 writer에 readwriter 인스턴스가 접근할 수 있다.
예시를 보면 보다 이해가 확고해진다.
func main() {
type animal struct {
name string
age int
}
type cat struct {
animal animal
breed string
}
c := &cat{
animal{"somi", 12},
"Persian",
}
fmt.Printf("Breed is %v, name is %v , age is %v", c.breed, c.animal.name, c.animal.age)
// Breed is Persian, name is somi , age is 12
}
연속된. 을 이용해 랩핑 된 값들을 꺼내서 사용할 수 있다. 자바와 매우 유사한 부분인 것 같다.
이런 임베딩 타입의 문제점으로 제시되는 부분이 깊이에 따른 동일한 이름의 필드가 존재한다면?
이에 대한 문제는 매우 심플하게 해결된다. 각 깊이별로 이름이 따로 존재하기 때문에 깊이에 맞는. 을 이용해 체이닝 하면 된다.
두 번째로 동일한 깊이에 동일한 이름이 존재한다면? 이건 그냥 오류다 애초에 컴파일조차 허용 되지 않는다. 같은 구조체 안에 같은 이름 이라니 끔찍하다.
'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 02 (0) | 2023.02.09 |
Effective Go 01 (1) | 2023.02.08 |