LoginSignup
119
94

More than 1 year has passed since last update.

Goで超簡易版Twitterを作ってみました(初心者向け)

Last updated at Posted at 2020-01-16

#はじめまして
最近Goの勉強を始めたばかりで何か開発してみようと思い、簡単なアプリを開発しました。
twitterもやってます@khm_itac

##見た目
スクリーンショット 2020-01-16 11.57.40.png
##出来る事
・フォームから投稿する事が出来ます
・投稿をデータベースに保存する事が出来ます
・保存されている投稿を表示してくれます
・投稿を消す事も出来ます

はい、それだけです。
めちゃくちゃ低レベルですが、UIからサーバーにデータを送りDBへ保存、DBのデータをビューに表示するというのは全てのアプリの基本的な部分だと思いますので、これが出来るとようやくエンジニアレベル1達成です!
僕と同じようにこれからGoの勉強を始める方の少しでも役に立てればと思い記事を投稿しました。
僕も初心者ですので間違ってる部分や至らない点があるかと思いますがご了承ください。

##解説
Goの導入や基礎文法やVSCodeの使い方などは解説しません、僕より詳しい人がYouTube等で動画を出していますのでそちらで勉強した方がいいと思います(あとProgateとか)
Macでの開発ですのでwindowsの方はうまく動かない可能性もあります

まずmain.goを作ります
メインディレクトリの直下でいいと思います。

スクリーンショット 2020-01-16 12.59.20.png
この画面上でF5を押すとスタートさせる事が出来ます、その後http://localhost:8080 
にアクセスすると
スクリーンショット 2020-01-16 13.01.28.png
この画面になります
これでサーバーを建てられました。
Goでは起動させると func main() が動き出します。
なのでmain関数の中に書いてあるhttp.ListenAndServe(":8080", nil)が起動するという事ですね。
8080という数字はお好みで大丈夫です、他の数字でも動きます。
ちなみにimport "net/http"も書いてあげないと動きません。
Goでは使用したい機能(パッケージ)をimportしてあげる必要があります。
ではサーバーを起動する事が出来たのでHTMLを表示させたいと思います。

メインディレクトリの下にviewsディレクトリを作成し、その中にindex.htmlを作ります
そして適当に文字を書いてみます
スクリーンショット 2020-01-16 13.18.07.png
そしてこのHTMLファイルをmain.goから呼び出す記述を書いてみます
スクリーンショット 2020-01-16 13.32.38.png

この記述後に再起動してみて、http://localhost:8080 
をリロードしてみてください
スクリーンショット 2020-01-16 13.25.22.png
このようにHTMLの文字が画面に表示されると成功です
###解説
http.HandleFuncは第一引数でURLのパスを指定し、第二引数で動かしたい関数を指定します。
今回第一引数は "/" なのでルートパスがリクエストされた時にindexHandlerという関数を呼び出しているという事です。
次にindexHandlerの中身ですが、10行目でtemplate.ParseFilesにHTMLファイルを指定しその中身を tとerrという変数に代入しています。11行目で、もしtemplate.ParseFilesに渡したHTMLが存在しなければerr変数にエラーが代入されエラーを出してくれます。
この if err != nil { log.Fatalln(err) }という記述はGoではめちゃめちゃ出てきますので、とりあえずお決まりの文という認識でいいと思います。(エラーが出た時に内容を教えてくれるよーというものです)
で、HTMLが存在すればその t変数に対してExecuteメソッドを使用するとHTMLを表示する事が出来ます。
Executeに渡している引数ですが、第一引数にResponseWriterを渡しています(これもお決まり文という認識で大丈夫です)第二引数にmain.goからHTMLに渡すデータを指定してあげる事が出来ます。
今回は何も渡さないのでnilにしておきます。

とりあえずHTMLを完成させます、こんな感じにしました
スクリーンショット 2020-01-16 14.06.20.png
次にCSSファイルを作成します。
メインディレクトリの下にresourcesディレクトリを作り、さらにcssディレクトリを作ります。その中にview.cssを作ります。こんな感じです

