LoginSignup
6
5

More than 5 years have passed since last update.

GolangのWritingWebApplicationsやってみた(日本語訳、翻訳)

Last updated at Posted at 2017-07-31

Gowikiをやってみたよ

Goを触ることになったので、導入で動かすミニマムなものを探していたところ、
良さげなのがあったのでやってみました。
gowikiリンク
入門編、拙い翻訳ですが悪しからず。。。

Introduction(導入)

このチュートリアルの対象

  • loadおよびsaveメソッドを使用したデータ構造の作成
  • net/httpパッケージを使用してWebアプリケーションを構築する
  • html/templateパッケージを使用してHTMLテンプレートを処理する
  • regexpパッケージを使用してユーザー入力を検証する
  • クロージャーの使用

想定される知識

  • プログラミング経験
  • 基本的なWeb技術(HTTP、HTML)の理解
  • 一部のUNIX / DOSコマンドラインに関する知識

Getting Started(はじめに)

現在、Goを実行するには、FreeBSD、Linux、OS X、またはWindowsマシンが必要です。
$はコマンドプロンプトの表示に使用します。
Install Go(インストール手順を参照)。
GOPATHに新しいディレクトリを作り、そこに移動してください

$ mkdir gowiki
$ cd gowiki

wiki.goというファイルを作成し、好きなエディタで開き、次の行を追加します。

wiki.go
package main

import (
    "fmt"
    "io/ioutil"
)

私たちはfmtとioutilパッケージをGoの標準ライブラリからインポートします。
後で、追加の機能を実装するときには、このimport内にパッケージを追加します。

Data Structures(データ構造)

データ構造を定義することから始めましょう。
wikiは一連の相互接続されたページで構成され、それぞれのページにはタイトルと本文(ページの内容)があります。
ここではPageというstructを、タイトルとボディを表す2つのフィールドを持つものとして定義します。

type Page struct { 
    Title string
    Body [] byte 
}

[]byteタイプは「byte slice」を意味します。(参照:sliceの使い方や詳細について
これから使用するioライブラリで定められているため、Body要素はstringというより[]byteです。

Page structは、ページデータをメモリに格納する方法を説明します。
しかし、永続ストレージはどうでしょう?次のsaveメソッドを作成することで対応できます。

func (p *Page) save() error {
    filename := p.Title + ".txt"
    return ioutil.WriteFile(filename, p.Body, 0600)
}

このメソッドの説明は以下の通りです。

"これは、返り値の受け手をpとしてのポインタをとる、saveという名前のメソッドです。
パラメータはなく、error型の値を返します。"

このメソッドは、PageのBodyをテキストファイルに保存します。
理解しやすくするために、ファイル名としてTitleを使用します。

このsaveメソッドはWriteFile(bitesliceをファイルに書き込む標準ライブラリ関数)の返り値であるため、error型を返します。
このsaveメソッドはerror値を返して、ファイルの書き込み中に何か問題が発生した場合にアプリケーションがそれを処理できるようにします。
すべてがうまくいけば、Page.save()はnilを返します。

WriteFileの3番目のパラメータとして渡された8進整数リテラル0600は 、ファイルが現在のユーザーに対してのみ読み書き可能アクセス許可で作成される必要があることを示します。

ページを保存するだけでなく、ページを読み込むこともできます:

func loadPage(title string) *Page {
  filename := title + ".txt"
  body, _ := ioutil.ReadFile(filename)
  return &Page{Title: title, Body: body}
}

この関数loadPage()は、titleパラメータからファイル名を構築し、ファイルの内容を新しい変数bodyに読み込みPage、適切なタイトルとボディ値で構築されたリテラルへのポインタを返します。

関数は複数の値を返すことができます。

標準ライブラリ関数io.ReadFile[]byteerrorを返します。
loadPage内で、エラーはまだ処理されていません。アンダースコア(_)記号で表される

"空白の識別子"は、エラーの戻り値を取り除くために使用されます。
(本質的に値を何も指定しません。これがないとコンパイルが通らなくなってしまいます)。

しかしReadFile()においてエラーが発生した場合はどうなるでしょう?
たとえば、ファイルが存在しない可能性があります。このようなエラーは無視してはいけません。
*Pageとerrorを返す関数を変更してみましょう

func loadPage(title string) (*Page, error) {
    filename := title + ".txt"
    body, err := ioutil.ReadFile(filename)
    if err != nil {
        return nil, err
    }
    return &Page{Title: title, Body: body}, nil
}

この関数の呼び出し元は、2番目のパラメータを確認できるようになりました。
2番目の返り値がnilであれば、ページを正常にロードしたということになります。
そうでない場合、それは error呼び出し側が処理できるものになります

この時点では、単純なデータ構造とファイルのセーブ、ロード機能があります。

私たちが書いたことをテストするために、main()を書いてみましょう:

func main() {
    p1 := &Page{Title: "TestPage", Body: []byte("This is a sample Page.")}
    p1.save()
    p2, _ := loadPage("TestPage")
    fmt.Println(string(p2.Body))
}

このコードをコンパイルして実行すると、p1を含んだTestPage.txtという名前のファイルが作成されます。

ファイルはp2に読み込まれ、Body要素は画面に表示されます。

あなたは次のようにプログラムをコンパイルして実行することができます:
$ go build wiki.go
$ ./wiki
This is a sample page.

(Windowsを使用している場合はwiki、 " ./"を入力してプログラムを実行する必要があります)。
これまでに書いたコードを見るには、ここをクリックしてください。

Introducing the net/http package(パッケージ紹介)

単純なWebサーバーのsampleです。

package main

import (
    "fmt"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:])
}

