🐭

C# -> Golang

2024/09/03に公開

まえがき

C#とGo言語を比べつつ、Go言語を説明します。

C#についての詳しい説明は割愛します。

言語概要

  • Google が開発したプログラミング言語です。「Go言語」や「Golang」と表記されます。
  • UNIX、B言語(C言語の元)、UTF-8の開発者ケン・トンプソンや、UNIX、Plan 9、UTF-8の開発者ロブ・パイクによって設計されました。
  • 静的型付け、メモリ安全性、ガベージコレクションを備えるコンパイル言語です。
  • シンプル、高速、メモリ効率が良い、メモリ破壊が無い、並行処理が得意などの特徴を備えています。
  • メモリ破壊が無く、並行処理を得意とする、進化したC言語という側面があります。
  • Linux、Mac OS X、Windows、Android、iOS で動作します。

言語仕様

セミコロン

C系統の言語と違い、基本的にセミコロンは必要ない

int hoge = 123;
int fuga = 456;
// セミコロンは各行に書く必要は無いが、
// 書いてもコンパイル自体に影響はない
var hoge int = 123;
// 次のように、一行で記述する際
// 区切りとして利用する場合がある
hoge := 123; fuga := 456

インデント

C++と同じインデント

インデントや波括弧の位置を正しく記述しないと、構文として認識されずにコンパイルエラーが起こる場合がある

public static void Main()
{
	Console.WriteLine("Hello World");
}
func main() {
	fmt.Println("Hello World")
}

未使用の変数

Go言語では未使用の変数があると、コンパイルエラーが起こる

ブランク変数 _

宣言する必要はあるが、値を使いたくない場合には

ブランク変数 _ を利用することで、未使用でもコンパイルエラーが起こらない

var hoge = 10 // 未使用だとコンパイルエラー
var _ = 0 // 未使用でも問題ない

命名規則

言語仕様による命名規則として、

小文字からスタートするのは private

大文字からスタートするのは public

となっている

インポート

C#でいうusing

using System;
using UnityEngine;
// 文字列で指定する
import "fmt"
// 括弧で括ると一気にimportできる
import (
// 改行の際に何もいらない
	"os"
	"fmt"
)

C#とだいたい同じ

bool				真偽値(true or false)
int8/int16/int32/int64		nビット整数
uint8/uint16/uint32/uint64	nビット非負整数
float32/float64		nビット浮動小数点数
complex64/complex128		nビット虚数
byte				1バイトデータ(uint8と同義)
rune				1文字(int32と同義)
uint				uint32 または uint64
int				int32 または int64
uintptr				ポインタを表現するのに十分な非負整数
string				文字列

C#との大きな違い

float

float32 / float64 と明示的に書く必要がある (int型は必要ない)

rune

char型とは中身が微弱に違う

string

C#のようにstring[n] で1文字指定して取得することはできない

Goの場合、string[n]だとbyte単位、for文で取り出すとrune単位

変数宣言

Go言語は動的型付け言語なので構文が特殊です。

varステートメント を変数 (Variables**)** 宣言に利用します。

int hoge = 123;
// C#での型推論 見やすいね
var piyo = 456;
// 省略無しの変数宣言(初期値は0)
var hoge int = 123
// 初期値により型名が明白な場合、
// 型名を省略できる
var hoge2 = 123
// 初期値を指定する場合に = ではなく、
// := を用いるとvarも省略可能 
hoge3 := 123

一括変数宣言と代入

// C#は一括で宣言は行えるが
// 初期値を指定することはできない
int a, b;
// 同じ値を代入する場合は=で繋げることができる
a = b = 3;
// 丸括弧で括ると一括で宣言を行える
var (
	hoge int
	hoge2 int = 456
	// ここで初期値も指定しているので、
	// 型名も省略できる
	hoge3 = 789
)

// 複数個の変数に対しても型名の省略と
// 代入を行える
piyo, piyo2, piyo3 := 1, 2, 3

定数 (const)

// C#のconst
const int hoge = 334;

// c# 10.0 からは文字列補完でも、
// {}の中身がconst文字列の場合に限り、
// 補完結果もconstにできる
const string hoge = "hoge";
const string fuga = "fuga";
const string piyo = $"{hoge} {fuga}";
// piyoには "hoge fuga" が代入されている

// 次のように中身が文字列ではない場合エラーになる
const int hage1 = 334;
const string hage2 = $"{hage1}"; // エラー
// 型無し定数 (untyped constant)
// 指定された値によって型推論する
const hoge = 334
// 明示的に型を指定していないので
// 次のように異なるデータ型変数に代入できる
var fuga int = hoge
var piyo float64 = hoge

