[golang] method, interface, pointer 개념 다시잡기
golang을 공부하는 중에 해당 개념들이 헷갈려서 다시 복습하고자 한다.
function and method
메소드는 객체를 통해 호출할 수 있는 특별한 함수를 의미한다. 즉 넓은 의미에서 메소드는 함수에 포함된다고 볼 수 있다.
golang에는 class, object라는 개념이 존재하지 않는다. 대신 type이라는 개념이 class, object를 어느정도 대체한다고 볼 수 잇다.
메소드는 함수와 다르게 리시버가 필수이다. 다시 정리해보자면 메소드는 어느 객체에 속한다는 특징이 있으며, 언제 어디서든 호출이 가능한 함수와는 달리 해당 객체를 통해서만 호출이 가능하다는 차이점이 있다.
※ 리시버
func (v *Vertex) VertexInfo() {
~
}
리시버는 Vertex 타입의 객체만 해당 메소드를 사용할 수 있음을 나타낸다.
즉 Vertex 로 선언된 객체 말고는 해당 메소드(특별한 함수)를 사용할 수 없다는 의미!
- 포인터 리시버
- 밸류 리시버
포인터 리시버는 인스턴스(데이터) 직접 조작 가능
type Vertex struct {
X, Y float64
}
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
func main() {
v := Vertex{3, 4}
Scale(&v, 10) // 리시버로 포인터 밸류를 받으므로, 포인터 값(메모리 주소)와 인자 f를 넣음
fmt.Println(v.X, v.Y) // 30, 40으로 값 변경
}
이렇게 리시버로 받은 v를 통해 struct Vertext로 접근해서 계산할 경우 -> 본래 인스턴스의 값도 변경된다.
밸류 리시버는 데이터 복사해서 사용 -> 포인터 리시버와 다르게 인스턴스를 새로 만들어서 조작하기 때문에 실제 인스턴스를 직접 조작 x
함수
func Abs(v Vertex) float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
보시다시피 리시버(receiver)를 필수적으로 필요로 하지 않는다.
메소드
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X+ v.Y*v.Y)
}
Vertex 타입의 리시버 v를 받고 있다.
포인터
변수의 메모리 주소를 가리키는 특별한 데이터 타입
var i int = 42
var p *int
p = &i
int 형 변수 선언 및 초기화 후, int형 타입 변수를 가리키는 포인터 변수 p를 선언한다.
그 후 p에 i 변수의 메모리 주소를 저장하는 코드이다.
만약 아래와 같이
*p = 43
포인터 p를 통해 메모리에 존재하는 값을 변경 가능하다.
go playground에서 확인해보면
포인터를 통해 값이 바뀐 것을 확인할 수 있다.
& : 변수의 메모리 주소를 저장, 이미 존재하는 변수의 메모리 주소를 가져와서 저장
* : 포인터를 통해 값에 접근, 변경 가능 (역참조 연산자)
인터페이스
쉽게말해, 메소드들의 집합체라고 생각하면 된다.
인터페이스를 구현하겠다는 것은 인터페이스에 속한 메소드들을 모두 구현하겠다는 의미와 같다.
type Shape interface {
area(distance float64) float64
perimeter() float64 // 둘다 반환값 float64라는 의미
}
※ struct와의 차이점 -> struct(구조체)는 필드, 즉 변수 값을 정의한다.
인터페이스를 구현하려면 해당 인터페이스에 속한 메소드를 모두 만들어줘야한다.
함수가 인자로 인터페이스를 받아들이는 경우? -> 인터페이스 구현만 되면 입력 파라미터(인자)로 사용할 수 있음
뭔 말인지 모르겠다 싶으면... -> 타입이 만약 인터페이스에 속한 메소드를 구현했다면 해당 타입은 인터페이스를 구현한 것으로 치고, 해당 인터페이스 변수를 통해 해당 타입을 다룰 수 있다. 그냥 예시로 보는게 깔쌈하다.
package main
import "fmt"
type Shape interface {
Area() float64
}
type Rectangle struct {
width, height float64
}
func (r Rectangle) Area() float64 {
return r.width * r.height
}
type Circle struct {
radius float64
}
func (c Circle) Area() float64 {
return 3.14 * c.radius * c.radius
}
func main() {
var s Shape
r := Rectangle{width: 10, height: 5}
c := Circle{radius: 7}
s = r
fmt.Printf("Rectangle Area: %.2f\n", s.Area())
s = c
fmt.Printf("Circle Area: %.2f\n", s.Area())
}
Rectangle과 Circle 타입은 Area() 메소드를 구현하였기 때문에 Shape 인터페이스를 구현한 것으로 간주된다.
// 타입 Rectangle을 리시버로 받음
func (r Rectangle) Area() float64 {
return r.width * r.height
}
// 타입 Circle을 리시버로 받음
func (c Circle) Area() float64 {
return 3.14 * c.radius * c.radius
}
해당 타입을 리시버로 받는 Area() 메소드를 명시하였기 때문에 Shape 인터페이스를 구현한것으로 간주되었다는 의미이다.
r := Rectangle{width: 10, height: 5}
c := Circle{radius: 7}
s =r
fmt.Printf(~)
s = c
fmt.Printf(~)
r, c에서 구조체의 변수값을 정의하고, s 변수에 각 구조체 r, c를 넣어서 테스트해본다.
각 구조체는 위에서 말했듯이, Shape 인터페이스를 구현한 것으로 간주되기 때문에 해당 인터페이스에 속한 메소드를 사용할 수 있다.
ex> s.Area() 등
잘 이해가 안될 수 있지만 계속 코드를 짜보면서 실전으로라도 익혀야 한다.
빈 인터페이스
메소드 집합이 비어있는 인터페이스
= 모든 타입은 빈 인터페이스를 구현한다. (어떠한 타입의 값도 받을 수 있기 때문에 dynamic type에서 쓰인다)
우리가 타입 변환을 할 때, 처음부터 타입 고정을 하면 번거로울 수 있으나 빈 인터페이스로 받아버리면, 빈 인터페이스면 아무 값이나 다 ok이기 때문에 (쉽게 말해 c/c++에서 void, java의 object같은 존재) 타입 변환이 자유로워진다.
package main
import "fmt"
func printValue(i interface{}) {
fmt.Printf("value: %v, type: %T\n", i, i)
}
func main() {
printValue(42) // 상황에 따라 i의 타입이 바뀐다
printValue("Hello, Go!")
printValue(3.14)
printValue(true)
}
즉 빈 인터페이스 타입을 받는다는 것은 어떠한 타입이 들어와도 된다는 것이기 때문에, 값에 따라 자동적으로 다양한 타입이 들어올 때 유용하게 쓰인다.
만약 형변환 시 / 데이터 타입을 여러개 써야 할 때 / 굳이 타입 선언이 불필요할때 유용하게 쓰일듯
● type assertion
타입 확인
변수 x와 타입 t에 대해, 만약 x.(t)라고 표현하였을 때 x가 nil이 아니고 t 타입에 속한다는 것을 확인할 경우, t 타입의 x를 그대로 리턴함.
+) 만약 nil이거나 t 타입이 아니면...? -> 런타임 에러
package main
import "fmt"
func main() {
var i interface{} = "hi"
// 타입 단언을 통해 값이 string인지 확인
s, ok := i.(string) // i가 string인 경우 s에 값 할당, ok에 true 할당
if ok {
fmt.Printf("String value: %s\n", s)
} else { // ok에 false가 할당 -> i가 string 형식이 아닌 경우 (만약 변수가 string 형이 안왔을 경우)
fmt.Println("Not a string")
}
}
다음번엔 고 채널과 고 루틴에 대해서 간략하게 정리해보고자 한다.
확실히 직접 코드를 짜면서 공부하는게 이해가 더 빠른듯 하다. 그냥 문서만 봤을때는 아리까리한 개념들이 많았다.