func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8080", nil)
}

このmain関数はhttp.HandleFuncへのコールで始まります。http.HandleFuncはhttpパッケージに対して、Webルート("/") へのすべての要求をhandlerによって処理するように指示をします。

次にhttp.ListenAndServeをコールします。
(今のところ、2番目のパラメータがnilであることについては心配しないでください。)
この関数は、プログラムが終了するまでブロックします。

関数handlerhttp.HandlerFuncと同じ型です。
引数としてhttp.ResponseWriterhttp.Requestをとります。

http.ResponseWriterの値は、HTTPサーバの応答を組み立てます。
これを書き込むことで、HTTPクライアントにデータを送信します。

http.Requestは、クライアントのHTTPリクエストを表すデータ構造です。
r.URL.PathはリクエストURLのパスコンポーネントです。
末尾の[1:]は、「最初から最後までのサブスライスを作成する」という意味があります。
これにより、パス名から先行する "/"が削除されます。

このプログラムを実行してURLにアクセスすると:
http://localhost:8080/monkeys

プログラムは以下のようになるでしょう。
Hi there, I love monkeys!

Using net/http to serve wiki pages(ページ表示のため、httpを使う)

net/httpパッケージ を使用するには、パッケージをインポートする必要があります。

import (
    "fmt"
    "io/ioutil"
    "net/http"
)

Wikiページを表示できる viewHandlerを作成しましょう。
このハンドラは接頭辞が "/ view /"のURLを処理します。

func viewHandler(w http.ResponseWriter, r *http.Request) {
    title := r.URL.Path[len("/view/"):]
    p, _ := loadPage(title)
    fmt.Fprintf(w, "<h1>%s</h1><div>%s</div>", p.Title, p.Body)
}

まず、この関数はリクエストURLのパスコンポーネントである[len("/view/"):]からページタイトルを抽出します。
このPathは/view/以下のみに再スライスされます。
これはバリデーションによって、view以下のみが重要だからです。

この関数はページデータをロードし、単純なHTMLの文字列でページをフォーマットし、それを変数wに書き込みます。

ここでも、loadPageから_のerrorを無視することに注意してください。
これは一般的に良くないですが、ここでは単純化するためそのままにします。
のちに修正します。

このハンドラを使用するために、main関数を書き換えて、/view/以下のどのようなパスでも対応できるようにviewHandlerを用いて、httpを初期化します。

func main(){ 
    http.HandleFunc "/ view /"viewHandler
    http.ListenAndServe ":8080"nil
}

