とある神戸大生の走り書き

主にプログラミングに関して、学んだことを走り書きとして残していきます。

Golangを始めて少し経ったので、WebSocketを使ってリアルタイムタスク管理アプリを作った。

Golang初めて1、2ヶ月ほど経ったので、Asana風なリアルタイムタスク管理アプリを作ってみました。 ちなみにAsanaはこんな感じのやつです。 デザインが好きなので、個人的に使ってます。 asana.com

目的

今回は実際にデプロイして使ってもらうことが目的ではなく、GitHubでコードを公開すること・Golangに慣れること・WebSocketに触れることを目的にしました。

作ったもの

Image from Gyazo

github.com

こんな感じのやつを作りました。 ユーザーごとにタスクを追加したり、削除したりできるようになっていて、それがリアルタイムで他のユーザーにも反映されるようになっています。 DBには接続していないので、CSSのクラス名でそれぞれのタスクを管理しています。

具体的には、作成した順番にタスクにCSSのクラス名としてIDが割り振られるようになっています。

例:

  • 最初に作ったタスクは、クラス名task1。
  • 二つ目に作ったタスクは、クラス名task2。
  • 次にtask1を削除して、三つ目のタスクを作るとクラス名がtask3になる。

デザインはRESUMEの感じが好きなので、それを参考にしています。

Golangで書いたコード

クリックしてコードを見ることができます。

handler.go

package main

import (
    "html/template"
    "net/http"
    "path/filepath"
    "sync"
)

/*
HTMLテンプレートをサーブするためのハンドラ
*/
type templateHandler struct {
    once     sync.Once          //HTMLテンプレートを1度だけコンパイルするための指定
    filename string             //テンプレートとしてHTMLファイル名を指定
    tmpl     *template.Template //テンプレート
}

/* templateHandlerをhttp.Handleに適合させるため、ServeHttpを実装する */
func (t *templateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {

    // テンプレートディレクトリを指定する
    path, err := filepath.Abs("./templates/")
    if err != nil {
        panic(err)
    }

    // 指定された名称のテンプレートファイルを一度だけコンパイルする
    t.once.Do(
        func() {
            t.tmpl = template.Must(template.ParseFiles(path + t.filename))
        })

    t.tmpl.Execute(w, nil)
}

main.go

package main

import (
    "log"
    "net/http"
)

func main() {

    /* ルートへのアクセスに対してハンドラを貼り、group.htmlをサーブする */
    http.Handle("/", &templateHandler{filename: "/group.html"})

    group := newRoom()

    http.Handle("/room", group)

    go group.run()

    /* webサーバを開始する */
    log.Println("webサーバを開始します。")
    if err := http.ListenAndServe(":8080", nil); err != nil {
        log.Fatalln("webサーバの起動に失敗しました。:", err)
    }
}

task.go

package main

type task struct {
    Name   string
    Detail string
    Delete string
}

group.go

package main

import (
    "fmt"
    "log"
    "net/http"

    "github.com/gorilla/websocket"
)

const (
    socketBufferSize = 1024
    taskBufferSize   = 256
)

/* websocket用の変数 */
var upgrader = &websocket.Upgrader{
    ReadBufferSize:  socketBufferSize,
    WriteBufferSize: socketBufferSize,
}

type group struct {
    forward chan *task
    join    chan *client
    leave   chan *client
    clients map[*client]bool
}

/*
groupをhttp.handleに適合させる。
ここでは以下のことを実装する。
   ・websocketの開設
   ・clientの生成
*/
func (c *group) ServeHTTP(w http.ResponseWriter, r *http.Request) {

    /* websocketの開設 */
    socket, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Fatalln("websocketの開設に失敗しました。:", err)
    }

    /* クライアントの生成 */
    client := &client{
        socket: socket,
        send:   make(chan *task, taskBufferSize),
        room:   c,
    }

    c.join <- client
    defer func() {
        c.leave <- client
    }()

    go client.write()
    client.read()
}

func newRoom() *group {
    fmt.Println("groupが生成されました。:", t.Format(layout))
    return &group{
        forward: make(chan *task),
        join:    make(chan *client),
        leave:   make(chan *client),
        clients: make(map[*client]bool),
    }
}

