20210426のGoに関する記事は4件です。

go の write once, run anywhere ではない部分

これは何? Go は Java ほど write once, run anywhere ではないことは知っているけど、どこで実装依存になるのかを知らなかったので調べてみた。 まあ unsafe なやつは含めなかった。 情報源はすべて The Go Programming Language Specification - The Go Programming Language である。 int, uint, uintptr のサイズ まあこれは有名だよね。 C/C++ と違って、int, uint のサイズは 32 または 64 ビット。 16 ビットだったり 40ビットだったりする可能性はない。 一方、uintptr は「ポインタを格納できるサイズ」としか決まっていない。文章を見る限り、40ビットとかの可能性を排除していないように見える。 浮動小数点数の周辺 浮動小数点数から他の数値型への変換 In all non-constant conversions involving floating-point or complex values, if the result type cannot represent the value the conversion succeeds but the result value is implementation-dependent. とあるので、2の100乗などの float64, float32 を整数に変換した場合などの動作が実装依存。 ゼロ除算の振る舞い The result of a floating-point or complex division by zero is not specified beyond the IEEE-754 standard; whether a run-time panic occurs is implementation-specific. 浮動小数点数のゼロ除算で panic になるのかそれ以外のなにか(たぶん、非数)になるのかは実装依存。 非数になっても大丈夫なように対応していたら、特定のアーキテクチャでまれに panic が起こり、手元の環境では再現しないなぁ、みたいな事がありえる模様。 サイズヒントつきで map を作る場合 Calling make with a map type and size hint n will create a map with initial space to hold n map elements. The precise behavior is implementation-dependent. これは、直前の例 m := make(map[string]int, 100) // map with initial space for approximately 100 elements のような、サイズヒントつき map の話。サイズヒントを書くことはできるけど、そのサイズヒントが実際どんな効果をもたらすのかは実装依存ということだと思う。この文を見る限り、サイズヒントを無視する実装があっても許されそうに見える。 import のパス The interpretation of the ImportPath is implementation-dependent but it is typically a substring of the full file name of the compiled package and may be relative to a repository of installed packages. とある。この文の ImportPath は、 import "fmt" の "fmt" の部分。 まあこれは処理系のファイルシステムに依存せざるを得ないので仕方ない。 Windows だと go? import "net\\http\\httputil" とか書けたりするのかなぁ(試してない)。 print の出す文字列 こんな print prints all arguments; formatting of arguments is implementation-specific ことが書いてある。ここでいう print は、fmt.Print のことではなく go がちゃんと起動するまでに使われる関数のことらしい。普通のユーザは関係ない。 FMA とか適当に使うので計算結果が実装依存 some architectures provide a "fused multiply and add" (FMA) instruction that computes x*y + z without rounding the intermediate result x*y. These examples show when a Go implementation can use that instruction: とある。FMA が使えるなら x*y+z を一気にやってから丸めるし、使えないなら x*y をやって、それを丸めてから z を足して、もう一回丸める。 なので、丸め誤差の出方が変わるかもね、ということらしい。 数値計算の文脈だとわりと痛そう。 調べてないこと ライブラリは調べてない。色々あると思う。time.Sleep で寝られる時間とか明らかに実行環境依存。 chan や goルーチンのタイミングとかも色々あると思う。 まとめ ゼロ除算の振る舞いが実装依存なのはちょっとびっくりした。わりと嫌な罠として機能しそう。 範囲外の値から整数への変換結果が実装依存なのもびっくりした。 C++ みたいな速度に全振りの言語じゃないんだから、そこは振る舞いを揃えたほうが良かったんじゃないの、と思った。 FMA は、まあそうかなとも思うけど、それは困るという人もたくさんいるよね。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

gooseパッケージの取得に失敗する(goose not found)

事象 gooseパッケージを以下のように取得しようとすると失敗してしまう $ go get -u -v bitbucket.org/liamstask/goose/cmd/goose $ which goose goose not found 原因 GOPATHを設定していなかった 対応 以下のようにGOPATHを通す $ echo 'export GOPATH=$HOME/go' >> ~/.zshrc $ echo 'export PATH=$PATH:$GOPATH/bin' >> ~/.zshrc $ source ~/.zshrc // 設定を保存する 再度、go getしてみる。そしてPATHが通っているか確認してみる。 $ go get -u -v bitbucket.org/liamstask/goose/cmd/goose $ which goose /Users/[userName]/go/bin/goose go get でインストールしたツールの実行ファイルは /Users/ユーザ名/go/bin 配下に配置されます。 無事にgooseがあることを確認できました!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go RestAPI のあれこれ ついでにDockerも

