Goの基本概念だけを学習してみた
はじめに
Go は近年非常に人気があり、多くの企業や開発者が使用しています。この記事では、Go の基本的な概念について学び、そのメリットを探ります。
ポインタ
Go ではポインタを使用して変数のアドレスを参照できます。これにより、関数間でデータを効率的に共有することができます。
package main
import "fmt"
func main() {
x := 5
fmt.Println("Before:", x)
increment(&x)
fmt.Println("After:", x)
}
func increment(x *int) {
*x = *x + 1
}
このコードでは、increment 関数に x のポインタを渡しています。increment 関数内で x の値を変更すると、main 関数内の x の値も変更されます。
ポインタのメリット
-
効率的なデータ共有: 大きなデータを関数に渡す際に、データ全体をコピーするのではなく、ポインタを渡すことで効率的にデータを共有できます。
-
メモリ管理: ポインタを使用することで、メモリ管理が容易になります。
構造体とメソッド
Go では、構造体とメソッドを使用してデータとその操作をまとめることができます。これにより、オブジェクト指向プログラミングのようなスタイルでコードを書くことができます。
package main
import "fmt"
type Rectangle struct {
Width int
Height int
}
func (r Rectangle) Area() int {
return r.Width * r.Height
}
func main() {
rect := Rectangle{Width: 10, Height: 5}
fmt.Println("Area:", rect.Area())
}
このコードでは、Rectangle 構造体とそのメソッド Area を定義しています。Area メソッドは Rectangle の面積を計算します。
構造体とメソッドのメリット
-
データのカプセル化: データとその操作を一つの単位としてまとめることで、コードの可読性と保守性が向上します。
-
再利用性: 構造体とメソッドを定義することで、同じ機能を持つコードを簡単に再利用できます。
-
明確なインターフェース: メソッドを使用することで、構造体の操作方法が明確になります。
インターフェース
Go では、インターフェースを使用して、異なる型に共通のメソッドセットを定義できます。これにより、多態性を実現することができます。
package main
import "fmt"
type Shape interface {
Area() int
}
type Rectangle struct {
Width int
Height int
}
func (r Rectangle) Area() int {
return r.Width * r.Height
}
type Circle struct {
Radius int
}
func (c Circle) Area() int {
return 3 * c.Radius * c.Radius
}
func printArea(s Shape) {
fmt.Println("Area:", s.Area())
}
func main() {
rect := Rectangle{Width: 10, Height: 5}
circle := Circle{Radius: 3}
printArea(rect)
printArea(circle)
}
このコードでは、Shape インターフェースを定義し、Rectangle と Circle の両方がこのインターフェースを実装しています。printArea 関数は Shape インターフェースを受け取り、任意の Shape の面積を計算します。
インターフェースのメリット
-
多態性の実現: インターフェースを使用することで、異なる型のオブジェクトを同じように扱うことができます。
-
柔軟な設計: インターフェースを使用することで、異なる実装を持つオブジェクトを簡単に交換できます。
-
コードの拡張性: 新しい型を追加する際に、既存のコードを変更することなくインターフェースを実装することができます。
ジェネリクス
Go 1.18 以降、Go ではジェネリクスが導入され、型パラメータを使用して、より汎用的な関数やデータ構造を作成できるようになりました。
package main
import "fmt"
func Print[T any](s []T) {
for _, v := range s {
fmt.Println(v)
}
}
func main() {
ints := []int{1, 2, 3}
strings := []string{"hello", "world"}
Print(ints)
Print(strings)
}
このコードでは、ジェネリック関数 Print を定義しています。Print 関数は任意の型 T を受け取ることができ、スライス内のすべての要素を出力します。
ジェネリクスのメリット
-
コードの再利用性: ジェネリクスを使用することで、同じロジックを異なる型に対して再利用できます。
-
型安全性: ジェネリクスを使用することで、型安全なコードを記述することができ、ランタイムエラーを防ぐことができます。
-
コードの簡潔性: ジェネリクスを使用することで、同じ処理を行うために複数の関数やデータ構造を定義する必要がなくなり、コードが簡潔になります。
ゴルーチンとチャネル
ゴルーチンの例
package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
}
このコードでは、say 関数をゴルーチンとして実行しています。go キーワードを使うことで、並行して関数を実行できます。
チャネルの例
package main
import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c
fmt.Println(x, y, x+y)
}
このコードでは、チャネルを使ってゴルーチン間でデータを送受信しています。c <- sum でチャネルにデータを送信し、<-c でチャネルからデータを受信します。
ゴルーチンとチャネルのメリット
-
並行処理の簡単な実装: ゴルーチンとチャネルを使うことで、複雑なスレッド管理を行わずに並行処理を実装できます。
-
効率的なリソース使用: ゴルーチンは非常に軽量であり、スレッドよりも少ないリソースで並行処理を実現できます。
-
安全なデータ共有: チャネルを使用することで、ゴルーチン間のデータ共有が安全に行えます。
デファード(defer)
defer ステートメントを使うと、関数の終了時に特定のコードを実行することができます。これはリソースのクリーンアップなどに便利です。
package main
import "fmt"
func main() {
defer fmt.Println("world")
fmt.Println("hello")
}
このコードでは、defer fmt.Println("world") により、main 関数が終了する直前に "world" が出力されます。
デファードのメリット
-
リソース管理の簡素化: defer を使うことで、ファイルやネットワーク接続のクリーンアップコードを簡単に管理できます。
-
コードの可読性向上: defer を使うことで、リソースのクリーンアップコードを関数の末尾に書く必要がなくなり、コードの可読性が向上します。
Discussion