func (c *group) run() {
    for {
        // チャネルの動きを監視し、処理を決定する
        select {

        /* joinチャネルに動きがあった場合(クライアントの入室) */
        case client := <-c.join:
            // クライアントmapのbool値を真にする
            c.clients[client] = true
            fmt.Printf("クライアントが入室しました。現在 %x 人のクライアントが存在します。\n",
                len(c.clients))

        /* leaveチャネルに動きがあった場合(クライアントの退室) */
        case client := <-c.leave:
            // クライアントmapから対象クライアントを削除する
            delete(c.clients, client)
            fmt.Printf("クライアントが退室しました。現在 %x 人のクライアントが存在します。\n",
                len(c.clients))

        /* forwardチャネルに動きがあった場合(タスクの受信) */
        case msg := <-c.forward:
            fmt.Println("タスクを受信しました。")
            // 存在するクライアント全てに対してタスクを送信する
            for target := range c.clients {
                select {
                case target.send <- msg:
                    fmt.Println("タスクの送信に成功しました。")
                default:
                    fmt.Println("タスクの送信に失敗しました。")
                    delete(c.clients, target)
                }
            }
        }
    }
}

大まかには以上です。

READMEに実行の仕方が書いてあるので、もし興味があれば見てみてください。

参考

[ golang ] WebSocketを使ったチャット機能を実装してみる。 | Wild Data Chase -データを巡る冒険-

誰でも新卒年収800万宣言できる、Twitter就活サービス「Negomo(ネゴモ)」をリリースした。

Twitter就活の促進」をテーマにしたサービスNegomo(ネゴモ)をリリースしました!

Twitterアカウントで利用できるので、もしよかったら使ってみてください!

リリースしたサービス

Negomo(ネゴモ) - もっとTwitter就活しよう!

サービスの概要

「Negomo(リンク)」は、自分が欲しい報酬額をツイートしてTwitter就活できるサービスです。

何か技術を身につけている人が、Twitterで欲しい報酬額(時給・年収)を積極的にアピールすることで、適切な報酬をもらえることを理想としています! もちろん、エンジニア以外の方も大歓迎です!

こんな感じのツイートができます。

参考:Negomo(ネゴモ)とは

追記

  • 単位で「兆」を選ぶことができるようになりました!

  • 時給・月収・年収によって画像の色を変えたほうが見やすいというアドバイスをいただいたので、そのように仕様を変更しました! 既に画像を作成されている場合は、金額を変えて編集していただくと色が変わります。

スクリーンショット 2019-04-09 15.46.36.png

  • 時給・月給・年収以外にも秒給・分給を選ぶことができるようになりました!

デザイン

デザインはSpeakerDeckを参考にしました!

トップページ

スクリーンショット 2019-04-08 0.45.24.png

画像作成ページ

Image from Gyazo

アピールポイントのフォームの高さは可変的になっていて、コンパクトになるように心がけました。また、ポートフォリオなどのURLが入力された場合は、自動的にリンクに変換されるようになっています。

画像表示ページ

スクリーンショット 2019-04-08 0.56.29.png

作成した画像をクリックすることでツイートできるようになっています。 アピールポイントの欄では、どれがリンクでどれが普通の文字列なのか分かりやすくするようにしました。

使用した技術

苦労した点

今回苦労したのは、画像生成の部分です。 コードとしては下のような感じで、 HTMLから画像をレンダリングするwkhtmltoimageRailsで利用できるようになるgem、IMGKitを利用しています。 ローカルでは比較的簡単に動かすことができましたが、Heroku上で動かすのに少し時間がかかりました。 最終的に、build packを使うのが一番良さそうだと分かったので、元からあったbuild packのレポジトリをforkして少し自分用にカスタマイズして使いました。

wkhtmltoimageをHerokuで使うためのbuild packカスタマイズ版

画像作成部分のコード

def create_image
  Tempfile.create(["#{@profile.id}", '.png'], :encoding => 'ascii-8bit') do | file |
    file.write(IMGKit.new(get_html, quality: 20, width: 800).to_png)
    file.rewind
    @profile.image.attach(io: file, filename: "q_#{@profile.id}.png", content_type: "image/png")
  end
end