今回できたこと Goを使用してREST APIを作成した。 その際,Dockerを使用し,フレームワークには軽量なechoを使用した。 また,SSL化も行い,Let`s Encriptを使用した。 最初に知っておきたいコマンド (全削除) (コンテナ)$ docker rm -f `docker ps -a -q` (イメージ)$ docker rmi $(docker images -q) -f (build) $ docker-compose up -d --build (コンテナ起動) $ docker-compose exec golang bash (動いているdocker一覧表示) $ docker-compose ps (作成した全てのdocker一覧表示) $ docker-compose ps -a (コンテナ内に入る) $ docker-compose exec golang bash $ docker-compose exec mysql bash (MySQL接続) root@コンテナ番号:/# mysql -u root -p -h 127.0.0.1 mysql> show databases; mysql> use golang; mysql> show tables; mysql> SELECT * FROM `users`; 全容 初期設定 docker, docker-compose インストール済み Docker のインストール手順 Docker Compose のインストール フォルダは以下を作る └─プロジェクト名 ├─server │ ├─docker │ │ ├─golang │ │ │ └─Dockerfile │ │ ├─mysql │ │ │ ├─Dockerfile │ │ │ └─db │ │ │ └─init.sql │ │ ├─src │ │ │ ├─crt <-Dockerで自動生成(証明書が入る予定) │ │ │ ├─databases │ │ │ │ └─main.go │ │ │ ├─routing │ │ │ │ └─main.go │ │ │ ├─go.mod <-自動生成 │ │ │ ├─go.sum <-自動生成 │ │ │ └─main.go │ │ ├─docker-compose.yml │ │ └─api_v1.yaml <-必要に応じて │ └─Server.md <-必要に応じて └─README.md <-必要に応じて 証明書作成 無料の認証局を利用する Let`s Encrypt certbot 証明書が生成されているか確認する $ ls /etc/letsencrypt/live/<自分のドメイン名> >> README cert.pem chain.pem fullchain.pem privkey.pem プロジェクトを書き込み DockerComposeの設定 docker-compose.yml version: "3" services: golang: build: ./golang/ image: image_golang container_name: container_api volumes: - ./src:/go/src # 証明書を/go/src/crtへ - /etc/letsencrypt:/go/src/crt ports: - "443:443" tty: true mysql: # platform: linux/x86_64 #AppleSiliconを使用時,必要 build: ./mysql/ image: image_mysql container_name: container_db volumes: - ./mysql/db:/docker-entrypoint-initdb.d #初期データをマウントする場所 environment: # コンテナ内のMySQLを起動する際のパスワードを設定 - MYSQL_DATABASE=golang # 作成するデータベース名 - MYSQL_USER=akidon # 作業ユーザー名(任意の値) - MYSQL_PASSWORD=12345 # 作業ユーザーのパスワード(任意の値) - MYSQL_ROOT_PASSWORD=root # rootユーザーのパスワード(デフォルトでは「root」) ports: - "3306:3306" tty: true 説明 versionは3系が最新版で、versionによって書き方が異なる build: Dockerfileが存在する場所 image: イメージの名前 container_name: コンテナの名前 volumes: 「./a:/b」 aはホスト側のフォルダを bのdockerコンテナ側へ ports: ポート environment: 環境変数 tty: true コンテナ起動永続化 links: 他のコンテナの接続? AppleSiliconを使用している場合は17行目のリマークを外す DockerFileの設定 golang/Dockerfile. # golang/Dockerfile FROM golang:1.14 WORKDIR /go/src mysql/Dockerfile. # mysql/Dockerfile FROM mysql MySQLの初期設定 init.sql -- MySQL dump 10.13 Distrib 5.1.51, for pc-linux-gnu (i686) -- -- Host: 127.0.0.1 Database: world -- ------------------------------------------------------ -- Server version 5.1.51-debug-log /*以下Databaseの内容が記載される*/ DROP SCHEMA IF EXISTS golang; CREATE SCHEMA golang; USE golang; DROP TABLE IF EXISTS golang; CREATE TABLE users ( id INT AUTO_INCREMENT NOT NULL PRIMARY KEY, name VARCHAR(40), age INT(10), created_at TIMESTAMP DEFAULT NULL, updated_at TIMESTAMP DEFAULT NULL, deleted_at TIMESTAMP DEFAULT NULL ); INSERT INTO users (id, name, age) VALUES (1, "testKun", 21); 説明 created_at updated_at deleted_at はGormを使用する際,追加,更新,削除で必要になる要素 Goの設定 databases/main.go package databases import ( "fmt" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql" ) // SQLConnect DB接続 func GormConnect() (database *gorm.DB) { DBMS := "mysql" // MySQL PROTOCOL := "tcp(container_db)" // MySQLコンテナ名 DBNAME := "golang" // テーブル名 USER := "akidon" // MySQLユーザー名 PASS := "12345" // パスワード CONNECT := USER + ":" + PASS + "@" +PROTOCOL + "/" + DBNAME + "?charset=utf8&parseTime=True&loc=Local" // 修正!! db, err := gorm.Open(DBMS, CONNECT) if err != nil { panic(err.Error()) } else { fmt.Println("DB接続成功") } return db } 説明 PROTOCOL := "tcp(container_db)" // MySQLコンテナ名 のcontainer_dbはDockerComposeのcontainer_name:によるもの 間違っていれば接続できない routing/main.go package routing import ( "fmt" "github.com/labstack/echo" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql" "src/databases" ) type User struct { gorm.Model Name string `json:"name"` Age int `json:"age"` } func (u User) String() string { return fmt.Sprintf("Name:%s \n Age:%d \n ", u.Name, u.Age) } // ユーザー情報取得 func BaseAPI_GET() echo.HandlerFunc { return func(c echo.Context) error { db := databases.GormConnect() defer db.Close() text1 := search(db) return c.JSON(200, text1) } } // ユーザーを登録,更新 func BaseAPI_POST() echo.HandlerFunc { return func(c echo.Context) error { db := databases.GormConnect() defer db.Close() //追加・更新 user := new(User) if err := c.Bind(user); err != nil { return err } user1 := User{ Name: user.Name, Age: user.Age, } insertUsers := []User{user1} insert(insertUsers, db) // update(user1, db) return c.JSON(200, "追加完了") } } func insert(users []User, db *gorm.DB) { for _, user := range users { db.NewRecord(user) db.Create(&user) } } func update(users User, db *gorm.DB) { var user User db.Model(&user).Where("id = ?", 1).Update(map[string]interface{}{"name": users.Name, "age": users.Age}) } func search(db *gorm.DB) []User { var user []User // 同じ位置にいるリストを取得 db.Raw("SELECT * FROM users").Scan(&user) return user } 説明 routing/main.go type User struct { gorm.Model Name string `json:"name"` Age int `json:"age"` } User(単数形)はMySQLのテーブル名「Users(複数形)」単数名にする必要がある Name,Ageは先頭大文字である必要がある。 json:"name"はリクエストボディに関連させる名前。 main.go package main import ( "github.com/labstack/echo" "src/routing" ) func main() { e := echo.New() // routing e.GET("/user",routing.BaseAPI_GET()) e.POST("/user",routing.BaseAPI_POST()) e.Logger.Fatal(e.StartTLS(":443", "/crt/live/<あなたのドメイン名>/fullchain.pem", "/crt/live/<あなたのドメイン名>/privkey.pem")) } HTTPでよいなら docker-compose.yml version: "3" services: golang: build: ./golang/ image: image_golang container_name: container_api volumes: - ./src:/go/src # 証明書を/go/src/crtへ # - /etc/letsencrypt:/go/src/crt <- 削除 ports: # - "443:443" <- 削除 - "8080:8080" <- 追加 tty: true mysql: # platform: linux/x86_64 #AppleSiliconを使用時,必要 build: ./mysql/ image: image_mysql container_name: container_db volumes: - ./mysql/db:/docker-entrypoint-initdb.d #初期データをマウントする場所 environment: # コンテナ内のMySQLを起動する際のパスワードを設定 - MYSQL_DATABASE=golang # 作成するデータベース名 - MYSQL_USER=akidon # 作業ユーザー名(任意の値) - MYSQL_PASSWORD=12345 # 作業ユーザーのパスワード(任意の値) - MYSQL_ROOT_PASSWORD=root # rootユーザーのパスワード(デフォルトでは「root」) ports: - "3306:3306" tty: true main.go // e.Logger.Fatal(e.StartTLS(":443", "/crt/live/<あなたのドメイン名>/fullchain.pem", "/crt/live/<あなたのドメイン名>/privkey.pem")) e.Logger.Fatal(e.Start(":8080")) // <- 追加 オレオレ証明書では crtに ・ myself.csr ・ myself.key ・ myself.crt がある場合 main.go // e.Logger.Fatal(e.StartTLS(":443", "./crt/live/<あなたのドメイン名>/fullchain.pem", "./crt/live/<あなたのドメイン名>/privkey.pem")) e.Logger.Fatal(e.StartTLS(":443", "crt/myself.crt", "crt/myself.key")) // <- 追加 動かす $ cd ../プロジェクト名/server/docker (再ビルドしても変更されない場合はキャッシュが残っている可能性がある為,全削除) (注意) $ docker rm -f `docker ps -a -q` (ビルドする) $ docker compose up -d --build (コンテナ内に入る)**1分ぐらい待つ(MySQLの初期設定の完了に時間がかかる)** $ docker-compose exec mysql bash root@コンテナ番号:# exit $ docker-compose exec golang bash (go mod init フォルダ名) root@コンテナ番号:/go/src# go mod init src root@コンテナ番号:/go/src# go run main.go MySQLの中を覗く % docker-compose exec mysql bash root@コンテナ番号:/# mysql -u root -p -h 127.0.0.1 (パスワードの入力を求められるので「root」を入力) -> root (現在存在するアカウントの確認) (hostに「%」がついてるか確認) mysql> select host,user from mysql.user; +-----------+------------------+ | host | user | +-----------+------------------+ | % | akidon | | % | root | | localhost | mysql.infoschema | | localhost | mysql.session | | localhost | mysql.sys | | localhost | root | +-----------+------------------+ (ついていない場合は以下コードで追加できたはず) mysql> grant all privileges on golang.* to 'akidon'@'%'; mysql> show databases; +--------------------+ | Database | +--------------------+ | golang | <- 作成できてるか確認 | information_schema | | mysql | | performance_schema | | sys | +--------------------+ 5 rows in set (0.06 sec) mysql> use golang; mysql> show tables; +------------------+ | Tables_in_golang | +------------------+ | users | <- 作成できてるか確認 +------------------+ 1 row in set (0.01 sec) mysql> SELECT * FROM `users`; +----+---------+------+------------+------------+------------+ | id | name | age | created_at | updated_at | deleted_at | +----+---------+------+------------+------------+------------+ | 1 | testKun | 21 | NULL | NULL | NULL | +----+---------+------+------------+------------+------------+ 1 row in set (0.00 sec) この時点でエラーが起きた場合 ・src/main.go 27行目のcontainer_dbはdocker-composeで指定したコンテナ名を指定しているか。 ・ポート解放してるか ・データベース(テーブル)が正しく作成されているか(今回ならgolang) ・大文字小文字の区別を無視していないか ・go mod init src したか?(この辺は理解不足で間違っている場合がある。src以下のファイルをサブパッケージとして参照できるようにする??) ・docker起動できているか以下コマンドで確認 # 動いているdocker一覧表示 docker-compose ps # 作成した全てのdocker一覧表示 docker-compose ps -a 豆知識 main.goの内容を変更してもDocker再buildする必要はない SequelAce GoENV GO111MODULE="" GOARCH="arm64" GOBIN="" GOCACHE="/root/.cache/go-build" GOENV="/root/.config/go/env" GOEXE="" GOFLAGS="" GOHOSTARCH="arm64" GOHOSTOS="linux" GOINSECURE="" GONOPROXY="" GONOSUMDB="" GOOS="linux" GOPATH="/root/go" GOPRIVATE="" GOPROXY="https://proxy.golang.org,direct" GOROOT="/usr/local/go" GOSUMDB="sum.golang.org" GOTMPDIR="" GOTOOLDIR="/usr/local/go/pkg/tool/linux_arm64" GCCGO="gccgo" AR="ar" CC="gcc" CXX="g++" CGO_ENABLED="1" GOMOD="/go/src/go.mod" CGO_CFLAGS="-g -O2" CGO_CPPFLAGS="" CGO_CXXFLAGS="-g -O2" CGO_FFLAGS="-g -O2" CGO_LDFLAGS="-g -O2" PKG_CONFIG="pkg-config" GOGCCFLAGS="-fPIC -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build147035606=/tmp/go-build -gno-record-gcc-switches" 参考記事 docker-compose upって何? Go言語の依存モジュール管理ツール Modules の使い方 他多数
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Go】QiitaでLGTMされたらライン通知する【LINE Notify】