header{
      height:50px;
      line-height: 50px;
      border:1px solid lightgray;
      text-align: center;
      color: rgb(142, 168, 184);
      background-color: rgb(249, 252, 252);
      }
    .tweet_view{
      float: left;
      height: 100vh;
      width: 50vw;
      background-color: rgb(235, 243, 243);
      padding-top: 20px;
    }
    .tweet_message{
      background-color: white;
      color: gray;
      font-size: 30px;
      margin: 0 20px 15px;
      padding: 10px;
      position: relative;
      border-radius: 15px;
    }
    button.tweet_delete_button{
      background-color: rgb(130, 168, 238);
      border-style: none;
      height: 20px;
      width: 20px;
      position: absolute;
      right: 10px; 
      bottom: 17px;
      color: rgb(255, 255, 255);
      line-height: 5px;
      padding-right: 18px;
      cursor: pointer;
    }
    button:focus {
	    outline:0;
    }
    .tweet_form{
      height: 100vh;
      width: 100vw;
    }
    textarea{
      background-color: rgb(235, 243, 243);
      width: 450px;
      height: 250px;
      margin: 50px 0 0 120px;
    }
    input{
      width: 200px;
      height: 50px;
      background-color: rgb(130, 168, 238);
      color: white;
      position: absolute;
      left: 68vw;
      top: 400px
    }

ではリロードしてみます
スクリーンショット 2020-01-16 15.17.11.png

あれ、CSSが効いてませんね。
Goでは静的ファイルを読み出す時はmain関数内で宣言をしてあげないと行けないらしいです。
ではmain.goにcssを呼び出す記述を書きます


func main() {
	http.Handle("/resources/", http.StripPrefix("/resources/", http.FileServer(http.Dir("resources/"))))
	http.HandleFunc("/", indexHandler)
	http.ListenAndServe(":8080", nil)
}

一番上にhttp.Handleという関数を追加しました。
正直自分はコードの細部は理解出来ていなんですが、とりあえずresourcesディレクトリを呼び出してくれる関数という事らしいです(なのでコピペで大丈夫です)
この記述後もう一度リロードしてみます(cm + sf + R でスーパーリロードした方がいいかも)
スクリーンショット 2020-01-16 15.29.34.png
今度はちゃんとCSSが効きました!

###次はデータベースを導入します
SQLite3というDBを導入します。導入用記事はまた今度書きます(ググればすぐ出来ます)

ではDBインストール出来たのでmain.goにインポートします

package main

import (
	"database/sql"
	_ "github.com/mattn/go-sqlite3"
	"log"
	"net/http"
	"text/template"
)

var DbConnection *sql.DB

func indexHandler(w http.ResponseWriter, r *http.Request) {
	t, err := template.ParseFiles("views/index.html")
	if err != nil {
		log.Fatalln(err)
	}
	t.Execute(w, nil)
}

func main() {
	http.Handle("/resources/", http.StripPrefix("/resources/", http.FileServer(http.Dir("resources/"))))
	http.HandleFunc("/", indexHandler)
	http.ListenAndServe(":8080", nil)
}

上記のようにimportした後、var DbConnection *sql.DBでDBを定義してあげます。
これでDbConnectionという名前でDBにアクセス出来ます。
では実際にDBにデータベースを作ってみます。

今回はターミナルからSQLコマンドを打ちます。メインディレクトリでsqlite3 example.sqlと打ち込んでください。
スクリーンショット 2020-01-16 16.07.43.png
こんな風になると思います、次に**CREATE TABLE tweets (id INTEGER PRIMARY KEY,tweet STRING);**と打ち込みます。
これはtweetsという名前のテーブルを作成し、idとtweetというカラムを作成するというコマンドです。
tweetは文字型なのでstring、idはintegerにします。
idのprimary keyとは、何もしなければ自動的に数字を割り振ってくれるというものです。

これでtweetsテーブルは作成出来ました、一応確認してみます。ターミナルで.tabelと打ち込んでください。
スクリーンショット 2020-01-16 17.09.37.png

tweetsテーブルが作成されているのを確認出来ました。

###ではフォームに入力した値を実際にDBに入れてみます。
index.htmlの14行目に注目してください。

<form action="/tweet/" method="POST" class="tweet_form">
 <textarea name="tweet" rows="20" cols="80"></textarea>
 <input type="submit" value="speak">
</form>

actionに"/tweet/"を指定してるので、ボタンを押すとhttp://localhost:8080/tweet/
というURLが走ります、なのでmain関数にそのパスが通った時に呼び出される関数を記述すればいいという訳です。

package main

import (
	"database/sql"
	_ "github.com/mattn/go-sqlite3"
	"log"
	"net/http"
	"text/template"
)

var DbConnection *sql.DB