def get_html
  <<~HTML
  <!DOCTYPE html>
  <html>
    <head>
      <link href="http://fonts.googleapis.com/earlyaccess/notosansjp.css">
      <meta charset="UTF-8">
      <style>
        @charset "UTF-8";
        html {
          font-family: sans-serif;
          -ms-text-size-adjust: 100%;
          -webkit-text-size-adjust: 100%;
        }
        body {
          width: 800px;
          margin: 0;
          background: #eee;
          font-family: 'Noto Sans JP', sans-serif;
        }
        .q-frame {
          width: 100%;
          background-color: #fff;
          padding: 25px 25px 10px 25px;
        }
        .q-frame .q-body {
            vertical-align: middle;
            text-align: center;
            height: 220px;
            width: 750px;
            font-size: 2.6em;
            background-color: white;
            padding: 1.3em;
            border-radius: 3px;
            display: table-cell;
            vertical-align: middle;
            background-image: url(#{assets_image_url('negomo_background.png')});
            background-size: 100% 100%;
            background-repeat: no-repeat;
            background-position: 50%;
            box-shadow: 0.5px 3.5px 10px 0px #ccc;
        }
        .q-frame .q-icon {
          font-size: 2.2em;
          padding: 5px 0px 0px 0px;
          margin: 0px;
          color: #333;
          border-radius: 5px;
        }
        .q-frame .q-icon img {
          width: 150px;
          margin-top: 7px;
        }
        span.word {
          font-size: 75px;
          font-weight: bold;
          color: #7d7458;
        }
        span.description {
          font-size: 30px;
          font-weight: 100;
          color: #545454;
          line-height: 45px;
          display: block;
          margin-top: 10px;
          font-weight: bold;
          width: 650px;
          overflow-wrap: break-word;
          word-wrap: break-word;
          margin: 0 auto;
        }
      </style>
    </head>
    <body>
      <div class="q-frame">
        <div class="q-body">
          <span class="word">#{@profile.adjust_money_style}</span><br>
          <span class="description">#{@profile.title}</span>
        </div>
        #{}
        <div class="q-icon">
          #{assets_image_tag('negomo.png').html_safe}
        </div>
      </div>
    </body>
  </html>

  HTML
end

リリースする上で心がけたこと

イデアを形にするスピード

少しずつWEBサービスを作ることにも慣れてきたので、自分が思いついたアイデアを形にするスピードを意識するようにしました。それでも、このサービスを作るのに1週間ほど費やしてしまったので、まだまだだなと感じます。

ユーザーにとって分かりやすいサービスかどうか

今までも「ユーザーにとって分かりやすいサービス作り」を意識するようにはしてきましたが、イマイチ分かりやすさを追求できていないように感じていました。特に、非エンジニアの人に使ってもらうとなると非常に難しいと感じます。 そのため、今回は、大まかに言えば「画像をツイートできるだけのサービス」というシンプルな設計にしました。

最後に

タイトルに「誰でも新卒年収800万宣言できる」と書きましたが、最近、コミさんという技術的に強い方が新卒年収800万円芸なるものを行なって、話題になっているのを1フォロワーとして眺めていました。エンジニアだけでなく、他の分野にもコミさんのように優れた人材はたくさん居られると思うので、その方達がTwitterなどを利用して、しっかりといい待遇を受けられるようになればいいなと思います。 僕もその方達に加われるように頑張っていきます! また、このサービスをきっかけに、もっとTwitter就活が当たり前になれば嬉しいです。

ここまで読んでいただきありがとうございました!

もしよろしければ、いいねボタンを押していただけたら嬉しいです!

Negomo、ぜひ使ってみてください!!

Negomo(ネゴモ) - もっとTwitter就活しよう!

参考

Railsで検索機能を実装する時に、PostgreSQLとSQLiteでは、Active Recordが違う挙動を取ることを初めて知った。

以前リリースしたサービスLinch(リンク)で検索機能を実装する時に初めて分かったことをまとめておきます。

tako8ki.hatenablog.com

分かった問題

Railsで作ったサービスのあいまい検索の挙動がPostgreSQLSQLiteで少し違う。(大文字・小文字の区別に関して)

具体的にどう違うのか

改善前

このサービスは、Herokuでデプロイしているので、DBはPostgreSQLを利用しています。 それに対して、ローカル環境では、SQLiteを利用するようになっています。 ここのところの挙動の違いなどを全く意識せずにコードを書いていたので、 検索機能のところのコードが下記のようになっていました。

@articles = Article.where("title like '%" + params[:q] + "%'")

検索ボックスに入力されたクエリをパラメーターとして持たせて検索するというごく普通なコードです。

ローカル環境(つまり、SQLite上)では、このコードでも大文字・小文字の区別なく検索してくれます。 しかし、本番環境(PostgreSQL上)では、大文字・小文字の区別がされてしまっていました。

例)「Python」で検索した時に、ローカルでは、「Python入門」「pythonスクレイピング」が検索結果に引っ掛かる> が、本番では、「Python入門」しか引っかからない。