// 複数の定数宣言も行える
const (
	hage1 = 123
	hage2 = 456
)

// 型付き定数 (typed constant)
// 型を明示的に指定して宣言する
const hoge int = 334
// 型が明示されているので
// 同じ型の変数にしか代入できない
var fuga int = hoge // これはOK
var piyo float64 = hoge // エラー
// 代入する場合には明示的なキャストが必要
var piyo float64 = float64(hoge)

// 複数の宣言でも型無し定数と
// 型付き定数を宣言できる
const (
	hoge = 0
	piyo int = 1
)

(Goのみ)定数への値の自動割り当て

定数の宣言時に iotaキーワード を割り当てると、0から順番に数値の連番を割り当てる

const (
	hoge = iota // 0
	fuga // 1
	piyo // 2
	hage // 3
)
// iotaに値を割り当てると、その数基準に連番となる
const (
	hoge2 = iota + 10 // 10
	fuga2 // 11
	piyo2 // 12
	hage2 // 13
)

標準入出力

Go言語では標準入出力の種類が多め

標準入力 (fmt ライブラリ)

// 改行またはスペースまで読み込む
// 型は事前に定義しておく
var str string
fmt.Scan(&str)
// スペース区切りの数が分かっている入力を受け取る
var arr [3]int
fmt.Scan(&arr[0], &arr[1], &arr[2], &arr[3])

標準入力 (bufio ライブラリ)

// 入力はstring型で受け取る
// 標準入力を受け付けるスキャナ
var scanner *bufio.Scanner = bufio.NewScanner(os.Stdin)
// 1行分のスキャンを行い、スキャナ内に格納する
scanner.Scan()
// スペース区切りで分割する
var inputs []string = strings.Split(scanner.Text(), " ")

標準出力 (fmt ライブラリ)

var name string = "hoge"
var age int = 24
// 改行無しで与えられた引数をスペース区切りで標準書式(%v)で出力
fmt.Print("Name:", name, " Age:", age) // 出力: Name:hoge Age:24
// 改行有りで与えられた引数をスペース区切りで標準書式で出力
fmt.Println("Name:", name, "Age:", age) // 出力: Name:hoge Age:24 (\n)
// 書式指定子を利用して、値を出力する
fmt.Printf("Name: %s Age: %d", name, age) // 出力: Name: hoge Age: 24

明示的なキャスト

Go言語はキャストする際に変数名を丸括弧でくくる

int hoge = 123;
float fuga = (float)hoge;
var hoge int = 123
var fuga float64 = float64(hoge)

配列 (array)

Go言語における配列とは、コンパイル時にサイズが決まっていて変更不可のもの

途中でサイズを変更することはできない

int[] hoge = new int[n];
var hoge = new int[n];
int[] hoge = new[] {n1, n2, n3};
int[] hoge = {n1, n2, n3};
// C# 12からはコレクション式が利用できる
int[] hoge = [n1, n2, n3];
// 型名の前にサイズを指定する
var hoge [n]int = [n]int{}
// 左辺の型名を省略する場合、
// サイズ指定は右辺のみ
var hoge = [n]int{}
hoge := [n]int{}

// サイズに...を指定すると、
// 初期値に指定された値の数によって
// 配列のサイズが決まる
hoge := [...]int{1, 2}
// 次のように:を使用すると
// n番目の要素に1を指定できる
// それ以外の要素は0
hoge := [...]int{n: 1}

使い方はどちらも同じ

スライス (slice) C#でいうListに近い

Go言語において、厳密ではないがイメージとして

サイズが固定のもの → 配列

サイズが可変のもの → スライス

と考えると良いかもしれない

List<int> hoge = new List<int>();
var hoge = new List<int>();

// C# 12からはコレクション式が利用できる
List<int> hoge = [];

// 要素追加
hoge.Add(334);
// 宣言自体は配列とほぼ同じ
// サイズは指定しない
var hoge []int = []int {}
var hoge = []int{}
var hoge := []int{}
// スライスは配列への参照型で値を持つので、
// 配列から部分列を取り出して作成することもできる
hoge := [...] string{"piyo", "piyo2"}
fuga := hoge[0:2] // fuga = [piyo piyo2]
// 上記の操作は次の指定方法がある
arrName[n1:n2] // n1 から n2 - 1まで
arrName[n:] // n から末尾まで
arrName[:n] // 先頭から n - 1 まで
arrName[:] // 先頭から末尾まで
// 要素の追加は
// append(追加するスライス, 追加する要素)
newHoge = append(hoge, n)
// appendとは末尾に要素を追加した新しいスライスを返す