func indexHandler(w http.ResponseWriter, r *http.Request) {
	t, err := template.ParseFiles("views/index.html")
	if err != nil {
		log.Fatalln(err)
	}
	t.Execute(w, nil)
}
func getPostTweet(w http.ResponseWriter, r *http.Request) {
	DbConnection, _ := sql.Open("sqlite3", "./example.sql")
	defer DbConnection.Close()
	v := r.FormValue("tweet")
	cmd := `INSERT INTO tweets(tweet)VALUES(?)`
	DbConnection.Exec(cmd, v)
	http.Redirect(w, r, "/", http.StatusFound)
}

func main() {
	http.Handle("/resources/", http.StripPrefix("/resources/", http.FileServer(http.Dir("resources/"))))
	http.HandleFunc("/", indexHandler)
	http.HandleFunc("/tweet/", getPostTweet)
	http.ListenAndServe(":8080", nil)
}

ますmain関数内に**http.HandleFunc("/tweet/", getPostTweet)**という関数を記述します。
これは先ほど説明しましたね、これで"/tweet/"というパスが通ればgetPostTweetという関数が呼び出されます。
ではgetPostTweet関数の中身な解説をします。
**DbConnection, _ := sql.Open("sqlite3", "./example.sql")**でデータベースを開いて読み込みます。
**defer DbConnection.Close()**で開いたら閉じましょう。
これもお決まり文という認識で大丈夫だと思います、openしたらdeferでcloseです
ビューから送信したデータはr変数(http.Request)として送られてくるので
**v := r.FormValue("tweet")**としてv変数に代入してあげます。
このFormValueの中の"tweet"とは

 <textarea name="tweet" rows="20" cols="80"></textarea>

このtextareaのname属性の事ですね。
textareaに記述した投稿がtweetという名前で受け取れますよという意味ですね。

今度はv変数に投稿内容が渡せているのでこれをDBに書きこみたいです。なのでデータを書き込むSQL文を記述してあげます。
cmd := INSERT INTO tweets(tweet)VALUES(?)
それがこれですね。 
tweetsテーブルのtweetカラムに値を入れてね、というSQL文です。

**DbConnection.Exec(cmd, v)で実際にデータを書きこむ事が出来ます。
Execの第一引数にSQL文を、そして第二引数に書き込む値を渡している訳です。
そして最後の
http.Redirect(w, r, "/", http.StatusFound)ですが、この関数処理が終わった後に"/"パス(つまり元のページ)に戻ってきてねという関数です。
この処理を書かないとtweet.htmlの画面に飛んでしまうのですが、そんなもの作成していないので真っ白の世界に行っちゃいます。
では実際に投稿フォームから投稿してみたいと思います。
スクリーンショット 2020-01-16 17.10.51.png
今回は Go と投稿してみます、ボタンを押すと...
スクリーンショット 2020-01-16 17.12.04.png
またこのページがリロードされましたね。
では本当にDBへ保存されているのか確認してみます。
ターミナルで
select * from tweets;**と打ち込んでください。これでtweetsテーブルの中身を確認出来ます。
スクリーンショット 2020-01-16 17.13.54.png
1|Go と保存されているのが確認出来ました。
左のidはprimary keyを設定しているので勝手に数字が振られてます。

###次はこのデータを画面に表示してみます
まずはstructを作成します。

package main

import (
	"database/sql"
	_ "github.com/mattn/go-sqlite3"
	"log"
	"net/http"
	"text/template"
)

type Tweets struct {
	Id    int
	Tweet string
}

適当にこの辺りに作成します。
structとは他の言語でいうクラスのようなものです(間違ってたらごめんなさい)
DBから引っ張ってきたデータを一旦入れておく入れ物みたいなイメージですかね!
DBのデータをHTMLに渡したい時は一旦このstructに入れてからじゃないと渡せません。

次にindexHandleを編集します

func indexHandler(w http.ResponseWriter, r *http.Request) {
	DbConnection, _ := sql.Open("sqlite3", "./example.sql")
	defer DbConnection.Close()
	cmd := `SELECT * FROM tweets`
	rows, err := DbConnection.Query(cmd)
	if err != nil {
		log.Fatalln(err)
	}
	defer rows.Close()
	var body []Tweets
	for rows.Next() {
		var b Tweets
		err := rows.Scan(&b.Id, &b.Tweet)
		if err != nil {
			log.Fatalln(err)
		}
		body = append(body, b)
	}
	t, err := template.ParseFiles("views/index.html")
	if err != nil {
		log.Fatalln(err)
	}
	t.Execute(w, body)
}

