42dot 사내 세미나 'Go Gopher 길들이기'를 위해 만들어진 온라인 투어 페이지에서 공부한 내용입니다.
한글 공식 온라인 투어
https://go-tour-ko.appspot.com/welcome/1
fmt 패키지 공식문서
printf 예시
01. Hello, world
const 와 var 로 상수, 변수 선언
const name, age = "kim", 22
fmt
- Println
- Printf
공식문서: https://pkg.go.dev/fmt
02. 변수
모든 변수는 선언과 동시에 초기화 됩니다.
int 는 0, string 은 string("") pointer 는 nil 로 초기화 됩니다.
var a int = 10
var b string = "hello, world"
var c bool
d := 10.0
e := true
f := "hello, world"
var i *int = nil
i = &a
fmt.Println(*i)
a = 20
fmt.Println(*i)
* 는 포인터에 해당한다.
& 는 변수의 주소를 반환한다.
func zero(xPtr *int) {
*xPtr = 0
}
func main() {
x := 5
zero(&x)
fmt.Println(x) // x는 0
}
참조
https://www.codingnuri.com/golang-book/8.html
03. 제어문
if 문은 자바와 비슷하다.
Go 언어는 반복문이 for 밖에 없습니다.
기본적인 for 문은 Java 언어와 거의 유사합니다.
if 문과 for 문은
조건표현을 위해 괄호를 사용하지 않는다.
하지만 실행문을 위한 { } 는 반드시 작성해야 한다.
switch 문이 다른언어와 다른점은 case 의 코드실행을 마치면 알아서 break 를 실행한다.
fallthrough 로 끝나는 case 는 스스로 break 를 하지 않고, 다음 case 문을 평가한다.
fmt.Println(b, "world")
두 문자를 띄어쓰기로 출력한다.
package main
import "fmt"
func main() {
a := 100
b := "hello"
// if 문
if a > 100 {
fmt.Println("greater than 100")
}
if b == "hello" {
fmt.Println(b, "world")
} else {
fmt.Println("hello", b)
}
// for문; C for-loop 스타일
for i := 0; i < 3; i++ {
fmt.Printf("i: %d\n", i)
}
// for문; C while-loop 스타일
j := 0
for j < 3 {
fmt.Printf("j: %d\n", j)
j++
}
/* for문; 무한 루프
for {
fmt.Println("in infinite loop")
}
*/
// switch 문; C switch-case 스타일
switch b {
case "hello":
fmt.Println(b, "world")
case "world":
fmt.Println("hello", b)
case "안녕", "世界":
fmt.Println("안녕, 世界")
default:
fmt.Println("hello world")
}
// switch 문; if-then-else 스타일
switch {
case a < 100:
fmt.Println("less than 100")
case a > 100:
fmt.Println("greater than 100")
case a == 100:
fmt.Println("equal to 100")
case b == "hello":
fmt.Println("true, but never reached")
default:
fmt.Println("never reached")
}
}
04. 제어문 II. Challenge
Fizz Buzz 문제를 구현해 봅니다.
Fiz Buzz 문제는 다음과 같은 규칙을 가지고 있습니다.
- (1) 3으로 나누어지는 수가 주어지면 숫자 대신 'fizz' 를 출력 합니다.
- (2) 5로 나누어지는 수가 주어지면 숫자 대신 'buzz' 를 출력 합니다.
- (3) 3과 5로 나누어지는 수(e.g., 15)가 주어지면 숫자 대신 'fizz buzz' 를 출력 합니다.
위 규칙에 맞춰, 1에서 100까지 숫자를 출력 해 보세요.
func main() {
for i := 1; i < 100; i++ {
switch {
case i%3 == 0 && i%5 == 0:
fmt.Println("fizz buzz")
case i%3 == 0:
fmt.Println("fizz")
case i%5 == 0:
fmt.Println("buzz")
default:
fmt.Println(i)
}
}
}
05. function
Go 언어에서 함수는 다음과 같이 선언 합니다.
func 함수명(매개변수_이름1 매개변수_타입1, 매개변수_이름2 매개변수_타입2) 반환타입
매개 변수 리스트에서 연달은 매개 변수가 같은 타입인 경우, 가장 마지막 매개 변수를 제외하면 매개 변수 타입 명세를 생략할 수 있습니다.
Go 언어의 함수는 다중 값을 반환 할 수 있습니다.
func swap(x, y int) (int, int) {
return y, x
}
Go 언어에서는 함수에 대한 접근자를 가지고 있지 않습니다.
Java 의 public, private 와 같은 접근자를요.
대신 Go 언어에서는 함수명을 가지고 이를 구분합니다.
함수명의 첫 문자가 영문 대문자라면 public 속성을 가지며 현재 패키지 외부로 노출됩니다.
반대로 영문 소문자라면 private 속성을 가지며 현재 패키지 외부로 노출되지 않습니다.
Go 언어는 언어 자체에서 유니코드 (UTF-8) 를 지원합니다.
즉, 함수명도 한글로 지정할 수 있습니다.
다만, 한글은 영문 대문자가 아니므로 모두 private 속성을 지닙니다.
Challenge
- - 어떤 수 a, b가 주어졌을 때, 이를 나누는 함수 div 를 구현 하세요.
- - div 함수는 2개의 int 형 정수 받아 (float64, error)가 반환되어야 합니다.
- - 분모, 즉 b가 0인 경우 의도하지 않은 결과가 나올 수 있습니다. 이를 방지하기 위해 b가 0인 경우 적당한 error 객체가 반환되어야 합니다.
- - error 객체를 만들기 위해서는 errors.New 함수를 참고하기 바랍니다.
- - 에러가 발생하지 않은 경우, 반환되는 error 객체의 값은 nil 이어야 합니다.
package main
import (
"errors"
"fmt"
)
func add(a, b int) int {
return a + b
}
func mul(a, b int) int64 {
return int64(a) * int64(b)
}
func div(a, b int) (float64, error) {
switch b {
case 0:
error := errors.New("분모는 0 이 될 수 없습니다")
return 0, error
default:
answer := float64(a) / float64(b)
return answer, nil
}
}
func main() {
fmt.Println(add(1, 2))
fmt.Println(mul(2, 3))
fmt.Println(div(7, 3))
/*
if val, err := div(2, 3); err == nil {
fmt.Println(val)
}
*/
}
06. struct 타입 I & 열거자
struct 타입 I
func (변수_이름 리시버_타입) 매서드_이름(매개변수_리스트) 반환타입
package main
import "fmt"
type gender int
const (
male gender = iota
female
)
type person struct {
name string
age int
gender gender
}
func (p person) print() { // value receiver
fmt.Printf("%s/%v (%d)\n", p.name, p.gender, p.age)
}
func (p *person) aging() { // pointer receiver
p.age++
}
func main() {
elsa := person{"Elsa", 21, female}
anna := person{
name: "Anna",
age: 18,
gender: female,
}
elsa.print()
anna.aging()
anna.print()
}
열거자
Go 언어에서는 상수를 열거자 (enurmerator) 로 사용합니다.
이 때는 주로 const 블록 단위로 설정합니다.
다음에 등장하는 상수는 타입의 별도 기입이 없다면 자동적으로 윗 행의 타입을 따르게 됩니다.
그리고 숫자 타입인 경우 윗 행의 값에 1을 더하여 지정 됩니다.
참고로 iota 는 const 블록마다 0 으로 초기화 됩니다.
iota 는 0 을 의미한다.
07. struct 타입 II & 오버로딩과 오버라이딩
struct 타입 II
Go 언어는 객체의 상속이 없는 개체지향 언어이다.
Go 언어에서는 코드 재사용을 위해 내포 (embedding) 라는 기법을 제공합니다.
어떤 객체에서 다른 객체 (struct, interface) 의 필드 혹은 메서드를 온전히 내포시키는 방법이다.
얼핏 보면 '상속' 과 유사하지만, '상속' 에서 처럼 서브 타입 인스턴스가 슈퍼 타입 변수에 할당 될 수 없습니다.
정확히 말하면 '내포' 에서는 슈퍼 타입이나 서브 타입이라는 개념이 없습니다.
오버로딩과 오버라이딩
내포된 객체와 동일한 메서드명을 정의 한다면, 내포한 객체의 메서드가 호출되는 것을 확인할 수 있습니다. 즉, 내포한 객체의 메서드로 오버로딩 (overloading) 되었습니다.
package main
import "fmt"
type gender int
const (
male gender = iota
female
)
type person struct {
name string
age int
gender gender
}
func (p person) print() {
fmt.Printf("%s/%v (%d)\n", p.name, p.gender, p.age)
}
func (p employee) print() {
fmt.Println("employee info:")
p.person.print()
}
// struct embedding
type employee struct {
person
department string
}
func main() {
println()
scryner := employee{
person: person{
name: "scryner",
age: 38,
gender: male,
},
department: "Naver Labs",
}
scryner.print()
}
employee info:
scryner/0 (38)
08. interface 타입 & fmt.Stringer
interface 타입
'내포' 만으로는 '다형성 (polymorphism)' 을 제공할 수 없습니다.
Go 언어에서는 인터페이스 (interface) 타입으로 다형성을 제공합니다.
type Duck interface {
Swin()
}
type Swan struct {}
func (s Swan) Swin() {}
var duck Duck
duck = Swan{}
duck.Swin()
위 예제처럼 Go 언어의 인터페이스 타입은 JAVA 언어의 인터페이스 타입과 유사하다고 볼 수 있습니다.
하지만 큰 차이점이 있는데요.
바로 인터페이스를 구현하는 것을 명시적으로 선언하거나 지정하지 않는 점 입니다.
이러한 타이핑 방법을 덕 타이핑 이라고 합니다.
만약 어떤 새가 오리처럼 걷고, 헤엄치고, 꽥꽥거리는 소리를 낸다면 나는 그 새를 오리라고 부를 것이다.
덕 타이핑을 일반적으로 동적 타입 언어에 적용되기 쉽습니다. Go 언어는 강력한 정적 타입 언어이지만, 짧은 변수 선언에서와 마찬가지로 컴파일러 추론으로 덕 타이핑을 제공합니다.
fmt.Stringer
지난 struct 예제에서 정의한 print 함수를 사용하여 객체를 출력하면, 성별 부분에 숫자가 표기되었다.
gender 라는 타입은 int alias 타입으로 선언되었기에, 이를 출력하면 숫자가 나오는 것이다.
이를 숫자가 아닌 문자로 출력하려면 어떡해야 할까 ?
JAVA 언어에서는 모든 객체의 최상위 부모 객체인 Object 의 toString 함수를 오버라이딩하여 이를 해결합니다.
Go 언어에서는 이와 유사하게 fmt.Stringer 인터페이스를 제공합니다.
type Stringer interface {
String() string
}
09. 익명함수 & 클로져
익명함수 (anonymous function)
Go 언어에서 함수는 1급 (first-class) 객체입니다.
즉, 다른 객체 (int, stirng, struct) 들 처럼 간주되며, 다음과 같이 사용될 수 있습니다.
- 어떤 변수에 할당 될 수 있습니다.
- 어떤 함수에 매개 변수로 전달될 수 있습니다.
- 어떤 함수의 반환값이 될 수 있습니다.
익명 함수란 말 그대로 이름이 없는 함수를 말 합니다.
선언과 동시에 호출이 이루어지게 되며, 짧은 변수 선언 시 컴파일러는 변수를 익명함수의 반환타입으로 추론합니다.
클로져 (closure)
Go 언어의 익명 함수 (혹은 함수 리터럴) 는 클로져입니다.
즉, 익명 함수가 선언된 문맥의 접근 가능한 식별자는 익명 함수 내에서 함수의 생명 주기가 다할때까지 언제든 접근 가능합니다.
이러한 기법을 lexcial scoped name binding 이라고 합니다.
아래의 예제에서 살펴보자면 변수 i. 는 클로져 밖에서 생성되었지만, 클로져 내에서 접근 가능합니다.
또, 클로져 내에서 접근한 i 는 사실 함수 외부에서 생성한 변수 i 임을 확인할 수 있습니다.
package main
import (
"fmt"
"time"
)
func main() {
// anonymous function
i := func() int {
now := time.Now()
return int(now.Unix() % 10)
}()
fmt.Println(i)
// closure
printI := func() {
fmt.Println("printI:", i)
}
printI()
i = i * 2
printI()
}
6
printI: 6
printI: 12
10. 슬라이스 타입 & 슬라이스 자르기
package main
import "fmt"
func main() {
stringSet := []string{"hello", "world",
"d2", "campus seminar"}
foreachPrint := func(arr []string) {
// for-each loop
for _, entry := range arr {
fmt.Println(entry)
}
}
fmt.Println("set length:", len(stringSet))
foreachPrint(stringSet)
// append an element
stringSet = append(stringSet, "naver")
foreachPrint(stringSet)
// slicing slices
subSet1 := stringSet[1:3]
foreachPrint(subSet1)
subSet2 := stringSet[2:]
foreachPrint(subSet2)
subSet3 := stringSet[:3]
foreachPrint(subSet3)
}
set length: 4
hello // foreachPrint(stringSet)
world
d2
campus seminar
hello // foreachPrint(stringSet)
world
d2
campus seminar
naver
world // stringSet[1:3]
d2
d2 // stringSet[2:]
campus seminar
naver
hello // stringSet[:3]
world
d2
슬라이스 타입
Go 언어에서 배열처럼 사용 가능한 타입은 배열 타임과 슬라이스 타입이 있습니다.
먼저 배열은 C 언어에서와 유사하게 다음과 같이 선언됩니다.
var arr [3]int
하지만, Go 언어에서는 배열 타입 외에 슬라이스 타입을 제공합니다. 슬라이스 타입은 확장 가능한 배열이라 할 수 있으며, 슬라이스 자르기와 같은 기능을 제공하여 편리하게 이용할 수 있습니다.
슬라이스는 다음과 같은 방법들로 생성이 가능 합니다.
var arr1 []int
arr2 := []int{}
arr3 := make([]int, 5)
make 함수는 Go 언어의 기본 함수로 슬라이스, 맵 (map), 채널 (channel) 과 같은 Go 언어 기본 포인터 타입을 생성하는데 사용합니다.
make 함수를 사용하여 슬라이스를 만들 때는, 추가적으로 슬라이스의 용량 (capacity) 를 매개변수로 받을 수 있습니다.
그 결과 지정한 용량 크기 만큼의 슬라이스가 생성되며, 각 슬라이스 엔트리는 초기값 (zero value) 으로 설정됩니다.
슬라이스는 포인터 타입이므로 생성되지 않은 빈 슬라이스 (용량이 0) 는 nil 로 표현될 수 있습니다.
Go 언어의 기본 함수인 len 으로 슬라이스의 길이를, cap 으로 슬라이스의 용량을 알 수 있습니다.
슬라이스 자르기 (splicing slice)
슬라이스는 재분할 할 수도 잇고, 같은 배열을 가리키는 (point) 새로운 슬라이스를 만들 수 도 있습니다.
예제로 살펴보면
s[lo:hi]
위의 표현은 lo 에서 hi-1 의 요소 (element) 를 포함하는 슬라이스 입니다. 따라서
s[lo:lo]
는 빈 (empty) 슬라이스 이고
s[lo:lo+1]
는 하나의 요소를 가집니다.
[n:m] 일 때 n 인덱스부터 m-n 개를 가져온다고 이해하면 쉽다. n 과 m 은 생략할 수 있다.
11. 맵 타입 & 맵 다루기
맵 (map) 타입은 JAVA 언어의 Map<K, V> 인터페이스와 비슷하다 할 수 있겠습니다.
다만 Go 언어에서는 기본 타입으로 되어 있다는 점에서 조금 다르다.
맵은 반드시 사용하기 전에 make 를 명시해야 한다.
make 를 수행하지 않는 빈 맵 (즉, 맵의 값이 nil) 에는 값을 할당할 수 없다.
맵 m 의 요소를 삽입하거나 수정
m[key] = elem
요소 가져오기
elem = m[key]
요소 지우기
delete(m, key)
키의 존재 여부 확인하기
elem, ok = m[key]
위의 ok 의 값은 m 에 key 가 존재한다면 true, 존재하지 않으면 false, elem 은 타입에 따라 0 (zero value) 가 됩니다.
이처럼 map 을 읽을 때, 존재하지 않는 key 의 반환 값은 값 타입의 초기값 (zero value) 입니다.
package main
import "fmt"
func main() {
arr := []string{"alpha", "bravo", "charlie", "delta", "echo"}
m := make(map[string]int)
m["zero"] = 0
for i, val := range arr {
m[val] = i + 1
}
for key, val := range m {
fmt.Printf("%d: %s\n", val, key) // 출력 순서 보장 안됨
}
}
5: echo
0: zero
1: alpha
2: bravo
3: charlie
4: delta
12. 고루틴 & Challenge
고루틴 (goroutine) 은 Go 의 런타임에 의해 관리되는 경량 스레드 입니다.
다수의 고루틴은 다시 다수 (일반적으로 더 적은 수) 의 OS 스레드로 스케쥴 됩니다.
어떤 함수 fun 이 다음과 같이 정의되었다고 가정 합시다.
func fun(x, y int)
아래 코드는 현재 컨텍스트에서 fun 함수를 수행합니다.
fun(1, 2)
아래 코드는 새로운 고루틴을 시작하며, 그 고루틴의 컨텍스트에서 fun 함수를 수행 합니다.
go fun(1, 2)
고루틴은 동일한 주소 공간에서 실행되므로, 공유되는 자원으로의 접근은 반드시 동기화 되어야 합니다. sync 패키지가 이를 위해 유용한 기본 기능을 제공합니다.
Go 언어에서는 에서는 sync 패키지를 사용하여 자원을 공유하기 보다는, 채널(channel)을 사용해서 통신하도록 권장하고 있습니다.
“메모리를 공유해서 통신하지 말고, 통신하여 메모리를 공유합시다.”
package main
import (
"fmt"
"time"
)
func main() {
for i := 0; i < 3; i++ {
go func(id int) {
fmt.Println("goroutine", id)
}(i)
}
time.Sleep(time.Second * 1)
}
goroutine 0
goroutine 2
goroutine 1
13. 채널
“메모리를 공유해서 통신하지 말고, 통신하여 메모리를 공유합시다.”
채널(channel)은 채널 연산자 <- 를 이용해 값을 주고 받을 수 있는, 타입이 존재하는 파이프입니다.
ch <- v // v 를 ch로 보냅니다.
v := <-ch // ch로부터 값을 받아서
// v 로 넘깁니다.
(데이터가 화살표 방향에 따라 흐릅니다.)
맵이나 슬라이스처럼, 채널은 사용되기 전에 생성되어야 합니다:
ch := make(chan int)
생성되지 않은 채널을 사용하는 것을 프로그램이 비정상 종료 할 수 있으니 유의해야 합니다.
기본적으로, 송/수신은 상대편이 준비될 때까지 블록됩니다. 이런 특성이 고루틴이 명시적인 락이나 조건 없이도 동기화 될 수 있도록 돕습니다.
package main
import "fmt"
func main() {
c := make(chan int)
done := make(chan int)
go func() {
// producer
for i := 0; i < 5; i++ {
c <- i
}
close(c)
}()
go func() {
// consumer
for i := range c {
fmt.Println(i)
}
done <- 1
}()
<-done
fmt.Println("all done")
}
0
1
2
3
4
all done
14. select 문
select 문은 다수의 통신 동작으로부터 수행 준비를 기다릴 수 있게 합니다.
select 문은 case 문으로 받는 통신 동작들 중 하나가 수행될 수 있을 때까지 수행을 블록합니다. 다수의 채널이 동시에 준비되면 그 중 하나를 무작위로 선택합니다.
서버 프로그램에서 요청 핸들러는 일반적으로 다음 꼴로 구현 됩니다.
func serveRequest(reqChan chan Request, exit chan bool) {
for {
select {
case req := <-reqChan:
handle(req)
case <-exit:
return
}
}
}
package main
import (
"fmt"
"time"
)
const answerToEverything = 42
func getAnswerToEverything() chan int {
ch := make(chan int)
go func() {
// 0 ~ 9초간 슬립
nowUnix := time.Now().Unix()
time.Sleep(time.Second *
time.Duration(nowUnix%10))
ch <- answerToEverything
}()
return ch
}
// time.After() 함수와 같은 일을 수행
func after(duration time.Duration) chan bool {
ch := make(chan bool)
go func() {
time.Sleep(duration)
ch <- true
}()
return ch
}
func main() {
fmt.Println(
"Calculating the answer to everything")
select {
case result := <-getAnswerToEverything():
fmt.Println(
"I just found the answer to everything:",
result)
case <-after(time.Second * 3):
fmt.Println("ERROR: timeouted!")
}
}
Calculating the answer to everything
I just found the answer to everything: 42
15. sync 패키지
sync 패키지
0에서 20까지의 각 숫자에 대해 factorial을 구하는 프로그램을 작성하고자 합니다. 다음과 같은 출력 결과를 가지도록요.
Factorial of (0) = 1
Factorial of (1) = 1
Factorial of (2) = 2
Factorial of (3) = 6
...
아래와 같이 간단히 작성할 수 있을 것입니다.
for i := 0; i < ITER; i++ {
sum := 0
for j := 1; j <= i; j++ {
sum *= j
}
fmt.Printf("Factorial of (%d) = %d\n", i, sum)
}
sync.WaitGroup
위 프로그램은 고루틴을 통해 병렬화하기 쉽습니다. 간단히 아래처럼 pseudo-code를 작성할 수 있습니다.
for i := 0; i < ITER; i++ {
go func(n int) {
sum = factorial(n)
}(i)
}
이러한 병렬 처리시 모든 결과가 처리되기까지 기다릴 필요가 있습니다. sync 패키지는 병렬 처리시 발생활 수 있는 동기화 문제를 풀어주는 패키지입니다. sync.WaitGroup을 이용하면 모든 고루틴이 작업을 끝낼때까지 기다릴 수 있습니다.
var wg sync.WaitGroup
for i := 0; i < ITER; i++ {
wg.Add(1)
go func(n int) {
sum = factorial(n)
wg.Done()
}(i)
}
wg.Wait()
오른쪽 playground에는 실행할 수 있는 코드가 있습니다. 다만, 실제 실행시 문제가 발생할거에요. 결과를 보고 어떤 문제인지 생각해 보세요.
sync.Mutex
문제는 map 구조체에 여러 고루틴이 동시에 접근해서 발생합니다. sync패키지는 이러한 문제를 해결하기 위해서 다양한 lock을 제공합니다. 여기서는 map을 보호하기 위해 sync.RWMutex를 활용해 보고자 합니다.
일반적으로 sync.RWMutex는 아래와 같이 사용 합니다.
type Shared struct {
v int
lock sync.RWMutex
}
func (s *Shared) Set(i int) {
s.lock.Lock()
defer s.lock.Unlock()
s.v = i
}
func (s *Shared) Get() int {
s.lock.RLock()
defer s.lock.RUnlock()
return s.v
}
여기서 defer를 주목해주세요.
defer 함수는 함수 종료시 수행하는 함수입니다. Lock 구문 근처에 unlock 구문을 위치함으로써 lock과 관련된, 주로 unlock을 하지 않아 발생하는 문제를 실용적으로 풀어줍니다.
오른쪽 playground의 두번째 탭은 sync.RWMutex을 이용하여 factorial이 병렬적으로 계산되게 수정하였습니다.
package main
import (
"fmt"
"time"
)
const answerToEverything = 42
func getAnswerToEverything() chan int {
ch := make(chan int)
go func() {
// 0 ~ 9초간 슬립
nowUnix := time.Now().Unix()
time.Sleep(time.Second *
time.Duration(nowUnix%10))
ch <- answerToEverything
}()
return ch
}
// time.After() 함수와 같은 일을 수행
func after(duration time.Duration) chan bool {
ch := make(chan bool)
go func() {
time.Sleep(duration)
ch <- true
}()
return ch
}
func main() {
fmt.Println(
"Calculating the answer to everything")
select {
case result := <-getAnswerToEverything():
fmt.Println(
"I just found the answer to everything:",
result)
case <-after(time.Second * 3):
fmt.Println("ERROR: timeouted!")
}
}
Calculating the answer to everything
ERROR: timeouted!
16. 제네릭
제네릭
Go 1.18버전(2022년 3월)에서 드디어 제네릭(Generics)이 지원됩니다.
타입 파라미터 (type parameter)
함수는 타입 파리미터를 통해 다양한 타입을 지원하도록 일반화 할 수 있습니다. 다음과 같은 꼴을 살펴 봅시다.
func Index[T comparable](array []T, target T) int
위는 어떤 배열 array에서 주어진 target이 몇 번째 배열 위치인지 구하는 함수 꼴 입니다.
여기서 함수명 오른쪽에 [ ... ] 으로 표시된 부분을 타입 파라미터(type parameter)라고 하며, 이러한 꼴의 함수를 제네릭 함수(Generic function)라고 합니다. comparable 은 built-in constraint로 == , != 와 같은 연산자가 사용 가능한 타입을 지칭합니다.
참고로, 이러한 built-in constraint는 comparable 과 any 가 있습니다. any 는 말 그대로 모든 타입을 말하며, 타입 파리미터에서 뿐 아니라 기존의 interface{} 가 쓰였던 모든 곳을 대체할 수 있습니다.
제네릭 타입
제네릭 함수 뿐만 아니라, 타입 파리미터를 사용하는 제네릭 타입(Generic types)을 만들 수 있습니다. 제네릭 함수는 일반적으로 자료구조를 만들때 유용한 기능입니다.
오른쪽 playground의 첫번째 탭은 제네릭 함수와 제네릭 타입에 대한 예제를 담고 있습니다.
Factorial with generics
제네릭을 이용하면, 이전 Factorial에서 구현했던 syncMap 구조체를 보다 일반화할 수 있습니다.
오른쪽 playground의 두번째 탭을 살펴보시기 바랍니다.
package main
import "fmt"
// Index 함수는 배열 s에서 x 값의 배열 위치를 반환. 없다면, -1을 반환.
func Index[T comparable](s []T, x T) int {
for i, v := range s {
// v, x는 모두 T 타입이며, comparable함. 즉, ==를 사용할 수 있음.
if v == x {
return i
}
}
return -1
}
// Array 타입은 T 타입의 배열을 가짐.
type Array[T comparable] []T
// Array 타입은 Index 함수를 사용하여, 배열에서 어떤 값의 배열 위치를 반환.
func (a Array[T]) Index(x T) int {
return Index[T](a, x)
}
func main() {
// int형 배열
si := []int{10, 20, 15, -10}
fmt.Println(Index(si, 15))
// string형 배열
ss := []string{"foo", "bar", "baz"}
fmt.Println(Index(ss, "hello"))
// string형을 다룰 수 있는 Array 제네릭 타입
arr := Array[string]{"foo", "bar", "baz"}
fmt.Println(arr.Index("bar"))
}
2
-1
1
17. 축하합니다!
축하합니다!
42dot 사내 세미나 'Go Gopher 길들이기'의 모든 실습 과정을 살펴 보았습니다. 이제 Gopher로의 첫 발을 내딛게 되었습니다. 축하합니다!
더 자세하고 심화된 실습은 공식 온라인 투어 및 한글 공식 온라인 투어에서 진행할 수 있습니다.
Go 1.x 버전은 `Go 1` 이라는 언어 스펙을 따르고 있어 언어적인 구조는 2.x 버전이 나오기 전까지 크게 바뀌지 않겠지만, 아무래도 공식 온라인 투어가 최신의 내용을 반영하고 있을 확률이 높습니다.
Go 언어는 구조적으로 매우 단순하며, 아래 링크의 공식 안내서를 읽어보는 것 만으로도 언어적인 특성은 대부분 습득하실 수 있으리라 생각 합니다.
또, 아래와 같은 온라인 서적에서 Go 언어를 보다 자세하고 세심하게 살펴 볼 수 있습니다. (게다가 국문으로 번역 되었죠.)
To Be Continued...
다음장부터는 공식 온라인 투어의 내용을 담고 있습니다. 이 페이지에서 이어서 학습하시거나, 공식 온라인 투어에서 이어서 학습하시기 바랍니다!
'Others > 시작하기' 카테고리의 다른 글
Go 프로젝트 디렉토리 구조 잡기 (0) | 2023.04.30 |
---|---|
Create go app 시작하기 (0) | 2023.04.28 |
리눅스 시작하기 (0) | 2022.08.22 |
MDN 으로 JavaScript 공부하기 (0) | 2022.07.30 |
Learn IDE Features for Go (GoLand) (0) | 2022.06.26 |