<aside>
💡 スライスは配列への参照型で値を持つ

</aside>

要素数の確認

List<int> hoge = [1, 2, 3];
hoge.Count;

int[] hoge = [1, 2, 3];
hoge.length;
// 
arr := [...]int {1, 2, 3}
slice := arr[0:2]
// 長さ(length)はそれに含まれる要素数
// 容量(capacity)はスライスの最初の要素
// から数えて、元となる配列の要素数
len(slice) // sliceの要素数は2
cap(slice) // sliceの生成元の要素数は3

サイズを指定しての宣言

List<int> hoge = new List<int>(n)
// make()関数は
// 第一引数 []T 型
// 第二引数 len 長さ
// 第三引数 cap 容量
hoge := make([]int, n1, n2)

スライス (List) は容量超過時に倍のメモリを確保し要素をコピーするので、

宣言時に容量をあらかじめ確保しておくことで、超過時のメモリ再確保を減らして速度を高めることができる

スライスの初期値

null ではなく、nil

マップ(map) C#でいうDictionary<T>

Dictionary<string, int> hoge = new()
{
    {"x", 100},
    {"y", 200}
};
// map[keyの型]valueの型で宣言し、
// 要素の初期化は : 区切りで行う
hoge := map[string]int {
	"x": 100,
	"y": 200, // 末尾の , は省略できない
}
// マップに要素追加
hoge["addKeyName"] = value
// マップの要素削除
delete(hoge, "keyName")
// マップの長さを求める
len(hoge)
// マップに要素が存在する
_, flag := hoge["searchKeyName"]

If 文

インデントと括弧にさえ気を付ければ、どちらも同じ

if (hoge == true)
{
	Console.WriteLine("hoge");
}
else if (fuga == false)
{
	Console.WriteLine("fuga");
}
else
{
	Console.WriteLine("piyo");
}
// 条件に括弧は必要ない
if hoge == true {
	fmt.Println("hoge")
// if文を続ける際に下記の通りにインデント、
// 改行をしないとコンパイルエラー
} else if fuga == false {
	fmt.Println("fuga")
} else {
	fmt.Println("piyo")
}

Switch文

基本的なSwitch文のみ扱う

パターンマッチングなどは扱わない

int hoge = 334;
switch (hoge)
{
	case 3:
		Console.WriteLine("hoge");
		break;
	case 34:
		Console.WriteLine("fuga");
		break;
	// 上記のcaseの条件に当てはまらない場合
	// default内の処理が実行される
	default:
		Console.WriteLine("piyo");
		break;
}

hoge := 334
// Go言語ではbreakや括弧は記述しない
// switch 式 {...} では式の値によって
// 処理を振り分ける
switch hoge {
	case 3:
		fmt.Println("hoge")
	case 34:
		fmt.Println("fuga")
	default:
		fmt.Println("piyo")
}

// switch {...} ではcase分に条件を記述できる
switch {
	case fuga < piyo:
		fmt.Println("piyo big")
	case fuga > piyo:
		fmt.Println("fuga big")
	default:
		fmt.Println("Equal")
}

// 次の処理も実行できるfallthrough句がある
switch hoge {
	case 3:
		fallthrough
	case 34:
		fmt.Println("fuga")
	default:
		fmt.Println("piyo")
}
// この場合hogeが3または34の場合に
// "fuga"が実行される

While文

Go言語にそんなものは無い

For文

通常のfor文ではどちらも同じ

括弧は必要ない

for (var i = 0; i < 10; i++)
{
	Console.WriteLine("hoge");
}
// 通常のカウントアップ式for文
for i := 0; i < 10; i++ {
	fmt.Println("hoge")
}
// 条件を省略すると無限ループ
for {
	n++
	if n > 10 {
		break
	} else if n % 2 == 1 {
		continue
	} else {
		fmt.Println("hoge")
	}
}
// C#でいうForeach文
// range句を使用して中身を取り出す
for index, item := range array {
	fmt.Printf("%d: %s\n", index, item)
}

Goto文

goto文は指定したラベルにジャンプする

セミコロン以外どちらも同じ

goto LABEL; // LABEL: の位置に遷移する
// skip
// skip
// skip
// skip
LABEL:
	Console.WriteLine("hoge");
	
goto LABEL // LABEL: の位置に遷移する
// skip
// skip
// skip
// skip
LABEL:
	fmt.Println("hoge")

関数宣言

C#と違い、戻り値で複数の型の値を返すことができる