動機 Goの勉強を始め、A Tour of Go を一通り終え、何か作ろうと思っていました。 結局何か作ってみるのが一番身に付きますからね。 自分が使うもの HTTP通信してみたい DB操作もかじりたい という理由で、LGTMをLINE通知するプログラムを作成することにしました。 概要 ざっくり下記の処理を定期的に繰り返すものをGoで作り、AWSEC2にデプロイしました。 Qiita API v2を実行し、記事についてるLGTM数を取得 ローカルで保持しているLGTM数と取得したLGTM数を比較 比較した結果差分がある場合は、LINE Notify APIを実行し通知し、DBに保持するLGTM数に更新をかける 通知は画像のようにしてくれます。 『記事タイトル』(記事ID)のLGTM数が変化しました。変動前のLGTM数 -> 変動後のLGTM数 というメッセージでLGTM数に変動があったことを教えてくれます。 ※自分以外の方の記事データで動作確認していたため、黒塗りしてます。 説明 Qiita API v2を実行し、記事についてるLGTM数を取得する 記事についているLGTM数を取得するのに、下記APIを実行します。 GET /api/v2/users/:user_id/items(レファレンスはこちら) このAPIは:user_idに指定したユーザの記事一覧を返却します。 実行結果は下記のように返却されます。likes_countがLGTMの数になります。 実行結果 [ { "rendered_body": "xxx",(値が長くなるのでxxxで表現する) "body": "xxxx",(値が長くなるのでxxxで表現する) "coediting": false, "comments_count": 0, "created_at": "2020-07-11T20:33:09+09:00", "group": null, "id": "3280948b32bc5f1497fd", "likes_count": 4, ★★★★★★★likes_countがLGTMの数★★★★★★★  ~~ (略)〜〜 }, { // 記事数分だけ要素繰り返し } ] コードは下記になります。 QiitaAPIv2を実行し記事一覧を取得するコード // 引数のConfig、戻り値のItemは自分で作った構造体 // Configは種々の設定値を持つ、ItemはAPIの実行結果を持つ func GetItems(conf configs.Config, page int) (items []Item, err error) { // 実行するHTTPメソッド、URLの設定。conf.QiitaUserNameに:user_idに指定するIDを持ってる req, err := http.NewRequest("GET", "https://qiita.com/api/v2/users/"+conf.QiitaUserName+"/items?page="+strconv.Itoa(page)+"&per_page=20", nil) if err != nil { return nil, err } // ヘッダーの設定、conf.QiitaAccessTokenにアクセストークンを持ってる。 req.Header.Set("Authorization", "Bearer "+conf.QiitaAccessToken) client := &http.Client{} // リクエストの実行 resp, err := client.Do(req) if err != nil { return nil, err } // レスポンスボディ読み取り、構造体に変換 body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } defer resp.Body.Close() if err := json.Unmarshal(body, &items); err != nil { return nil, err } return items, nil APIの実行結果を持つ構造体 type Item struct { RenderedBody string `json:"rendered_body"` Body string `json:"body"` Coediting bool `json:"coediting"` CommentsCount int `json:"comments_count"` CreatedAt time.Time `json:"created_at"` Group struct { CreatedAt time.Time `json:"created_at"` ID int `json:"id"` Name string `json:"name"` Private bool `json:"private"` UpdatedAt time.Time `json:"updated_at"` URLName string `json:"url_name"` } `json:"group"` ID string `json:"id"` LikesCount int `json:"likes_count"` Private bool `json:"private"` ReactionsCount int `json:"reactions_count"` Tags []struct { Name string `json:"name"` Versions []string `json:"versions"` } `json:"tags"` Title string `json:"title"` UpdatedAt time.Time `json:"updated_at"` URL string `json:"url"` User struct { Description string `json:"description"` FacebookID string `json:"facebook_id"` FolloweesCount int `json:"followees_count"` FollowersCount int `json:"followers_count"` GithubLoginName string `json:"github_login_name"` ID string `json:"id"` ItemsCount int `json:"items_count"` LinkedinID string `json:"linkedin_id"` Location string `json:"location"` Name string `json:"name"` Organization string `json:"organization"` PermanentID int `json:"permanent_id"` ProfileImageURL string `json:"profile_image_url"` TeamOnly bool `json:"team_only"` TwitterScreenName string `json:"twitter_screen_name"` WebsiteURL string `json:"website_url"` } `json:"user"` PageViewsCount int `json:"page_views_count"` } 設定値を持つ構造体、jsonの設定ファイルをこの構造体に変換してます type Config struct { LineAccessToken string `json:"line_access_token"` LineNotifyURL string `json:"line_notify_url"` QiitaUserName string `json:"qiita_user_name"` QiitaAccessToken string `json:"qiita_access_token"` DbDataSourceName string `json:"db_data_source_name"` LogPath string `json:"log_path"` } ページング GET /api/v2/users/:user_id/itemsで一回に取得できる記事の件数には限りがあるので、全件取得するにはページングする必要があります。 再帰で実現しました。 ページングして記事全件取得する func GetAllItems(conf configs.Config, page int, items []Item) ([]Item, error) { // 記事を取得する temp, err := GetItems(conf, page) if err != nil { return nil, err } // 再帰の終了条件、記事の取得件数が0になるまでAPIの実行を繰り返す if len(temp) == 0 { return items, nil } // 取得した記事をスライスに入れる for _, item := range temp { if item.Private { // 非公開の記事はスキップ continue } items = append(items, item) log.Println(fmt.Sprintf("取得した記事 : %s(%s) LGTM(%d)", item.Title, item.ID, item.LikesCount)) } page++ return GetAllItems(conf, page, items) } こんなやり方しなくてもこの記載を読んで API実行結果のレスポンスヘッダのLinkをみれば、ページ分だけAPI実行ができそうなことに後から気づきました。 ローカルで保持しているLGTM数と取得したLGTM数を比較 mapに記事のIDをキーにして、記事情報を保持します。 取得した記事のIDでmapに検索をかけて、LGTM数を比較します。 取得した記事情報保持するmapとmapの初期化 var itemMap = map[string]qiita.Item{} // 記事情報保持するmapの初期化 func initializeMap(items []qiita.Item) { for _, item := range items { itemMap[item.ID] = item } } mapにある記事情報と取得した記事情報を比較する // 記事の取得 items, err := qiita.GetAllItems(conf, 1, []qiita.Item{}) for _, item := range items { // 取得した記事のIDでmapに検索をかける beforeItem, ok := itemMap[item.ID] // mapに該当記事のデータ存在しない場合、記事投稿してから初回の取得など if !ok { itemMap[item.ID] = item _, err = db.InsertIntoItem(item) if err != nil { errorNotifyAndTerminate(conf, err) } continue } // LGTMの数が変化した場合  if item.LikesCount != beforeItem.LikesCount { // 通知 line.Notify(conf, fmt.Sprintf("『%s』(%s)のLGTM数が変化しました。%d -> %d", item.Title, item.ID, beforeItem.LikesCount, item.LikesCount)) // ローカルで持ってる記事データの更新 itemMap[item.ID] = item _, err = db.UpdateItem(item) if err != nil { errorNotifyAndTerminate(conf, err) } } } LINE Notify APIを実行し通知する 通知するには下記APIを実行します。 POST https://notify-api.line.me/api/notify(レファレンスはこちら) リクエストパラメータmessageに指定した文字列が、通知されます。 API実行例 % curl -X POST -H 'Authorization: Bearer <access_token>' -F 'message=testメッセージ' https://notify-api.line.me/api/notify {"status":200,"message":"ok"} 下記のように通知されます。 コードは下記になります。 LINENotifyAPIを実行するコード // 引数のmessageに通知する文章を指定する func Notify(conf configs.Config, message string) { // リクエストパラメータの設定 form := url.Values{} form.Add("message", message) body := strings.NewReader(form.Encode()) // リクエスト生成、conf.LineNotifyURLにAPIのURLを持たせてる req, err := http.NewRequest("POST", conf.LineNotifyURL, body) if err != nil { log.Fatalln(err) } // ヘッダの設定、conf.LineAccessTokenにアクセストークンを持たせてる req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Authorization", "Bearer "+conf.LineAccessToken) client := &http.Client{} resp, err := client.Do(req) if err != nil { log.Fatalln(err) } defer resp.Body.Close() } DBに更新をかける。 下記のテーブルにLGTMの数を保持しています。 テーブル MySQL [qiita]> select id, likes_count, title from item; +----------------------+-------------+-------------------------------------------------------------------------------------------------------------------------------+ | id | likes_count | title | +----------------------+-------------+-------------------------------------------------------------------------------------------------------------------------------+ | 13bb03d163824fc0f3d0 | 0 | A Tour of GoのExercise: Mapsを解く | | 13d12f70b42e8b54ff01 | 0 | 今更CSVの形式について知る~RFC4180の2.Definition of the CSV Formatを読む~ | | 3280948b32bc5f1497fd | 4 | enumの比較は==がつよつよ、equalsはよわよわ【Java】 | | 48eb8f4f6ff82477a7d7 | 0 | Go言語に入門する -MacOS * VSCode で開発環境を構築して”Hello World"を出力する- | | 70baed0b4711434bd88e | 0 | dockerコンテナからホストにファイルをコピーする(docker cp) | | 90c656397260697dac3a | 0 | 【Spring Boot】独自のプロパティファイルを追加して、env.getProperty()で値を取得したい。 | | bb2e8903ab199b34ac70 | 2 | 【Oracle】ユーザ作ろうとしたら、ORA-65096: invalid common user or role name って怒られたときの解決法 | +----------------------+-------------+-------------------------------------------------------------------------------------------------------------------------------+ テーブルに記事データが存在しない場合は、func InsertIntoItem(item qiita.Item) (sql.Result, error)を呼び出してデータを作成します。 それ以外は、func UpdateItem(item qiita.Item) (sql.Result, error)を呼び出して更新します。 INSERT,UPDATE var db *sql.DB func InsertIntoItem(item qiita.Item) (sql.Result, error) { return db.Exec("INSERT INTO item(id, title, likes_count) values(?, ?, ?)", item.ID, item.Title, item.LikesCount) } func UpdateItem(item qiita.Item) (sql.Result, error) { return db.Exec("UPDATE item SET likes_count = ? WHERE id = ?", item.LikesCount, item.ID) } なお、var db *sql.DBは起動時に下記を呼び出して初期化してます。 dbの初期化 func Initialize(conf configs.Config, items []qiita.Item) { var err error db, err = sql.Open("mysql", conf.DbDataSourceName) if err != nil { log.Fatal(err) } db.SetMaxIdleConns(10) db.SetMaxOpenConns(10) db.SetConnMaxLifetime(10 * time.Second) } 一連の処理を定期的に繰り返す LGTM数の取得、比較、通知を繰り返すのに、無限ループさせました。 ループ中の処理の最後にtime.Sleep(time.Second * 60)させ、1分間隔でループさせます。 一連の処理を無限ループさせる。 // forに後ろに何も書かなければ無限ループになる for { // 記事の取得 items, err := qiita.GetAllItems(conf, 1, []qiita.Item{}) for _, item := range items { // 記事投稿してから初回の取得、mapに該当記事のデータ存在しない場合 beforeItem, ok := itemMap[item.ID] if !ok { itemMap[item.ID] = item _, err = db.InsertIntoItem(item) if err != nil { errorNotifyAndTerminate(conf, err) } continue } // LGTMの数が変化した場合 if item.LikesCount != beforeItem.LikesCount { // 通知 line.Notify(conf, fmt.Sprintf("『%s』(%s)のLGTM数が変化しました。%d -> %d", item.Title, item.ID, beforeItem.LikesCount, item.LikesCount)) // ローカルで持ってる記事データの更新 itemMap[item.ID] = item _, err = db.UpdateItem(item) if err != nil { errorNotifyAndTerminate(conf, err) } } } // 1分間隔でループさせる time.Sleep(time.Second * 60) } 感想とか DB操作してみたいからテーブルに記事データ持たせてみたけど、LGTM数に差分があったら通知するっていうのをやるだけなら特にテーブルは必要なかった。 作るだけ作って放置しないように、早めにqiitaに投稿しようと思っていたけど、結局一月くらい放置してた。 なにか作ったらすぐ投稿できる大人になりたかった。 ソース 参考 Qiitaでいいねが付くとLINE通知してくれる優しい世界を構築した Qiita APIを使って「いいね」をLINEに通知するアプリをLambdaで構築した [超簡単]LINE notify を使ってみる Qiita API v2 documentation - Qiita:Developer LINE Notify API Document
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む