解決策

解決策としては、挙げられたものは下記の二つです。

① Arel使う

@articles = Article.where(articles[:title].matches("%#{params[:q]}%"))

少し調べたところ下のような記事が出てきました。

qiita.com

② クエリと検索対象をどちらもdowncaseする

@articles = Article.where("lower(title) like ?", "%#{params[:q].downcase}%")

こっちの方がなんとなくActive Recordらしさがあるような気がします。

最終的に

1の方の記事を読んだ限りでは、バージョンアップによって動かなくなる可能性があるとのことだったので、 最終的には、2の方を採用しました。

まとめ

普段何の気なしに触っていたActive Recordが、使うDBによって違う挙動を示すことに気づけたことは非常に良かったと思います。また、Active Recordではなく、生のSQLと格闘する必要もありそうなので、そこらへんも勉強していこうと思います。

プライバシーポリシー

当サイトに掲載されている広告について

当サイトは第三者配信の広告サービス「Google Adsense グーグルアドセンス」を利用しています。

広告配信事業者は、ユーザーの興味に応じた広告を表示するためにCookie(クッキー)を使用することがあります。

Cookie(クッキー)を無効にする設定およびGoogleアドセンスに関する詳細は「広告 – ポリシーと規約 – Google」をご覧ください。

サポーターズ1on1面談イベントに行ってみたら、すごく楽しかった話

今回は、2019/03/02に渋谷で開催された2021年卒の学生対象のサポーターズ1on1面談イベントに行ってきたので、振り返ってみようと思います。

イベント概要

交通費

地域によって固定額が支給されます。僕の場合は、関西に住んでいるので、基本的に3万円支給で、アンケートに回答したら1万円増額という感じでした。 (今回は、前日にサイバーエージェントインターンに行っていたので、運よく交通費・宿泊費は支払わずに済みました。)

tako8ki.hatenablog.com

参加企業

今までのイベントでお話させていただく機会があった企業もあれば、メルカリやpixivのように初めての企業もありました

内容

学生と企業の人事の一対一もしくは、学生と企業の人事の方とエンジニアの方の一対二で面談を行うイベントです。 イベントに参加できる学生の人数は決められているようで、今回は16人でした。(イベント自体は3日程あったようなので、参加する学生の数としては、48人ということになります。)
服装はもちろん私服で大丈夫です。

面談の流れ

最初の5分間で自分が作ってきたスライドを使って自己紹介します。 その後は、自分が紹介した制作物やどんな技術に興味があるのかについて企業側から質問されたり、こちらから質問したりなど基本的にざっくばらんにお話しするみたいな感じでした。とても話しやすい雰囲気でした。 下の方で、僕が作ったスライドも載せているので、もしよければ参考にしてみてください。

1日の流れ

時間 内容
12:00 開始
25分 × 6企業 その後各企業と25分間面談する。(各学生2回ほど休憩がありますが、それ以外はノンストップで進行するので意外と疲れます。)
19:00 - 20:00 懇親会

どんな準備をしていったのか?

僕は、プログラミングを始めて八ヶ月ということもあり、まだまだ技術的に足りないところがあるのは目に見えているので、 基本的に、

  1. プログラミング歴がまだ浅いのにアウトプット量が多い(作ったWEBサービスや技術ブログ)
  2. 自分が作ったWEBサービスに対する分析をしていて、それを運営に繋げている