これまでに書いたコードを見るには、ここをクリックしてください。

ページデータを作成し、コードをコンパイルし、wikiページをつくりましょう。
エディタでtest.txtファイルを作り、文字列「Hello world」(引用符なし)をその中に保存します。

$ go build wiki.go
$ ./wiki

(Windowsを使用している場合はwiki、 " ./"を入力してプログラムを実行する必要があります)。

http://localhost:8080/view/testにアクセスすると、"test"というタイトルのページが表示されます。

Editing Pages(ページの編集)

wikiはページを編集できないwikiではありません。
2つの新しいハンドラーを作成しましょう。
1つは「編集ページ」フォームを表示するeditHandler
もう1つはフォームから入力したデータを保存するsaveHandler

まず、main()に次のコードを追加します。

func main() {
    http.HandleFunc("/view/", viewHandler)
    http.HandleFunc("/edit/", editHandler)
    http.HandleFunc("/save/", saveHandler)
    http.ListenAndServe(":8080", nil)
}

この関数editHandlerはページをロードし(存在しない場合は空のPage構造体を作成する)、HTMLフォームを表示します。

func editHandler(w http.ResponseWriter, r *http.Request) {
    title := r.URL.Path[len("/edit/"):]
    p, err := loadPage(title)
    if err != nil {
        p = &Page{Title: title}
    }
    fmt.Fprintf(w, "<h1>Editing %s</h1>"+
        "<form action=\"/save/%s\" method=\"POST\">"+
        "<textarea name=\"body\">%s</textarea><br>"+
        "<input type=\"submit\" value=\"Save\">"+
        "</form>",
        p.Title, p.Title, p.Body)
}

この関数は正常に動作しますが、ハードコードされたHTMLはイケてないです。
より良い方法があります。

The html/template package(html/template パッケージ)

このhtml/templateパッケージはGo標準ライブラリの一部です。
基礎となるGoのコードを変更せずに、ページのレイアウトを変更することができ、別のファイルにHTMLを保存します。

まず、インポートのリストにhtml/templateを追加する必要があります。
fmtはもう使用しませんので、削除しなければなりません。

import (
    "html/template"
    "io/ioutil"
    "net/http"
)

HTMLフォームを含むテンプレートファイルを作成しましょう。
edit.htmlというファイルを作成し、次の行を追加します。

edit.html
<h1>Editing {{.Title}}</h1>

<form action="/save/{{.Title}}" method="POST">
<div><textarea name="body" rows="20" cols="80">{{printf "%s" .Body}}</textarea></div>
<div><input type="submit" value="Save"></div>
</form>

ハードコードされたHTMLの代わりにテンプレートを使用するようにeditHandlerも変更

func editHandler(w http.ResponseWriter, r *http.Request) {
    title := r.URL.Path[len("/edit/"):]
    p, err := loadPage(title)
    if err != nil {
        p = &Page{Title: title}
    }
    t, _ := template.ParseFiles("edit.html")
    t.Execute(w, p)
}

このtemplate.ParseFilesedit.htmlの内容を読み込み、*template.Templateを返します。

このメソッドt.Executeは、テンプレートを実行し、生成されたHTMLをhttp.ResponseWriterに書き出します。
また.Title.Bodyp.Titleおよびp.Bodyを指します。

テンプレートディレクティブは二重中括弧で囲まれています。
printf "%s" .Bodyという命令が出力する関数は、fmt.Printfと同じように、byteの代わりに文字列として.Bodyアウトプットを呼び出します。
このhtml/templateパッケージは、テンプレートアクションによって安全かつ正確に見えるHTMLのみが生成されることを保証します。
たとえば、sign(>)よりも大きいものを自動的にエスケープし、それを&gt;を用いて置き換えて、
ユーザーデータがフォームHTMLを壊さないようにします。

テンプレートを使用して作業しているので、viewHandlerのためのテンプレートを作成しましょう

view.html
<h1>{{.Title}}</h1>

<p>[<a href="/edit/{{.Title}}">edit</a>]</p>

<div>{{printf "%s" .Body}}</div>

それに応じて、viewHandlerも変更:

