Golangを始めて少し経ったので、WebSocketを使ってリアルタイムタスク管理アプリを作った。
Golang初めて1、2ヶ月ほど経ったので、Asana風なリアルタイムタスク管理アプリを作ってみました。 ちなみにAsanaはこんな感じのやつです。 デザインが好きなので、個人的に使ってます。 asana.com
目的
今回は実際にデプロイして使ってもらうことが目的ではなく、GitHubでコードを公開すること・Golangに慣れること・WebSocketに触れることを目的にしました。
作ったもの
こんな感じのやつを作りました。 ユーザーごとにタスクを追加したり、削除したりできるようになっていて、それがリアルタイムで他のユーザーにも反映されるようになっています。 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(ネゴモ)」を作ってみました!
— TaKO8Ki (@takoyaki3160) April 7, 2019
下のような画像がツイートできます。
Twitterアカウントで利用できるのでぜひ使ってみてください!https://t.co/JRXFe8lHov#Negomo #RT拡散お願いします
Twitterアカウントで利用できるので、もしよかったら使ってみてください!
リリースしたサービス
Negomo(ネゴモ) - もっとTwitter就活しよう!
サービスの概要
「Negomo(リンク)」は、自分が欲しい報酬額をツイートしてTwitter就活できるサービスです。
何か技術を身につけている人が、Twitterで欲しい報酬額(時給・年収)を積極的にアピールすることで、適切な報酬をもらえることを理想としています! もちろん、エンジニア以外の方も大歓迎です!
こんな感じのツイートができます。
京都でWEB系のインターン探してます!! https://t.co/JRXFe8lHov #Negomo #Twitter就活
— TaKO8Ki (@takoyaki3160) April 7, 2019
追記
単位で「兆」を選ぶことができるようになりました!
時給・月収・年収によって画像の色を変えたほうが見やすいというアドバイスをいただいたので、そのように仕様を変更しました! 既に画像を作成されている場合は、金額を変えて編集していただくと色が変わります。
- 時給・月給・年収以外にも秒給・分給を選ぶことができるようになりました!
デザイン
デザインはSpeakerDeckを参考にしました!
トップページ
画像作成ページ
アピールポイントのフォームの高さは可変的になっていて、コンパクトになるように心がけました。また、ポートフォリオなどのURLが入力された場合は、自動的にリンクに変換されるようになっています。
画像表示ページ
作成した画像をクリックすることでツイートできるようになっています。 アピールポイントの欄では、どれがリンクでどれが普通の文字列なのか分かりやすくするようにしました。
使用した技術
- Ruby・Ruby on Rails
- IMGKit(HTMLから画像をレンダリングできるgem)
- google-analytics-rails(google analyticsが簡単に導入できるgem)
activestorage-cloudinary-service(Active StorageとCloudinaryを繋げるgem)
- Cloudinary 画像を保存するストレージです。Herokuのアドオンとして利用できるので非常やりやすかったです。Active Storageと連携させて利用しています。
- Git・GitHub
- Postgresql
- Heroku 今の所Hobbyプランを利用しています。
- build pack 画像を作る部分の実装で利用しました。
- アイコン アイコンは、相変わらずフリー画像にお世話になってます。
苦労した点
今回苦労したのは、画像生成の部分です。 コードとしては下のような感じで、 HTMLから画像をレンダリングするwkhtmltoimageをRailsで利用できるようになる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(リンク)で検索機能を実装する時に初めて分かったことをまとめておきます。
分かった問題
Railsで作ったサービスのあいまい検索の挙動がPostgreSQLとSQLiteで少し違う。(大文字・小文字の区別に関して)
具体的にどう違うのか
改善前
このサービスは、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]}%"))
少し調べたところ下のような記事が出てきました。
② クエリと検索対象をどちらも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万円増額という感じでした。 (今回は、前日にサイバーエージェントのインターンに行っていたので、運よく交通費・宿泊費は支払わずに済みました。)
参加企業
- メルカリ
- DeNA
- Wantedly
- クックパッド
- サイバーエージェント
- pixiv
今までのイベントでお話させていただく機会があった企業もあれば、メルカリやpixivのように初めての企業もありました
内容
学生と企業の人事の一対一もしくは、学生と企業の人事の方とエンジニアの方の一対二で面談を行うイベントです。
イベントに参加できる学生の人数は決められているようで、今回は16人でした。(イベント自体は3日程あったようなので、参加する学生の数としては、48人ということになります。)
服装はもちろん私服で大丈夫です。
面談の流れ
最初の5分間で自分が作ってきたスライドを使って自己紹介します。 その後は、自分が紹介した制作物やどんな技術に興味があるのかについて企業側から質問されたり、こちらから質問したりなど基本的にざっくばらんにお話しするみたいな感じでした。とても話しやすい雰囲気でした。 下の方で、僕が作ったスライドも載せているので、もしよければ参考にしてみてください。
1日の流れ
時間 | 内容 |
---|---|
12:00 | 開始 |
25分 × 6企業 | その後各企業と25分間面談する。(各学生2回ほど休憩がありますが、それ以外はノンストップで進行するので意外と疲れます。) |
19:00 - 20:00 | 懇親会 |
どんな準備をしていったのか?
僕は、プログラミングを始めて八ヶ月ということもあり、まだまだ技術的に足りないところがあるのは目に見えているので、 基本的に、
という二点を自分の推すべきところだと考えてイベントに臨むことにしました。 (技術的に強い方の参考にはならないかもしれません。。。申し訳ないです。)
スライドの見た目
これが僕のスライドです。
濃いめの黄色が好きなので、テーマカラーは黄色にしました。
スライドに載せる文字は基本的に少なめにして、作ったWEBサービスや技術ブログを実際に見せることに時間を使うようにしました。
後は、フリー素材を使ったりだとか、視覚的に捉えやすそうなスライドにするように心がけました。
スライドの構成
スライドは、
- 簡単な自己紹介
- インターン先での業務内容
- 制作物や参加したイベント
- 今後やっていきたいこと
という構成にしました。
この中でも一番のアピールポイントは、制作物や参加したイベントなので、その部分に比較的多く時間を使うようにしました。
自己紹介後の流れ
自己紹介後の企業からの質問
インターン先の業務に対する質問
- 現在長期インターンをしている企業でどんな言語を扱っているのか。
- どんなタスクがどのようにして割り振られるのか。
などを聞かれました。そこまで技術的に深く切り込んだような質問はなかったと思います。
制作物に対する質問
- 一人で作ったのか。
- 開発していて一番苦労した部分はどこか。
- どういう目的で作ったのか。
プログラミングを学ぶ上でのアプローチに関する質問
- プログラミングを学ぶ上で気をつけていることは何か。
- どういう風なプログラマーになりたいか。
- なぜ機械学習を始めたのか。(僕は、最近機械学習を始めたことを自己紹介の時点で言っているので、それに対する質問です。)
企業に対する質問
僕は、技術的な部分というよりは、その企業で成長できるのかなどに興味があるので、
- 企業で働く上で一人で様々なプロダクトに関わることはできるか。(複数プロダクトの掛け持ちや異なるプロダクト間の異動など)
- 副業はできるのか。
- 開発チームの構成はどうなっているのか。
- 職種に関わらずプロダクトの立ち上げなどに関わることができるのか。
- デザイナーなどもコーディングなどを行うことがあるのか。
などについて聞くことにしました。
今までに参加したイベントでもこれらの質問をしたことがあったのですが、違う社員の方に質問をすることで、また違った情報を知ることができるので今回も質問してみて非常によかったと思います。
企業からのフィードバック
やはり、
- プログラミングを初めて一年も満たないのに、アウトプット量が多いことに驚いた。
と言っていただくことが多かったです。
この点に関しては、思っていた通りになったので作戦通りです。
中には、
- スライドが見やすかった。
- 聞きやすい話し方だった。
というフィードバックもありました。
振り返ってみて思うこと
イベントの感想
実際に企業の方にスライドを使って自分をプレゼンしてみて思ったことは、
- 一年未満のプログラミング歴。
- それなりにプロダクトを作っていて、リリースもしている。
- 作ったプロダクトに対して分析をして、それを運営につなげている。
- プログラミング欲がすごい。
- 今後の展望がはっきりとしている。
みたいな条件が揃っているとそれだけで企業の方にアピールするポイントになるということです。中でも、サービスをしっかりとリリースしている点が非常に重要になると思います。
僕の場合、それに加えて、作ったサービスが運よく、Qiitaで少し評価されていたという点もアピールポイントになりました。(Qiitaに書いたサービス紹介記事が、187いいねで、一時期トレンド4位くらいになってた。下にリンク貼っておきます。)
また、僕は企業に対して聞きたいことが山ほどあったので、こちらからの質問が多くなった印象です。
今後どうするべきか
正直、「プログラミング歴短いのにアウトプット量すごい」みたいなギャップは、頑張れば誰でも実現することができると思っています。今回のイベントでは、プログラミングに対する「意志・意欲」を評価していただいたので、今後はこの部分を「技術力」にシフトしていけるような過ごし方をしていこうと思います。
Railsのgem作ってみたり、Sinatraやったり、Golangやったり、自然言語処理やったり、アルゴリズムそのものを勉強したり色々やりたいことがあるので、うまく時間を使ってやっていこうと思います。
今後読んでいきたい書籍
Pattern Recognition and Machine Learning (Information Science and Statistics)
The Elements of Computing Systems: Building a Modern Computer from First Principles