
こんにちは。よっしーです(^^)
本日は、Go言語のよくある質問 について解説しています。
背景
Go言語を学んでいると「なんでこんな仕様になっているんだろう?」「他の言語と違うのはなぜ?」といった疑問が湧いてきませんか。Go言語の公式サイトにあるFAQページには、そんな疑問に対する開発チームからの丁寧な回答がたくさん載っているんです。ただ、英語で書かれているため読むのに少しハードルがあるのも事実で、今回はこのFAQを日本語に翻訳して、Go言語への理解を深めていけたらと思い、これを読んだ時の内容を備忘として残しました。
デザイン
Goはオブジェクト指向言語ですか?
イエスでもありノーでもあります。Goには型とメソッドがあり、オブジェクト指向スタイルのプログラミングを可能にしますが、型階層はありません。Goにおける「インターフェース」の概念は、使いやすく、ある意味でより一般的であると私たちが信じる、異なるアプローチを提供します。また、サブクラス化に類似した—しかし同一ではない—ものを提供するために、他の型に型を埋め込む方法もあります。さらに、Goにおけるメソッドは、C++やJavaよりも一般的です:プレーンで「ボックス化されていない」整数などの組み込み型も含め、あらゆる種類のデータに対して定義できます。それらは構造体(クラス)に制限されません。
また、型階層の欠如により、Goにおける「オブジェクト」は、C++やJavaなどの言語よりもはるかに軽量に感じられます。
解説
この節では、Go言語のオブジェクト指向性について、伝統的なOOP言語との違いを明確にしながら説明されています。Go言語独自のアプローチが分かる重要な説明です。
「Yes and No」の詳細
Yes の部分:OOP要素の存在
// 型(構造体)とメソッドの定義
type Rectangle struct {
Width float64
Height float64
}
// メソッドの定義
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r *Rectangle) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}
// オブジェクト指向的な使用
func main() {
rect := Rectangle{Width: 10, Height: 5}
fmt.Println("面積:", rect.Area()) // メソッド呼び出し
rect.Scale(2) // オブジェクトの状態変更
fmt.Println("スケール後の面積:", rect.Area())
}
No の部分:型階層の不在
// 従来のOOP(Java/C++風の疑似コード)での継承
// class Shape {
// virtual double area() = 0;
// }
//
// class Rectangle extends Shape {
// double area() override { return width * height; }
// }
//
// class Circle extends Shape {
// double area() override { return pi * radius * radius; }
// }
// Goでは継承がない - インターフェースを使用
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 math.Pi * c.Radius * c.Radius
}
// 型階層なしでポリモーフィズムを実現
func printArea(s Shape) {
fmt.Printf("面積: %.2f\n", s.Area())
}
Go言語のインターフェースアプローチ
暗黙的な実装
// インターフェースの定義
type Writer interface {
Write([]byte) (int, error)
}
// 明示的にimplementsを宣言する必要がない
type FileWriter struct {
filename string
}
func (fw FileWriter) Write(data []byte) (int, error) {
// ファイルに書き込み処理
return len(data), nil
}
// BufferWriter も自動的に Writer インターフェースを満たす
type BufferWriter struct {
buffer []byte
}
func (bw *BufferWriter) Write(data []byte) (int, error) {
bw.buffer = append(bw.buffer, data...)
return len(data), nil
}
// どちらも Writer として使用可能
func saveData(w Writer, data []byte) {
w.Write(data)
}
小さなインターフェースの組み合わせ
// 単一責任の小さなインターフェース
type Reader interface {
Read([]byte) (int, error)
}
type Writer interface {
Write([]byte) (int, error)
}
type Closer interface {
Close() error
}
// 組み合わせによる大きなインターフェース
type ReadWriter interface {
Reader
Writer
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
型の埋め込み(Embedding)
継承に「類似した」機能
// 基本的な構造体
type Person struct {
Name string
Age int
}
func (p Person) Introduce() string {
return fmt.Sprintf("私は%sです、%d歳です", p.Name, p.Age)
}
// 埋め込みによる「継承風」の実装
type Employee struct {
Person // 埋め込み(名前なし)
JobTitle string
Salary int
}
func (e Employee) Work() string {
return fmt.Sprintf("%sは%sとして働いています", e.Name, e.JobTitle)
}
// 使用例
func main() {
emp := Employee{
Person: Person{Name: "田中", Age: 30},
JobTitle: "エンジニア",
Salary: 5000000,
}
// Person のメソッドが自動的に利用可能
fmt.Println(emp.Introduce()) // Person.Introduce() が呼ばれる
fmt.Println(emp.Work()) // Employee.Work() が呼ばれる
// フィールドへの直接アクセス
fmt.Println(emp.Name) // emp.Person.Name の省略形
}
「継承」との違い
// 埋め込みは継承ではない
type Animal struct {
Name string
}
func (a Animal) Speak() string {
return a.Name + "が鳴いています"
}
type Dog struct {
Animal
Breed string
}
// メソッドのオーバーライド(実際は別メソッド)
func (d Dog) Speak() string {
return d.Name + "がワンワン鳴いています"
}
func main() {
dog := Dog{
Animal: Animal{Name: "ポチ"},
Breed: "柴犬",
}
fmt.Println(dog.Speak()) // Dog.Speak() が呼ばれる
fmt.Println(dog.Animal.Speak()) // Animal.Speak() が呼ばれる
// dog は Animal の「サブクラス」ではない
// var animal Animal = dog // コンパイルエラー
var animal Animal = dog.Animal // 明示的に Animal を取得
}
あらゆる型へのメソッド定義
組み込み型へのメソッド
// 整数型に独自メソッドを追加
type Temperature int
func (t Temperature) Celsius() float64 {
return float64(t)
}
func (t Temperature) Fahrenheit() float64 {
return float64(t)*9/5 + 32
}
func (t Temperature) String() string {
return fmt.Sprintf("%.1f°C", t.Celsius())
}
// 使用例
func main() {
temp := Temperature(25)
fmt.Printf("摂氏: %.1f°C\n", temp.Celsius())
fmt.Printf("華氏: %.1f°F\n", temp.Fahrenheit())
fmt.Println("温度:", temp.String())
}
関数型へのメソッド
// 関数型の定義
type Handler func(string) string
// 関数型にメソッドを定義
func (h Handler) ServeWithLogging(input string) string {
fmt.Printf("入力: %s\n", input)
result := h(input) // 関数を実行
fmt.Printf("出力: %s\n", result)
return result
}
// 使用例
func main() {
var upperHandler Handler = strings.ToUpper
result := upperHandler.ServeWithLogging("hello world")
fmt.Println("最終結果:", result)
}
スライス型へのメソッド
// スライス型にメソッドを追加
type StringSlice []string
func (ss StringSlice) Join(separator string) string {
return strings.Join(ss, separator)
}
func (ss StringSlice) Contains(target string) bool {
for _, s := range ss {
if s == target {
return true
}
}
return false
}
func (ss *StringSlice) Add(item string) {
*ss = append(*ss, item)
}
// 使用例
func main() {
names := StringSlice{"Alice", "Bob", "Charlie"}
fmt.Println(names.Join(", ")) // "Alice, Bob, Charlie"
fmt.Println(names.Contains("Bob")) // true
names.Add("David")
fmt.Println(names.Join(", ")) // "Alice, Bob, Charlie, David"
}
軽量な「オブジェクト」
重いクラス階層との比較
// Java/C++での重い階層(疑似コード)
abstract class Vehicle {
protected String brand;
protected Engine engine;
public Vehicle(String brand) {
this.brand = brand;
this.engine = new Engine();
}
public abstract void start();
public abstract void stop();
protected void initializeDefaults() { ... }
protected void validateBrand() { ... }
// 多くのボイラープレートコード
}
class Car extends Vehicle {
private int doors;
public Car(String brand, int doors) {
super(brand); // 親クラスの初期化
this.doors = doors;
initializeDefaults();
validateBrand();
}
@Override
public void start() { ... }
@Override
public void stop() { ... }
// さらに多くのコード...
}
Goでの軽量アプローチ
// Goでのシンプルな実装
type Vehicle interface {
Start()
Stop()
}
type Car struct {
Brand string
Doors int
}
func (c Car) Start() {
fmt.Printf("%sの車がエンジンをかけました\n", c.Brand)
}
func (c Car) Stop() {
fmt.Printf("%sの車がエンジンを止めました\n", c.Brand)
}
// 使用例
func main() {
car := Car{Brand: "Toyota", Doors: 4}
var vehicle Vehicle = car // インターフェースとして使用
vehicle.Start()
vehicle.Stop()
}
Go のOOPアプローチの利点
柔軟性
// 異なる「オブジェクト」が同じインターフェースを実装
type DatabaseLogger struct {
db *sql.DB
}
func (dl DatabaseLogger) Log(message string) {
// データベースにログ
}
type FileLogger struct {
filename string
}
func (fl FileLogger) Log(message string) {
// ファイルにログ
}
type Logger interface {
Log(string)
}
// どちらも Logger として使用可能
func processWithLogging(logger Logger) {
logger.Log("処理開始")
// 何かの処理
logger.Log("処理完了")
}
テストの容易さ
// モックオブジェクトの簡単な作成
type MockLogger struct {
messages []string
}
func (ml *MockLogger) Log(message string) {
ml.messages = append(ml.messages, message)
}
// テストでのモック使用
func TestProcessing(t *testing.T) {
mock := &MockLogger{}
processWithLogging(mock)
if len(mock.messages) != 2 {
t.Errorf("期待されるログメッセージ数: 2, 実際: %d", len(mock.messages))
}
}
Go言語のOOPアプローチは、従来の重い継承ベースのシステムよりもシンプルで柔軟性があり、実用的なソフトウェア開発により適した設計となっています。
おわりに
本日は、Go言語のよくある質問について解説しました。

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。
それでは、また明日お会いしましょう(^^)
コメント