func viewHandler(w http.ResponseWriter, r *http.Request) {
    title := r.URL.Path[len("/view/"):]
    p, _ := loadPage(title)
    t, _ := template.ParseFiles("view.html")
    t.Execute(w, p)
}

ここで両方のハンドラでほぼ同じテンプレートコードを使用していることに注意してください。
重複部分を切り出してこの重複を削除しましょう:

func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
    t, _ := template.ParseFiles(tmpl + ".html")
    t.Execute(w, p)
}

そして、その関数を使用するようにハンドラを変更します:

func viewHandler(w http.ResponseWriter, r *http.Request) {
    title := r.URL.Path[len("/view/"):]
    p, _ := loadPage(title)
    renderTemplate(w, "view", p)
}
func editHandler(w http.ResponseWriter, r *http.Request) {
    title := r.URL.Path[len("/edit/"):]
    p, err := loadPage(title)
    if err != nil {
        p = &Page{Title: title}
    }
    renderTemplate(w, "edit", p)
}

mainの中で、未実装のsaveハンドラをコメントアウトした場合、もう一度プログラムを構築し、テストすることができます。
これまでに書いたコードを見るには、ここをクリックしてください。

Handling non-existent pages(存在しないページの処理)

もしあなたが/view/APageThatDoesntExist訪問したとしたらどうなるでしょう?
HTMLを含むページが表示されます。
これは、loadPageからのエラーを無視し、引き続きデータのないテンプレートを記入してしまうからです。
そうではなくて、要求されたページが存在しない場合は、コンテンツを作成できるようにクライアントを編集ページにリダイレクトする必要があります。

func viewHandler(w http.ResponseWriter, r *http.Request) {
    title := r.URL.Path[len("/view/"):]
    p, err := loadPage(title)
    if err != nil {
        http.Redirect(w, r, "/edit/"+title, http.StatusFound)
        return
    }
    renderTemplate(w, "view", p)
}

このhttp.Redirect関数は、http.StatusFound(302)のHTTPステータスコードとLocationヘッダーをHTTPに追加します 。

Saving Pages(ページの保存)

この関数saveHandlerは、編集ページにあるフォームの提出を処理します。
main内の関連する行のコメントを外した後、ハンドラを実装しましょう:

func saveHandler(w http.ResponseWriter, r *http.Request) {
    title := r.URL.Path[len("/save/"):]
    body := r.FormValue("body")
    p := &Page{Title: title, Body: []byte(body)}
    p.save()
    http.Redirect(w, r, "/view/"+title, http.StatusFound)
}

ページタイトル(URLで決定される)とフォームの唯一のフィールドBodyは、新しいにPage格納されます。
このsave()メソッドは、データをファイルに書き込むために呼び出され、クライアントは/view/ページにリダイレクトされます。

戻り値FormValueはstring型です。その値をPageに格納する前に変換する必要があります。
変換を実行するために[]byte(body)を使用します。

Error handling(エラー処理)

今の段階ではエラーが無視される場所がいくつかあります。これは良くないです。
なぜなら、エラーが発生したときにプログラムが意図しない動作をするからです。
より良い解決策は、エラーを処理し、エラーメッセージをユーザーに返すことです。
そうすれば、何かがうまくいかない場合、サーバーは望みどおりに機能し、ユーザーにそれを通知することができます。

まず、renderTemplate内でエラーを処理しましょう:

func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
    t, err := template.ParseFiles(tmpl + ".html")
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    err = t.Execute(w, p)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
    }
}

このhttp.Error関数は、指定されたHTTP応答コード(この場合は「内部サーバーエラー」)とエラーメッセージを送信します。すでにこれを別の機能に入れると決定されています。

さあ、saveHandlerを修正しましょう:

func saveHandler(w http.ResponseWriter, r *http.Request) {
    title := r.URL.Path[len("/save/"):]
    body := r.FormValue("body")
    p := &Page{Title: title, Body: []byte(body)}
    err := p.save()
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    http.Redirect(w, r, "/view/"+title, http.StatusFound)
}

その間p.save()で発生したエラーはユーザーに報告されます。