先ほどと同じくDBをOpenしてdeferでCloseしまして、tweetsテーブルを探すSQL文を記述してcmd変数に入れてやってるだけですね。
投稿の時と違うのは、DbConnection.ExecではなくDbConnection.Queryになっている事です。Queryメソッドでデータを引っ張り出せるんですね。
今回はrows変数にそのデータを代入してあげます。ちなみこれもdeferでcloseしてあげる必要があります。

その後スライスのTweets型のbody変数を定義してます。
rows.Next()でなにをしてるかと言いますと、DBから引っ張って来たrows変数を一つづつ b変数に代入して行って、それをスライスのbodyへ追加してあげてる訳です。(これもお決まり文として覚えればいいです)
これでtweetsテーブルのデータをTweets structへ変換する事が出来ました。
ではこれをHTMLヘ渡します。Executeの第二引数でデータを渡せるんでしたね。

###次にHTMLも編集します

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8" />
  <title>Gopher Tweets</title>
  <link rel="stylesheet" type="text/css"  href="../resources/css/view.css">
</head>
<body>
  <main>
    <header>Gopher Tweets</header>
    <div class="tweet_view">
      {{range .}}
        <form action="/tweet_delete/" method="DELETE">
          <p class="tweet_message">{{.Tweet}}
            <button type="submit" value={{.Id}} name="tweet_delete" class="tweet_delete_button">-</button>
          </p>
        </form>
      {{end}}
    </div>
    <form action="/tweet/" method="POST" class="tweet_form">
      <textarea name="tweet" rows="20" cols="80"></textarea>
      <input type="submit" value="speak">
    </form>
  </main>
</body>
</html>

{{}}←これでデータを受け取る事が出来ます。詳しくはこの人の記事で

ではまた再起動して画面を見てみましょう
スクリーンショット 2020-01-16 17.50.09.png
先ほど投稿した Go という投稿が表示されてますね、今度は Hello と投稿してみます。
スクリーンショット 2020-01-16 17.51.55.png
ちゃんと投稿する事が出来ました。

###次に削除します
今投稿の横に青いボタンがありますが、今は押しても何も変化しません。このボタンを押すと投稿が削除できるようにしてみましょう。

      {{range .}}
        <form action="/tweet_delete/" method="DELETE">
          <p class="tweet_message">{{.Tweet}}
            <button type="submit" value={{.Id}} name="tweet_delete" class="tweet_delete_button">-</button>
          </p>
        </form>
      {{end}}

これが投稿表示部分のhtmlですが、このボタンをおすと 
/tweet_delete/ パスが走り
tweet_deleteというnameで 
{{.Id}} という値が送られる訳ですね。

なので先ほどと同様にこのパスのHandlefuncをmain関数に記述します。



func deleteTweet(w http.ResponseWriter, r *http.Request) {
	DbConnection, _ := sql.Open("sqlite3", "./example.sql")
	defer DbConnection.Close()
	cmd := "DELETE FROM Tweets WHERE id = ?"
	i := r.FormValue("tweet_delete")
	var I int
	I, _ = strconv.Atoi(i)
	DbConnection.Exec(cmd, I)
	http.Redirect(w, r, "/", http.StatusFound)
}
func main() {
	http.Handle("/resources/", http.StripPrefix("/resources/", http.FileServer(http.Dir("resources/"))))
	http.HandleFunc("/", indexHandler)
	http.HandleFunc("/tweet_delete/", deleteTweet)
	http.HandleFunc("/tweet/", getPostTweet)
	http.ListenAndServe(":8080", nil)
}

はい、先ほどの投稿の時とほとんど同じですね、削除するSQL文と削除したいIDを渡して実行しているだけです。
ただ送られて来たIdはstring型で送られてくるのでInt型に変換する為に**strconv.Atoi(i)**を使用しているだけです。
ではまた再起動してから、一番上のGoという投稿のボタンをクリックしてみます。
スクリーンショット 2020-01-16 18.04.24.png
ちゃんと消えました!

このままでは寂しいのでマスコットキャラのゴーファくんを表示してあげて終了です。
resourcesディレクトリ内にimagesディレクトリを作成し、その中に画像を入れてCSSにてbackground-imageで指定するだけですね。

スクリーンショット 2020-01-16 18.11.48.png
以上です。
この記事が僕と同じような初学者の方の為になれば幸いです。

119
94
2

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
119
94