// 戻り値無しという概念は無く
// あくまでも戻り値はvoidである
public void funcA()
{
}
// params を用いると可変引数を実現できる
public void funcB(params int[] x)
{
}
// 関数宣言時に func句を使用する
// 括弧の中に引数、後ろに戻り値を指定する
func funcA(x int, y int) int {
	return x + y
}
// 戻り値が複数の場合には括弧で括る
func funcB(x int, y int)(int, int) {
	return x + y, x - y
}
// ... を用いることで可変引数を実現できる
func funcC(a int, b ... int) {
	for i, num := range b {
	}
}
// 戻り値無しの場合
// 括弧の後ろに何も記述しない
func funcD() {
}

構造体

Go言語ではクラスが無いので、構造体を使用する

構造体にはメンバ変数のみを定義し、クラスメソッドに相当する関数は関数名の前に、

**(**thisに相当する変数 *構造体名) をつけて定義する。

public struct Person
{
    public string name;
    private int age;
    
    public void Func(string name)
    {
	    this.name = name;
    }
}
var hoge = new Person
{
	name = "hoge",
};
// 宣言時にtype句を使用
type Person struct {
	name string
  age int
}

func (p *Person) SetFunc(name string) {
	p.name = name
}

func (p *Person) GetFunc() string {
	return p.name
}
// 構造体のメンバの内、
// 大文字で始まるものは外からアクセス可能
// 小文字で始まるものは外からアクセス不可
type Person struct {
	Name string // 外部からアクセス可能
	age int // 外部からアクセス不可
}
// 構造体を使用する際のパラメータの初期化
// 順序通りに初期化
hoge := Person{"Yamada", 24}
// 変数名で初期化
fuga := Person{name: "Tanaka", age: 25}

type句について

構造体やインターフェース宣言時によく用いられるが、意味としては違うので気を付ける

Goを学びたての人が誤解しがちなtypeと構造体について #golang - Qiita

インターフェース

インターフェースの利用方法はどちらも同じで、

命名に関しては接頭詞に I を付けることはない

public interface IHoge
{
	public void Hoge();
}

public class Fuga() : IHoge
{
	public void Hoge()
	{
	}
}
// インターフェース宣言
type Hoge interface {
	Hoge()
	Fuga() int
	Piyo(i int) int
}
type name struct {}
// インターフェース実装
type (n name) Hoge() {}

匿名型 (interface {}型)

匿名の型によって、型を明示的に定義しない型

// シンプルな宣言
var hoge = new
{
	Name = "hoge",
	age = 334,
};
// LINQなどで使うことが多い
// 宣言はこのように行う
func hoge(a interface {}) {}
// .(型名)でinterface{}型を他の型へ変換できる
func fuga(a interface {}) {
	fmt.Printf("%d", a.(int))
}
// 型変換の第二戻り値は型変換可能かどうかを返す
func piyo(a interface {}) {
	str, ok := a.(string)
}

ポインタ (pointer)

Go言語はガベージコレクション(GC)が搭載されているので、メモリリークは基本的に起こらない

メモリ割り当て自体は、内部で自動的にヒープ領域かスタック領域に割り当てられる

これらの機能により、メモリ安全な言語である

// オブジェクトのポインタを参照するには &
// ポインタの中身を参照するには *
var a int // int型変数
var p *int // intへのポインタ変数
p = &a // p に a のポインタを設定
*p = 334 // ポインタの中身に代入

// 演算子 . は、構造体のメンバ変数でも、ポインタでアクセスすることができる
a1 := Person{"Tanaka", 26}	// 構造体Personのオブジェクトa1を確保して初期化
p1 := &a1			// 構造体a1へのポインタをp1に格納
fmt.Println(a1.name)		// メンバ変数には左記のようにアクセス
fmt.Println((*p1).name)		// ポインタpの中身(後続体)のメンバ変数には左記のようにアクセス
fmt.Println(p1.name)		// ただし、Go言語ではこれを、左記のようにも記述できる

領域確保 (new)

ポインタ周りの話なので、C#は割愛

new()句 を用いて領域を動的に確保し、その領域へのポインタを得ることができる。

確保した領域は参照されなくなった後にガベージコレクションにより自動的に開放される。

type Hoge struct {
	name string
}

hogeArr := []* Hoge{}
// 構造体のポインタを得る
hoge := new(Hoge)
hoge.name = "fuga" // (*hoge).name = "fuga" と同じ意味

参考資料

A Tour of Go

とほほのGo言語入門 - とほほのWWW入門

たくさんのサイト

Discussion