Template caching(テンプレートキャッシング)

このコードには非効率的な部分があります。
それはページがレンダリングされるたびにrenderTemplateが呼び出されることです。
より良いアプローチは、すべてのテンプレートを1つにまとめ、プログラムの初期化時に一度だけ呼び出すことです。
そのために、このExecuteTemplateメソッドを使用して特定のテンプレートをレンダリングできます 。

最初に、templatesという名前のグローバル変数を作成し、それを使って初期化します。

var templates = template.Must(template.ParseFiles( "edit.html"、 "view.html"))

関数template.Mustは、nil error以外の値を渡すと騒いでくれる便利なラッパーです。
それ以外の場合は、変更されていない*Templateが返されます。
テンプレートを読み込めない場合は、プログラムを終了することが賢明でしょう。

このParseFiles関数は、テンプレートファイルを識別する任意の数の文字列引数を取り、それらのファイルをベースファイル名の後に名前が付けられたテンプレートに出力します。
プログラムにテンプレートを追加する場合は、その名前をParseFilesのコール部分の引数に追加します。

次に、適切なテンプレートの名前でtemplates.ExecuteTemplateメソッドを呼び出すようにrenderTemplate関数を変更します。

func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
    err := templates.ExecuteTemplate(w, tmpl+".html", p)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
    }
}

テンプレート名はテンプレートファイル名なので、tmpl引数に".html"を追加する必要があることに注意してください。

Validation(バリデーション)

お気付きのように、このプログラムには重大なセキュリティ上の欠陥があります。
ユーザーはサーバー上で読み書きできる任意のパスをいくらでも提供できます。
これを解決するために、タイトルを正規表現で検証する関数を書きましょう。

まず、importリストにregexp追加します。
次に、検証する式を格納するグローバル変数を作成します。

var validPath = regexp.MustCompile("^/(edit|save|view)/([a-zA-Z0-9]+)$")

この関数regexp.MustCompileは、正規表現を解析してコンパイルし、regexp.Regexpを返します。
コンパイル中に2番目のパラメータとしてエラーが返ってきた場合に反応するために、MustCompileCompileと区別されます。

validPathを使ってパスを検証し、ページのタイトルを抽出する関数を作成しましょう:

func getTitle(w http.ResponseWriter, r *http.Request) (string, error) {
    m := validPath.FindStringSubmatch(r.URL.Path)
    if m == nil {
        http.NotFound(w, r)
        return "", errors.New("Invalid Page Title")
    }
    return m[2], nil // The title is the second subexpression.
}

タイトルが有効な場合は、nil,エラー値が共に返されます。
タイトルが無効な場合、関数はHTTP接続に「404 Not Found」エラーを書き込み、ハンドラにエラーを返します。
新しいエラーを作成するには、errorsパッケージをインポートする必要があります。

それぞれのハンドラにgetTitleを入れてみましょう:

func viewHandler(w http.ResponseWriter, r *http.Request) {
    title, err := getTitle(w, r)
    if err != nil {
        return
    }
    p, err := loadPage(title)
    if err != nil {
        http.Redirect(w, r, "/edit/"+title, http.StatusFound)
        return
    }
    renderTemplate(w, "view", p)
}
func saveHandler(w http.ResponseWriter, r *http.Request) {
    title, err := getTitle(w, r)
    if err != nil {
        return
    }
    body := r.FormValue("body")
    p := &Page{Title: title, Body: []byte(body)}
    err = p.save()
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    http.Redirect(w, r, "/view/"+title, http.StatusFound)
}
func editHandler(w http.ResponseWriter, r *http.Request) {
    title, err := getTitle(w, r)
    if err != nil {
        return
    }
    p, err := loadPage(title)
    if err != nil {
        p = &Page{Title: title}
    }
    renderTemplate(w, "edit", p)
}

Introducing Function Literals and Closures(関数リテラルとクロージャの導入)

各ハンドラでエラー状態をキャッチしようとすると、多くの繰り返しコードが発生します。
この各ハンドラで行われる検証とエラーチェックを行う関数をラップすることができたらどうでしょうか?
Goの関数リテラルは、リファクタするための強力な手段を提供します。