という二点を自分の推すべきところだと考えてイベントに臨むことにしました。 (技術的に強い方の参考にはならないかもしれません。。。申し訳ないです。)

スライドの見た目

これが僕のスライドです。
濃いめの黄色が好きなので、テーマカラーは黄色にしました。
スライドに載せる文字は基本的に少なめにして、作ったWEBサービスや技術ブログを実際に見せることに時間を使うようにしました。 後は、フリー素材を使ったりだとか、視覚的に捉えやすそうなスライドにするように心がけました。

スライドの構成

スライドは、

  1. 簡単な自己紹介 
  2. インターン先での業務内容
  3. 制作物や参加したイベント
  4. 今後やっていきたいこと

という構成にしました。
この中でも一番のアピールポイントは、制作物や参加したイベントなので、その部分に比較的多く時間を使うようにしました。

自己紹介後の流れ

自己紹介後の企業からの質問

インターン先の業務に対する質問
  • 現在長期インターンをしている企業でどんな言語を扱っているのか。
  • どんなタスクがどのようにして割り振られるのか。

などを聞かれました。そこまで技術的に深く切り込んだような質問はなかったと思います。

制作物に対する質問
  • 一人で作ったのか。
  • 開発していて一番苦労した部分はどこか。
  • どういう目的で作ったのか。
プログラミングを学ぶ上でのアプローチに関する質問
  • プログラミングを学ぶ上で気をつけていることは何か。
  • どういう風なプログラマーになりたいか。
  • なぜ機械学習を始めたのか。(僕は、最近機械学習を始めたことを自己紹介の時点で言っているので、それに対する質問です。)

企業に対する質問

僕は、技術的な部分というよりは、その企業で成長できるのかなどに興味があるので、

  • 企業で働く上で一人で様々なプロダクトに関わることはできるか。(複数プロダクトの掛け持ちや異なるプロダクト間の異動など)
  • 副業はできるのか。
  • 開発チームの構成はどうなっているのか。
  • 職種に関わらずプロダクトの立ち上げなどに関わることができるのか。
  • デザイナーなどもコーディングなどを行うことがあるのか。

などについて聞くことにしました。
今までに参加したイベントでもこれらの質問をしたことがあったのですが、違う社員の方に質問をすることで、また違った情報を知ることができるので今回も質問してみて非常によかったと思います。

企業からのフィードバック

やはり、

  • プログラミングを初めて一年も満たないのに、アウトプット量が多いことに驚いた。

と言っていただくことが多かったです。 この点に関しては、思っていた通りになったので作戦通りです。
中には、

  • スライドが見やすかった。
  • 聞きやすい話し方だった。

というフィードバックもありました。

振り返ってみて思うこと

イベントの感想

実際に企業の方にスライドを使って自分をプレゼンしてみて思ったことは、

  • 一年未満のプログラミング歴。
  • それなりにプロダクトを作っていて、リリースもしている。
  • 作ったプロダクトに対して分析をして、それを運営につなげている。
  • プログラミング欲がすごい。
  • 今後の展望がはっきりとしている。

みたいな条件が揃っているとそれだけで企業の方にアピールするポイントになるということです。中でも、サービスをしっかりとリリースしている点が非常に重要になると思います。
僕の場合、それに加えて、作ったサービスが運よく、Qiitaで少し評価されていたという点もアピールポイントになりました。(Qiitaに書いたサービス紹介記事が、187いいねで、一時期トレンド4位くらいになってた。下にリンク貼っておきます。)

qiita.com

linch.link

また、僕は企業に対して聞きたいことが山ほどあったので、こちらからの質問が多くなった印象です。

今後どうするべきか

正直、「プログラミング歴短いのにアウトプット量すごい」みたいなギャップは、頑張れば誰でも実現することができると思っています。今回のイベントでは、プログラミングに対する「意志・意欲」を評価していただいたので、今後はこの部分を「技術力」にシフトしていけるような過ごし方をしていこうと思います。
Railsのgem作ってみたり、Sinatraやったり、Golangやったり、自然言語処理やったり、アルゴリズムそのものを勉強したり色々やりたいことがあるので、うまく時間を使ってやっていこうと思います。

今後読んでいきたい書籍