まず、各ハンドラの関数定義を書き換えて、タイトル文字列を受け取れるようにします。

func viewHandler(w http.ResponseWriter, r *http.Request, title string)
func editHandler(w http.ResponseWriter, r *http.Request, title string)
func saveHandler(w http.ResponseWriter, r *http.Request, title string)

次に、上記の型の関数を受け取り、http.HandleFuncと同じ型の関数を返すラッパー関数を定義しましょう(http.HandlerFunc関数に渡すのに適したもの):

func makeHandler(fn func (http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // Here we will extract the page title from the Request,
        // and call the provided handler 'fn'
    }
}

返される関数は、その外に定義された値を囲むのでクロージャと呼ばれます。
この場合、変数fn(makeHandlerの単一の引数)はクロージャで囲まれます。
変数 fnは、保存、編集、またはビューハンドラの1つになります。

さて、getTitleからコードを取り出し、ここで使用することができます(小さな変更は必要です):

func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        m := validPath.FindStringSubmatch(r.URL.Path)
        if m == nil {
            http.NotFound(w, r)
            return
        }
        fn(w, r, m[2])
    }
}

makeHandlerから返されるクロージャはhttp.ResponseWriterまたはhttp.Requestをとる関数です(つまりhttp.HandlerFuncです)。
クロージャは指定されたパスからタイトルを抽出し、TitleValidator regexpでそれを検証します。無効な場合、関数の使用にエラーが書き込まれます。
有効な場合、囲まれたfn関数は、引数としてResponseWriter, Request, titleに呼びだされます。

今度は、httpパッケージに登録される前に、ハンドラ関数をmainの中のmakeHandlerによってラップすることができます:

func main() {
    http.HandleFunc("/view/", makeHandler(viewHandler))
    http.HandleFunc("/edit/", makeHandler(editHandler))
    http.HandleFunc("/save/", makeHandler(saveHandler))

    http.ListenAndServe(":8080", nil)
}

最後に、ハンドラ関数からgetTitleの呼び出しを削除し、より簡単にします。

func viewHandler(w http.ResponseWriter, r *http.Request, title string) {
    p, err := loadPage(title)
    if err != nil {
        http.Redirect(w, r, "/edit/"+title, http.StatusFound)
        return
    }
    renderTemplate(w, "view", p)
}
func editHandler(w http.ResponseWriter, r *http.Request, title string) {
    p, err := loadPage(title)
    if err != nil {
        p = &Page{Title: title}
    }
    renderTemplate(w, "edit", p)
}
func saveHandler(w http.ResponseWriter, r *http.Request, title string) {
    body := r.FormValue("body")
    p := &Page{Title: title, Body: []byte(body)}
    err := p.save()
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    http.Redirect(w, r, "/view/"+title, http.StatusFound)
}

Try it out!(やってみよう!)

ここをクリックして、最終的なコードリストを表示してください。

コードを再度コンパイルし、アプリケーションを実行します。

$ go build wiki.go
$ ./wiki

http://localhost:8080/view/ANewPageへの訪れようとした場合、ページの編集フォームが提示されるはずです。
テキストを入力して[保存]をクリックし、新しく作成したページにリダイレクトすることができます。

Other tasks(その他のタスク)

もうすでに取り組んで簡単な作業は次のとおりです:

  • templatesをtmpl/内、ページデータをdata/の中に保存する。
  • /view/FrontPageにリダイレクトするハンドラを追加する。
  • CSSを追加して装飾する。
  • [PageName]のインスタンスを<a href="/view/PageName">PageName</a>に変換してページ間リンクを実装する。(ヒント:regexp.ReplaceAllFuncを使って見て)

所感

  • 初めてコンパイル型の言語を触った。cなどの言語に比べたらかなり柔軟らしいけど、挙動を厳格に明記するコーディングはむしろ自分にとっては気持ちが良かった。
  • 特に他人のコードを少し読んで見ても、インタプリタ型のソースより明らかにわかりやすく、学習意欲が湧いた。
  • goって検索しにくい。golangで統一してくれ。
6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5