Jex’s Note

OS 相關

動態庫與靜態庫

linux 下有動態庫和靜態庫. 動態庫以 .so 為副檔名, 靜態庫以 .a 為副檔名

ldd 指令可以查詢此程序用到哪些動態庫

$ ldd dch_worker
linux-vdso.so.1 =>  (0x00007fff60bfe000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f6d583b2000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f6d57fec000)
/lib64/ld-linux-x86-64.so.2 (0x00007f6d585dc000)

檢查主機狀態

  • pid : process id
  • ppid : parent process id

ps

顯示特定幾個欄位

ps -Ao pid,%cpu,%mem,comm

上面的參數 commcommand 的差別在 command 會完整顯示程序的指令,而 comm 只會顯示你用什麼程序,例如使用 command 會多看到後面的參數 /Google Chrome Helper --type=renderer

記億體使用量前十的程序

PID     %CPU    %MEM    COMMAND
ps -Ao pid,%cpu,%mem,comm | sort -k3rn | head -n 10

CPU使用量前十的程序

PID     %CPU    %MEM    COMMAND
ps -Ao pid,%cpu,%mem,comm | sort -k2rn | head -n 10

pstree

列出此 pid 下的 process tree

$ pstree 18082
-+= 18082 jex bash /Users/jex/projects/test/run.sh
 \--- 18083 jex ./test_pid

top

  • sorted by memory : top -o MEM

htop

Mac 使用者要用 sudo htop, 否則 CPU, MEM 可能會無法顯示正確

CPU:

  • Blue = Low priority threads
  • Green = Normal priority threads
  • Red = Kernel threads

Memory:

||||||||||||(綠藍黃)    57/2003MB   其中的 57 就是 Used memory
  • Green = Used memory
  • Blue = Buffers
  • Yellow/Orange = Cache

State:

  • R running or runnable (on run queue)
  • D uninterruptible sleep (usually IO)
  • S interruptible sleep (waiting for an event to complete)
  • Z defunct/zombie, terminated but not reaped by its parent
  • T stopped, either by a job control signal or because it is being traced

如果搭配 free -m 來看的話,

             total       used       free     shared    buffers     cached
Mem:          2003        153       1849          0         11         85
                                                        (藍色)      (黃色)

57 = 153 - 11 - 85

快捷鍵:

Shortcut Key    Function Key    Description
h               F1              Invoke htop Help
S               F2              Htop Setup Menu
/               F3              Search for a Process
I               F4              Invert Sort Order
t               F5              Tree View
>               F6              Sort by a column
[               F7              Nice – (change priority)
]               F8              Nice + (change priority)
k               F9              Kill a Process
q               F10             Quit htop
  • P : Sorted by CPU%
  • M : Sorted by Mem%
  • u : Display only processes of a single user

pmap

查看 process 使用記憶體的狀況

pmap 11047

strace

查看 process 或 command 執行時從頭到尾的狀況

strace -p 11047
strace echo "ff"

Process vs Thread

  • 一個程序會產生一個 process, 但一個 process 裡面可以有很多 thread
  • 不同的 process 之間的記憶體不是共用的
  • 同一個 process 的 threads 裡面, 記憶體資料是共用的
  • 每個 thread 有自已的 stack

Process thread

如果有一個程式執行在 htop 可能會看到4個行程, 而且 pid 都不一樣, 但並不代表他是用 4個process

如果按 t, 就會可以清楚的看到是一個主行程跟三個子行程, 但那三個子行程都是 thread, 有可能是在

不同 CPU 跑的, thread 的記憶體是跨 cpu 可以共用的, 但 process 不行

為什麼 thread 也會有 pid? 因為 linux 把 thread 也看成一個 process

Out of memory

當記憶體不夠用時, 系統 OOM Killer 會開始 kill 最耗記憶體的 process

先看 log 狀態

/var/log/messages (amz-linux)

[952079.901834] Node 0 hugepages_total=0 hugepages_free=0 hugepages_surp=0 hugepages_size=2048kB
[952079.906483] 357 total pagecache pages
[952079.908658] 0 pages in swap cache
[952079.910747] Swap cache stats: add 0, delete 0, find 0/0
[952079.913679] Free swap  = 0kB
[952079.915551] Total swap = 0kB
[952079.917343] 524189 pages RAM
[952079.919165] 0 pages HighMem/MovableOnly
[952079.921434] 11312 pages reserved
[952079.923367] [ pid ]   uid  tgid total_vm      rss nr_ptes nr_pmds swapents oom_score_adj name
[952079.928155] [ 1520]     0  1520     2866      232      11       3        0         -1000 udevd
[952079.932846] [ 1646]     0  1646     2865      221      10       3        0         -1000 udevd
                                        ...(略)...
[952080.158896] [ 5665]   500  5665   142716    41467     189       3        0             0 php-fpm-7.0
[952080.164699] [ 5693]   500  5693   142102    13936     136       3        0             0 php-fpm-7.0
[952080.169943] [ 5695]   500  5695   143401    16756     140       3        0             0 php-fpm-7.0
[952080.174979] [ 5707]   500  5707   141966    14276     136       3        0             0 php-fpm-7.0
[952080.180028] [ 5716]   500  5716   143000    41756     189       3        0             0 php-fpm-7.0
[952080.185071] [ 5753]   500  5753   143162    16590     141       3        0             0 php-fpm-7.0
[952080.190104] [ 5754]   500  5754   142978    14747     137       3        0             0 php-fpm-7.0
[952080.196286] [ 5766]   500  5766   142451    14158     135       3        0             0 php-fpm-7.0
[952080.201450] [ 5771]   500  5771   106828     5564     118       3        0             0 php-fpm-7.0
                                        ...(略)...
[952080.390850] Out of memory: Kill process 5716 (php-fpm-7.0) score 81 or sacrifice child
[952080.395916] Killed process 5716 (php-fpm-7.0) total-vm:572000kB, anon-rss:167024kB, file-rss:0kB
[952320.400064] INFO: task php-fpm-7.0:5716 blocked for more than 120 seconds.

在最後幾行可以看的出來, php-fpm 被 kill 了, 而在出現這行前面你可以看到當時各 process 使用的 memory 狀態

各欄位代表的意思

  • pid: process ID
  • uid: user ID
  • tgid: Thread group ID
  • total_vm: Virtual memory use (in 4 kB pages)
  • rss: Resident memory use (in 4 kB pages) (這裡的 rss 與 top 的 rss 單位不同, 這裡的是 4kb 不是 1kb)
  • nr_ptes: Page table entries
  • nr_pmds: TODO TODO TODO TODO
  • swapents: Swap entries
  • oom_score_adj: 要 kill 的優先權, 越高越先 kill
  • name: process name

算出某個 process 使用的記憶體量

[952080.395916] Killed process 5716 (php-fpm-7.0) total-vm:572000kB, anon-rss:167024kB, file-rss:0kB

例如 5716 這個 process, total_vm: 143000, rss: 41756, 上面一行得出來的數字也這裡的 x4, 也就是 5716 這個 process 實體記憶體+swap 用了 167MB

ref: https://stackoverflow.com/questions/9199731/understanding-the-linux-oom-killers-logs

系統 log

Ubuntu: /var/log/kern.log, /var/log/syslog

amz-linux: /var/log/messages

檢查 memory leak 工具

  • pidstat
  • Valgrind

AWS - S3 上傳 (PHP Codeigniter)

(最後更新: 2016-04-27)

介紹

以下會介紹上傳到 s3 的 2 種方法 :

  • mobile 上傳到 s3
  • server 上傳到 s3

command 上傳到 s3, 請參考此篇 AWS-CLI command 上傳到 s3

安裝 AWS SDK

首先先用 composer 安裝 aws-sdk, composer.json :

{
    "require": {
        "aws/aws-sdk-php": "2.7.0"
    }
}

執行

$ composer install

Codeigniter (application/config/config.php) 打開 : $config['composer_autoload'] = TRUE;

mobile 上傳到 s3

有兩種方法

  • 直接給手機一個 S3 的 IAM User 讓手機以 SDK 上傳圖片
  • S3 server 產生有 expire time 的 pre-signed URL (由 server 取得授權的 url) 給 mobile 上傳圖片

直接給手機一個 S3 的 IAM User 讓手機以 SDK 上傳圖片

原本的 Resource 的值是 *, 將它改成以下

Policy for Programmatic Access : (線上 AWS Console 是沒權限看到的)

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:ListBucket"],
      "Resource": ["arn:aws:s3:::test"]
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:PutObjectACL",
        "s3:GetObject",
        "s3:GetObjectACL",
        "s3:DeleteObject"
      ],
      "Resource": ["arn:aws:s3:::test/*"]
    }
  ]
}

但是設定好以上, 雖然程式可以有權限存取, 但是 AWS Console 是沒有權限訪問 S3 的, 所以可以參考以下讓這個 User 能看到 S3 的 Console

Policy for Console Access : (線上 AWS Console 是可以看到目錄結構的)

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetBucketLocation",
        "s3:ListAllMyBuckets"
      ],
      "Resource": "arn:aws:s3:::*"
    },
    {
      "Effect": "Allow",
      "Action": ["s3:ListBucket"],
      "Resource": ["arn:aws:s3:::test"]
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:PutObjectACL",
        "s3:GetObject",
        "s3:GetObjectACL",
        "s3:DeleteObject"
      ],
      "Resource": ["arn:aws:s3:::test/*"]
    }
  ]
}

這樣 mobile 就只能存取 test-mobile 這個 bucket 下的資源了

注意!! 上傳的檔案到 server 的權限是沒有 public-read 的, 所以多加兩個權限 PutObjectACL(更新權限), GetObjectACL(查看權限)

S3 server 產生有 expire time 的 pre-signed URL (由 server 取得授權的 url) 給 mobile 上傳圖片

create pre-signed url :

public function create_pre_signed_url()
{
    $config = array(
        'key'       => 'A******************Q',
        'secret'    => 'P**************************************4',
        'region'    => 'us-west-2',
    );

    $client = S3Client::factory($config);
    $bucket = "s3_bucket_name";
    $key = 'data.txt';
    $url = "{$bucket}/{$key}";

    // get() returns a Guzzle\Http\Message\Request object
    $request = $client->put($url);

    // Create a signed URL from a completely custom HTTP request that
    // will last for 10 minutes from the current time
    $signedUrl = $client->createPresignedUrl($request, '+10 minutes');
    echo $signedUrl;

    //echo file_get_contents($signedUrl);
    // > Hello!
}
  • 因為要讓這個 pre-signed url 可以被上傳檔案, 所以使用 $client->put(
  • 如果改成 $client->get( 輸出的 URL 就可以看到 bucket 的 data.txt 的內容, 那這功能就跟 getObjectUrl 一樣了

要測試它是否 OK 要實際用上傳檔案,不要用瀏覽器開,不然只會得到 XML 錯誤訊息

<Error>
    <Code>SignatureDoesNotMatch</Code>
</Error>

使用 command 模擬 mobile 上傳。建立 /tmp/data.txt, 然候執行以下指令上傳檔案

curl -v -T /tmp/data.txt https://s3_bucket_name.s3.amazonaws.com/data.txt?AWSAccessKeyId=A********************2Q&Expires=1423551936&Signature=y**************2FvYVo%3D
  • 不管傳什麼格式,不用特別指定 Header
  • -T 是使用 PUT 上傳檔案

ref :

server 上傳至 s3

到 IAM 建立一個 User, policy 新增只允許存取某個 bucket 下某個 folder 的 custom policy

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetBucketLocation",
                "s3:ListAllMyBuckets"
            ],
            "Resource": "arn:aws:s3:::*"
        },
        {
          "Effect": "Allow",
          "Action": ["s3:ListBucket"],
          "Resource": ["arn:aws:s3:::bucket_name"]
        },
        {
        "Effect": "Allow",
        "Action": "*",
        "Resource": ["arn:aws:s3:::bucket_name/folder_name/*"]
        }
    ]
}

Uploading files to s3 in php

$config = array(
    'key'       => 'A******************A',
    'secret'    => '9**************************************i',
    'region'    => 'us-west-2',
);

$client = S3Client::factory($config);

$result = $client->putObject([
    'Bucket'    => 'bucket_name',
    'Key'       => 'folder_name/qqq/test.txt',
    'SourceFile'    => '/tmp/qq.txt',
]);

不存在的 folder 會自動建立, 如果檔案已存在會覆蓋原始檔案

刪除 s3 檔案

TODO: 補上 code

這裡有個很容易踩雷的點,而且錯誤訊息不好判斷,就是要刪除的檔案路徑最前面不要加 /,直接路徑加檔名即可 e.g. xxxx/ffff/ssss.mp4

[其他] 產生 S3 只能 GET 不能 PUT 的 Pre-signed URL

get pre-signed url :

use Aws\S3\S3Client;

class S3 extends MY_Controller
{
    public function __construct()
    {
        parent::__construct();
    }

    public function get_pre_signed_url()
    {
        $config = array(
            'key'       => 'A******************Q',
            'secret'    => 'P**************************************4',
        );

        $client = S3Client::factory($config);

        // Get a plain URL for an Amazon S3 object
        $plainUrl = $client->getObjectUrl('s3_bucket_name', 'data.txt');
        // > https://my-bucket.s3.amazonaws.com/data.txt

        // Get a pre-signed URL for an Amazon S3 object
        $signedUrl = $client->getObjectUrl('s3_bucket_name', 'data.txt', '+10 minutes');
        // > https://my-bucket.s3.amazonaws.com/data.txt?AWSAccessKeyId=[...]&Expires=[...]&Signature=[...]

        // Create a vanilla Guzzle HTTP client for accessing the URLs
        $http = new \Guzzle\Http\Client;

        // Try to get the plain URL. This should result in a 403 since the object is private
        try {
            $response = $http->get($plainUrl)->send();
        } catch (\Guzzle\Http\Exception\ClientErrorResponseException $e) {
            $response = $e->getResponse();
        }
        echo $response->getStatusCode();
        // > 403

        // Get the contents of the object using the pre-signed URL
        echo $signedUrl;
        $response = $http->get($signedUrl)->send();
        echo $response->getBody();
        // > Hello!
    }
}

?>

以上面程式產生 pre-signed url, 然候頁面上會印出 url, 再拿這個 url 就可以看到檔案裡的內容了

ref:

Golang 測試

原生 testing

Commands

執行當下目錄的 test

go test

執行所有 test 包括子目錄

go test ./...

ignore 某個 package

go test `go list ./... | grep -v your_go_app/utilities`

test 特定的 package

go test your_go_app/utilities/ip

First example

import "testing"

func Sum(x int, y int) int {
    return x + y
}

func TestSum(t *testing.T) {
    total := Sum(5, 5)
    if total != 10 {
       t.Errorf("Sum was incorrect, got: %d, want: %d.", total, 10)
    }
}

Ginkgo

Ginkgo 是一套 Golang BDD Testing Framework

Install

go get github.com/onsi/ginkgo/ginkgo
go get github.com/onsi/gomega

Create first test

Init

ginkgo bootstrap
ginkgo generate [app_name]

user_test.go :

var _ = Describe("User", func() {
    Describe("Test User", func() {
        Context("with SetName", func() {
            data := map[string]interface{}{
                "Name":       "Jex",
                "Address":    "Taiwan",
            }
            It("should be a empty errMsg", func() {
                Expect(SetName(data)).To(Equal(""))
            })
        })
    })
})

Run

$ ginkgo

...略...
Ran 3 of 3 Specs in 6.895 seconds
SUCCESS! -- 3 Passed | 0 Failed | 0 Pending | 0 Skipped PASS

BeforeEach 在每個 Context 跑之前先執行

var _ = Describe("Sample", func() {
    Describe("XXX", func() {
        Describe("FFF", func() {
            var qq string
            BeforeEach(func() {
                qq = "XXX"
                fmt.Println("BeforeEach")
            })
            Context("Valid inputs for add function", func() {
                It("TTT", func() {
                    qq = "ZZZZZ"
                    fmt.Println(qq)
                    Expect(4).Should(Equal(4))
                })
                It("TTT", func() {
                    fmt.Println(qq)
                    Expect(4).Should(Equal(4))
                })
            })
        })
    })
})

結果 :

BeforeEach      // qq = XXX
ZZZZZ           // qq = ZZZZ
BeforeEach      // qq = XXX
XXX             // qq = XXX
  • 不管是 BeforeEach 賦與的變數或不同地方賦與的變數都要在 It 裡面才拿得到,在 It 外是取不到的,不知道是什麼原因?
  • Describe / Context 似乎是用 goroutine 去跑的,如果有些值要先等前一個 Describe/context Set 完,執行時可能不會有你預期的結果,有可能根本沒 Set 到就先執行那段了

ref:

Tools

clusterssh 一次管理多台主機安裝及設定

  sudo apt-get install clusterssh

執行 :

  sudo cssh -l root 192.168.1.200 192.168.1.201

設定成設定檔 :

$ sudo nano /etc/clusters
clusters = testcluster
testcluster = 192.168.1.200 192.168.1.201

$ sudo cssh -l root testcluster

format 你的 jq

可以將 curl 後的結果 format 成好閱讀的格式

http://stedolan.github.io/jq/

下載 64 binaries 到 /usr/local/bin/

使用 : curl http://www.example.com/api | jq .

sysdig

Sysdig is open source, system-level exploration: capture system state and activity from a running Linux instance, then save, filter and analyze.

Sysdig is scriptable in Lua and includes a command line interface and a powerful interactive UI, csysdig, that runs in your terminal.

Think of sysdig as strace + tcpdump + htop + iftop + lsof + awesome sauce.

With state of the art container visibility on top.

DNS Server

Bind9

Bind9 是一個可以架設 dns server 的套件

Installation

sudo apt-get install bind9 bind9-doc dnsutils

/etc/bind

  • named.conf : 主要設定檔, bind 啟動會讀取它, 它會負責讀取其他檔案
  • named.conf.local : 裡面可以設定正反解的zone, 在裡面加入 zone 的 DNS record 存放位置以及 DNS server 的形態
  • named.conf.options : 主要在設定dns的一些選項和安全性. 其他的檔案就是預設的一些localhost的正反解相關, 這裡我們不用理他沒關係

add blog.jex.tw

name.conf.options

不用改

name.conf.local

正解 :

zone "jex.tw" {
    type master;
    file "/etc/bind/zones/jex.tw.db";
};

反解只有 ISP 可以做到, 所以我就不設定了

/etc/bind/zones/jex.tw.db

自行建立 zones 資料夾, zone 都放在這裡方便管理

改設定要記得增加Serial值

$TTL 604800
; 指定 SOA
@ IN SOA ns.jex.tw. root.jex.tw. (
    2015020101     ; Serial YYYYMMDDnn
     604800     ; Refresh
      86400     ; Retry
    2419200     ; Expire
     604800 )   ; Negative Cache TTL

@       IN NS   ns.jex.tw. ; @ 代表 domain name

@       IN A    106.185.47.26
ns      IN A    106.185.47.26
www     IN A    106.185.47.26
blog IN  CNAME domains.logdown.com.

/etc/resolv.conf

加到第一行

nameserver 106.185.47.26

### iptable open 53 port

-A INPUT -p tcp --dport 53 -j ACCEPT
-A INPUT -p udp --dport 53 -j ACCEPT
Testing

1) 先檢查設定檔是否有誤

    $ named-checkzone jex.tw jex.tw.db
    zone jex.tw/IN: loaded serial 2015013001
    OK

2) 重啟 sudo service bind9 restart

如果有什麼啟動錯誤可以到 /var/log/syslog 搜尋 bind 看是錯在哪裡

因為上層的 DNS Record 還沒生效, 所以以下測試要加上 ip

3) host [domain name] [dns server ip]

    host blog.jex.tw 106.185.47.26
    Using domain server:
    Name: 106.185.47.26
    Address: 106.185.47.26#53
    Aliases:

    blog.jex.tw is an alias for domains.logdown.com.
    domains.logdown.com has address 106.186.25.116

也可以多加 -a 參數獲得更完整資訊

4) nslookup [domain name] [dns server ip]

    nslookup blog.jex.tw 106.185.47.26
    Server:         106.185.47.26
    Address:        106.185.47.26#53

    blog.jex.tw     canonical name = domains.logdown.com.
    Name:   domains.logdown.com
    Address: 106.186.25.116

5) dig @[dns server ip] [domain name], 看ANSWER SECTION是否有回應, 有的話代表本機的 dns 是有 work 的

    $ dig @106.185.47.26 blog.jex.tw

    ; <<>> DiG 9.9.5-3ubuntu0.1-Ubuntu <<>> @106.185.47.26 blog.jex.tw
    ; (1 server found)
    ;; global options: +cmd
    ;; Got answer:
    ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 5392
    ;; flags: qr aa rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 13, ADDITIONAL: 1

    ;; OPT PSEUDOSECTION:
    ; EDNS: version: 0, flags:; udp: 4096
    ;; QUESTION SECTION:
    ;blog.jex.tw.                   IN      A

    ;; ANSWER SECTION:
    blog.jex.tw.            3600    IN      CNAME   domains.logdown.com.
    domains.logdown.com.    11      IN      A       106.186.25.116

    ;; AUTHORITY SECTION:
    .                       3418    IN      NS      i.root-servers.net.
    .                       3418    IN      NS      d.root-servers.net.
    .                       3418    IN      NS      j.root-servers.net.
    .                       3418    IN      NS      c.root-servers.net.
    .                       3418    IN      NS      h.root-servers.net.
    .                       3418    IN      NS      e.root-servers.net.
    .                       3418    IN      NS      f.root-servers.net.
    .                       3418    IN      NS      m.root-servers.net.
    .                       3418    IN      NS      b.root-servers.net.
    .                       3418    IN      NS      l.root-servers.net.
    .                       3418    IN      NS      k.root-servers.net.
    .                       3418    IN      NS      g.root-servers.net.
    .                       3418    IN      NS      a.root-servers.net.

    ;; Query time: 2 msec
    ;; SERVER: 106.185.47.26#53(106.185.47.26)
    ;; WHEN: Sat Jan 31 18:04:10 UTC 2015
    ;; MSG SIZE  rcvd: 300

106.185.47.26#53 是這台主機第一個會去問的 dns, 如果是家裡上網沒有特別指定 dns 的話可能是 192.168.1.1#53(192.168.1.1)

6) 確認有無生效可以兩種方式測

nslookup blo.jex.tw 看解析的 server 是不是 8.8.8.8

或直接 ping ns.jex.tw

當 dig, nslookup 都能正確解析 domain 不代表 ping 就會 ping 的到, 即使你在 dns server ping ping 自己的 domain

如果 ping: unknown host ns.jex.tw

+trace 找問題

DNS 補充

  • A : 用來解析IP位址, 將 HostName 對應到某個IP.
  • CNAME : 將 HostName 對應某個網域.
  • MX : 郵件伺服交換記錄, 對應郵件伺服器使用.
  • TXT : 文件記錄,這幾乎用不到, 特殊用途.
  • SRV : 資源記錄,一樣用不到, 特殊用途.
  • AAAA : 等同A記錄,只不過A是IPv4,而AAAA是IPv6.
  • NS : Name Server, 指定你的DNS要交由哪個NS進行解析.

只能指向 domain 或 ip

例如 :

  • example.com -> 106.173.25.11
  • a.example.com -> ttt.example222.com

無法解析到 / 之後, 因為這不是 dns 的工作 :

  • example.com -> ttt.example.222.com/home

只允許特定 host

只開放 example.com, www.example.com, db.example.com

A (Host) 設定
@       45.33.60.10             => example.com
db      45.33.60.10             => www.example.com
www     45.33.60.10             => db.example.com

將 www.example.com 導到 example.com

A (Host) 設定
@     45.33.60.10
*     45.33.60.10

CName (Alias)
www   example.com

將 example.com 導到 www.example

這要在 http server (nginx) 設定

解決 DNS 一直沒有更新問題

解法: 直接用 google 的 dns server (8.8.8.8)

我的 macbook 預設的 DNS 第一個是 seednet, 第二才是 google

我在 godaddy 的 dns 設定將 domain 指向新 IP 時, 在半小時內就生效了

但不管本機 DNS cache 怎麼清 ping 出來的一直是錯的 IP,

用 dig 及 nslookup 以 google dns 去找都可以找到正確的 IP,

猜想是不是 seednet 的 dns server 的資料一直是舊的, 造成我一直抓不到正確的 IP,

索性最後直接把本機的 DNS 改成 googld dns, 問題都解決了, 改的方法如下 :

系統偏好設定 -> 網路 -> USB 乙太網路 下面 進階 -> 新增 8.8.8.8

不需要改 /etc/resolv.conf, 因為改它是沒有用的, mac 每次開機都會自動產生一份檔案

dig

顯示 domain 經由 dns 路徑的主機

$ dig +short NS blog.jex.tw
jex-lin.github.io.
github.map.fastly.net.

顯示完整的 DNS 路徑

dig +trace blog.jex.tw

; <<>> DiG 9.8.3-P1 <<>> +trace blog.jex.tw
;; global options: +cmd
.                       369863  IN      NS      k.root-servers.net.
.                       369863  IN      NS      e.root-servers.net.
(...略...)
;; Received 228 bytes from 168.95.1.1#53(168.95.1.1) in 205 ms          // 第一個 dns server IP, 中華電信 DNS server

tw.                     172800  IN      NS      d.dns.tw.
tw.                     172800  IN      NS      i.dns.tw.
(...略...)
;; Received 508 bytes from 192.5.5.241#53(192.5.5.241) in 266 ms        // 第二個 dns server IP
jex.tw.                 86400   IN      NS      ns73.domaincontrol.com.
jex.tw.                 86400   IN      NS      ns74.domaincontrol.com.
;; Received 84 bytes from 61.220.48.1#53(61.220.48.1) in 114 ms         // 第三個 dns server IP

blog.jex.tw.            3600    IN      CNAME   jex-lin.github.io.
;; Received 60 bytes from 216.69.185.47#53(216.69.185.47) in 201 ms     // 第四個 dns server IP

1) 先找到全球性的根節點, 問到 .tw 的 dns 主機位置

2) 在 dns.tw 主機問到 jex.tw 主機位置在 ns73.domaincontrol.comns74.domaincontrol.com, 這兩個都是我的 domain 商 godaddy 幫我安排的 dns server

3) 在 godaddy ns server 中問到需要 CNAME 到 jex-lin.github.io

nslookup

找出 domain 的 dns server

nslookup blog.jex.tw
Server:     106.187.35.20
Address:    106.187.35.20#53        // 第一個 dns server IP, linode DNS server

Non-authoritative answer:
blog.jex.tw canonical name = jex-lin.github.io.
jex-lin.github.io   canonical name = github.map.fastly.net.
Name:   github.map.fastly.net
Address: 103.245.222.133

ref

Rails Basic

Rails 指令

  • rails new my_app : 新增 my_app 專案
  • rails s : 啟動 server
  • rails s RAILS_ENV=development : 啟動 development 環境的 server
  • rails s -b 0.0.0.0 : 開放外部, 預設只允許本機 (127.0.0.1:3000)
  • rvmsudo rails s -p 80 : 使用 80 port, 使用 rvmsudo 是因為 rails 起 80 port 一定要用 sudo 身份
  • rails s -p 5000 : 換 port, 預設是 3000
  • rails c : rails console, 可以直接操作 ActiveRecord
  • rails c --sandbox : 沙盒的 console, 在這期間改的 DB 內容在離開時都會還原
  • rails db : 進入 db console 根據 config/database.yml. ex: 如果是 sqlite, 就會進到 sqlite 的 console
  • rake stats : 統計 code 寫了幾行
  • rake tmp:clear : 清除 cache

console :

  • Rails.cache.clear : 清除 cache

Custom helper

  • View 在不需要 controller include 可以直接用 helper 定義的 method
  • controller 要用 定義的 helper 一定要 include

app/helpers/event_helper.rb

module EventsHelper
  def do_something
  end
end

controller :

class BadgeController < ApplicationController
  include EventHelper

  ....
end

Mailer

SMTP 設定

config/application.rb

config.action_mailer.default_url_options = { host: $settings[:host] }
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
    address: "smtp.mailgun.org",
    port: 587,
    user_name: "postmaster@example.com",
    password: "9******************************d",
}

寄信範例

app/mailers/application_mailer.rb

class ApplicationMailer < ActionMailer::Base
  # 如果有設定的話, 在寄信時就不用特別指定
  default from: 'contact@gmail.com'
  default to: 'jxxxlin@gmail.com'

  layout 'mailer'
end

app/mailers/my_mailer.rb

class MyMailer < ApplicationMailer
  def welcome(user)
    @user = user

    mail(to: @user.email, subject: 'Welcome')
  end
end

寄給多位 User : to: 'aaa@gmail.com,bbb@gmail.com'

app/views/layouts/mailer.text.erb

<%= @user.nickname %> 您好~

<%= yield %>

如果有任何疑問,歡迎隨時聯絡我們

謝謝您選擇我們,期待與您的合作。

app/views/my_mailer/welcome.text.erb

Welcome to example.com, <%= @user.name %>

app/controllers/user_controller.rb

MyMailer.welcome(current_user).deliver_later

寫完一封 mail 可以在 console 下執行, 測試信件是否發送出去

Send to multiple recipients

emails = @recipients.collect(&:email).join(",")
mail(to: emails, subject: "A replacement clerk has been requested")

cc & bcc

cc 看到的所有副本的收件人 email, bcc 則看不到

mail(to: recipient.email_address_with_name, bcc: ["bcc@example.com", "Order Watcher <watcher@example.com>"])

reply-to

當收件人按下回覆,預設回覆是給寄件人,但寄件人的 email 往往是客服的信箱,所以可以透過指定 reply-to 去指定收件人

mail(to: user_email, reply_to: [email_1, email_2])

.length vs .count vs .size差異

這三個共同點都是算一個集合的數量,

如果對像是 Array 這兩者的行為無差別,

有差別的是有時候我們會利用這兩者去判斷從 DB 撈出來的筆數如果 >0 再做相關的處理,

從 DB 撈出來的是 ActiveRecord::Relation

a = User.all
User Load (115.4ms)  SELECT "users".* FROM "users"

使用 .length, .size 會直接算出數量

a.length
 => 203

a.size
 => 203

使用 .count 就會使用 SQL count, 成本相對是比較高的

a.count
(2.5ms)  SELECT COUNT(*) FROM "users"<F6>
 => 203

結論 : 如果只是需要單純判斷目前 DB 資料的筆數用 .count, 如果不是或不確定用哪個就用 .length.size

區分環境變數

config/settings.yml

:development:
  :host: '127.0.0.1'

:production:
  :host: 'example.com'

config/application.rb

# 載入 settings.yml
require 'yaml'
$settings = YAML.load(File.open("#{__dir__}/settings.yml"))[Rails.env.to_sym]

要記得先重啟 rails server 才能讀取新的 config,在程式裡使用,會依照你的環境讀取相對的設定

$settings[:host]

其他

.gitignore 新增

*.swp
db/schema.rb
*.DS_Store
Gemfile.lock
config/database.yml

# Personal :
config/settings.yml

只顯示 date 就好

如果網站上常使用 date 顯示, 一般輸出 created_at 都要用 created_at.strftime('%F %T') 很不方便

可以建一個 config/initializers/time_format.rb 或直接寫在 application.rb

Time::DATE_FORMATS[:default] = "%Y-%m-%d %H:%M:%S"

將物件儲存成字串

Marshal

a = {qq: 'xxx', ff: {cc: 'ccc', dd: 'ddddd'}}

# 轉成字串
Marshal.dump(a)
 => "\x04\b{\a:\aqqI\"\bxxx\x06:\x06ET:\aff{\a:\accI\"\bccc\x06;\x06T:\addI\"\nddddd\x06;\x06T"

# 轉回成物件
Marshal.load("\x04\b{\a:\aqqI\"\bxxx\x06:\x06ET:\aff{\a:\accI\"\bccc\x06;\x06T:\addI\"\nddddd\x06;\x06T")
 => {:qq=>"xxx", :ff=>{:cc=>"ccc", :dd=>"ddddd"}}

JSON

# 轉成 JSON
a.to_json
 => "{\"qq\":\"xxx\",\"ff\":{\"cc\":\"ccc\",\"dd\":\"ddddd\"}}"

# 轉成 json ( hash key)
JSON.parse("{\"qq\":\"xxx\",\"ff\":{\"cc\":\"ccc\",\"dd\":\"ddddd\"}}")
 => {"qq"=>"xxx", "ff"=>{"cc"=>"ccc", "dd"=>"ddddd"}}

# 轉成 <F10>
JSON.parse("{\"qq\":\"xxx\",\"ff\":{\"cc\":\"ccc\",\"dd\":\"ddddd\"}}", symbolize_names: true)
 => {:qq=>"xxx", :ff=>{:cc=>"ccc", :dd=>"ddddd"}}

Marshal 會佔比較多的字符 55 : 43

解開 session

Marshal.load(Base64.decode64(cookie_token.split("--")[0]))

devise 登入使用記住我,cookie_token 才能解

Time zone - Taipei 時間

application.rb

config.time_zone = 'Taipei'

簡化判斷 + 迴圈的寫法

View 迴圈

如果要使用 each 前需要先判斷他是否為 nil 或 empty, 否則物件空的可能會噴 Error

此寫法只適合傳回空值是 empty 的, 因為可以將 if 及 each 寫在同一行, 就不需要兩層了(if 一層, each 一層),

但在寫法上也比較麻煩一些, 得先知道他是 empty, 還是 nil

取多筆時, 如果不存在, 返回 empty 的話, 如下 :
>   @posts = Post.where(user_id: 3333)
  Post Load (0.1ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ?  [["user_id", 3333]]
 => #<ActiveRecord::Relation []>

因為返回的是 empty, 不是 nil 所以它會是 ture, 以下寫法可以將 ifeach 寫成同一行

<% if @posts.each do |post| %>
  <%= post.title %>
<% end.empty? %>
  You have no posts.
<% end %>

不顯示錯誤訊息的話 :

<% if @posts.each do |post| %>
  <%= post.title %>
<% end.empty?; end %>

對於不顯示錯誤訊息的寫法我覺得 end 那邊有點稍亂

希望能繼續找到更好的寫法

簡化 console 指令

~/.irbrc

class T
  def self.mail(mail)
    User.where(email: mail)
  end
end

Rails console :

T.mail('me@gmail.com)

console 下自動 include (好像沒用.. at 2015/9/3)

config/application.rb

config.console do
  include Rails.application.routes.url_helpers
end

找出 Gem 的真實路徑

In console

 > Gem.loaded_specs['rails'].full_gem_path
 => "/usr/local/rvm/gems/ruby-2.2.1/gems/rails-4.2.3"

Rails lib 真實路徑

ex: upload
/usr/local/rvm/gems/ruby-2.2.1/gems/actionpack-4.2.3/lib/action_dispatch/http/upload.rb

為每條 log 前加上 user_id

Started GET "/" for 127.0.0.1 at 2016-03-31 14:12:52 +0800

加上後 :

[user_id:2] Started GET "/" for 127.0.0.1 at 2016-03-31 14:12:52 +0800

如何加上 :

config/application.rb

config.middleware.delete(ActionDispatch::Cookies)
config.middleware.delete(ActionDispatch::Session::CookieStore)
config.middleware.insert_before(Rails::Rack::Logger, ActionDispatch::Session::CookieStore)
config.middleware.insert_before(ActionDispatch::Session::CookieStore, ActionDispatch::Cookies)

config/initializers/logging.rb

Rails.configuration.log_tags = [
  proc do |req|
    if req.session["warden.user.user.key"].nil?             # 這個 key 是 devise 的
      "Anonym"
    else
      "user_id:#{req.session["warden.user.user.key"][0][0]}"
    end
  end
]

ref : http://stackoverflow.com/questions/10811393/how-to-log-user-name-in-rails

Network

Link anatomy

  • href: http://example.org:8888/foo/bar?q=baz#bang
  • protocol: http
  • host: example.org:8888
  • hostname: example.org
  • port: 8888
  • pathname: /foo/bar
  • query: q=baz
  • hash: bang

Load Balance

分幾種, 常見的是 L3, L7

L3 是 IP 層, 是以指派 IP 到後面的主機

L7 是 HTTP 層, 分析 Domain 或 Header 再指派到後面的主機, 效率較 L3 快

長連線

飽活封包底層就會做掉了, 建 socket 就會傳心跳 (habit) 給 server 確認它活著

habit 是 tcp 三項交握的 sync 封包

Keep-alive

tcp 沒有 keep-alive 一個 request 就會佔用一條通道

tcp 有 keep-alive 就多個 request 只需做一次三項交握, 都在同一條通道傳送

一個 socket 只能接一個 session, 不能一對多

HTTPS

SSL 憑證加密, 從 Header 到 Body 都是加密的

HTTP method

PUT 與 PATCH 差異

PUT 相當於是 delete + insert, 是對整個資源進行更新

PATCH 是只更新部份的資源

瀏覽器支援 PATCH, PUT 跟 DELETE 嗎?

不支援

HTML 只定義了 GET/POST,

所以 HTML Form 是沒有支援 PUT/DELETE 的

但 XmlHttpRequest (也就是Ajax) 有定義 GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS

但可以在 HTML Form 裡偷藏 _method 參數, 定義不支援的 method, 送到 server 端再判斷

Etag

第一次 : browser 對 server 發出請求, server 回應 200 ok, 並多加上 header[‘ETag’] = body 以 md5 編碼

browser 會 cache response 及儲存 Etag

第二次 : browser 對 server 發出請求並多帶 headers[‘If-None-match’] = 上面 ETag 的值, server 再算出 ETag 是否值為一樣, 一樣的話會返回 304 Not Modified

browser 收到 304 會從 cache 拿之前 cache 的結果

IP

5 個 CLASS

Class A :   0.xx.xx.xx ~ 127.xx.xx.xx       # 0xxxxxxx.xxxxxxxx.xxxxxxxx.xxxxxxxx   開頭是 0
Class B : 128.xx.xx.xx ~ 191.xx.xx.xx       # 10xxxxxx.xxxxxxxx.xxxxxxxx.xxxxxxxx   開頭是 10
Class C : 192.xx.xx.xx ~ 223.xx.xx.xx       # 110xxxxx.xxxxxxxx.xxxxxxxx.xxxxxxxx   開頭是 110
Class D : 224.xx.xx.xx ~ 239.xx.xx.xx       # 1110xxxx.xxxxxxxx.xxxxxxxx.xxxxxxxx   開頭是 1110
Class E : 240.xx.xx.xx ~ 255.xx.xx.xx       # 1111xxxx.xxxxxxxx.xxxxxxxx.xxxxxxxx   開頭是 1111

Public & Private

Class A:10.0.0.0    - 10.255.255.255
Class B:172.16.0.0  - 172.31.255.255
Class C:192.168.0.0 - 192.168.255.255

Port

Web app 開 80 port 遇到 permission denied

只有 root 才可以開小於 1024 的 port,最簡單的解決方法就是先開一個高一點的 port (e.g. 8080),再用 iptable 去 forward 80 -> 8080

iptables -t mangle -A PREROUTING -p tcp --dport 80 -j MARK --set-mark 1
iptables -t mangle -A PREROUTING -p tcp --dport 443 -j MARK --set-mark 1
iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080
iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 8181
iptables -I INPUT -m state --state NEW -m tcp -p tcp --dport 8080 -m mark --mark 1 -j ACCEPT
iptables -I INPUT -m state --state NEW -m tcp -p tcp --dport 8181 -m mark --mark 1 -j ACCEPT

VPN

讓 Private 可以被特定的連線操作

  • OpenVPN (SSL VPN) or IPsec
  • OpenVPN 安全性會比 IPsec 好

常用的 Http status

  • 301 (Permanent) — Redirects to the site you specified in the Forward To field using a “301 Moved Permanently” HTTP response. The HTTP 301 response code tells user-agents (including search engines) that the location has permanently moved.
  • 302 (Temporary) — Redirects to the site you specified in the Forward To field using a “302 Found” HTTP response. The HTTP 302 response code tells user-agents (including search engines) that the location has temporarily moved.
  • 200 (:ok) : ok. 常用在一般 api
  • 201 (:created) : 新增成功. 在 call create api 時回應
  • 422 (:unprocessable_entity) : Creating a new resource is not successful. create 失敗時回應
  • 204 (:no_content) : Successful responses with no content. 在 call destroy api 時回應
  • 500 : internal server errors # Rails automatically handles server errors and returns a 500 response.
  • 401 : Unauthorized

Server 回覆較常使用的

  • 200 : 成功回覆
  • 400 : Client 發送時引發的錯誤,例如參數有少
  • 401 : 認證錯誤,例如 AccessToken 錯誤
  • 500 : Server 內部發生錯誤,例如 DB 連線錯誤

產生 API 文件

swagger-ui

Introduction

swagger 是個介面, 有許多語言實作它, 但 swagger-ui 不是跟任何語言綁定, 它只讀 json, 然候生成 api 文件

撰寫 API 文件

可用 online swagger IDE, 匯入 json, 寫完再匯出 json 放到自己的 code 裡

其他

  • swagger 上面可以讀外部連結的 json, 如果沒有跨 domain 問題就可以讀取
  • 可以設定 access key 給不同人, 然候 public 這個 api 文件, 讓只有 access key 的人才可以用這個 api

Golang - Job Queue

介紹

Job Queue 是一個工作的等待區,只要有工作就往 Queue 裡面塞,背景有隻程式會去 Queue 裡面找工作做, 最主要的目的是除了讓使用者的體驗更好之外,也可以讓 Web app server 的工作單純一點,將複雜的工作交給專門執行 job queue 的主機, 例如圖片縮圖, 寄信.. etc.

goworker

介紹

Goworker 是 golang 的一套 job queue 的 package,它可以幫你達成這些事,它是使用 resque 的資料格式(resque 有固定的資料格式), resque 是一套 Ruby 開發的 job queue,也有被其他的語言開發成該語言的版本,你可以把它當作是 golang 版的 resque, 最主要的好處是你後端語言可以用 php, ruby etc. 你想的語言寫,把它丟到 redis 裡的 job queue, 然候 goworker 再去拿, 並針對不同的 task 寫出不同對應的程式

Worker

package main

import (
    "fmt"
    "time"

    "github.com/benmanns/goworker"
)

func myFunc(queue string, args ...interface{}) error {
    fmt.Printf("From %s, %v\n", queue, args)
    return nil
}

func init() {
    goworker.Register("MyClass", myFunc)
}

func main() {
    if err := goworker.Work(); err != nil {
        fmt.Println("Error:", err)
    }
    fmt.Printf("Started on %v", time.Now().Format("2006-01-02 15:04:05"))
}

Run :

go run main.go -queues=MyClass

增加一筆 job 讓 worker 執行

你也可以用其他語言 insert 一筆 job

package main

import (
    "encoding/json"
    "log"
    "time"

    "github.com/garyburd/redigo/redis"
)

var redisPool redis.Pool

func init() {
    redisPool = redis.Pool{
        MaxIdle:     3,
        MaxActive:   0, // When zero, there is no limit on the number of connections in the pool.
        IdleTimeout: 30 * time.Second,
        Dial: func() (redis.Conn, error) {
            conn, err := redis.Dial("tcp", "127.0.0.1:6379")
            if err != nil {
                log.Fatal(err.Error())
            }
            return conn, err
        },
    }
}

func main() {
    redisConn := redisPool.Get()
    x := map[string]interface{}{
        "foo": []string{"a", "b"},
        "bar": "foo",
        "baz": 10.4,
    }
    resque := map[string]interface{}{
        "class": "MyClass",
        "args":  []interface{}{x},
    }
    b, _ := json.Marshal(resque)
    redisConn.Do("RPUSH", "resque:queue:MyClass", string(b[:]))
}

Run :

go run qq.go

結果 :

worker 就會 print 那筆 job 的資料

Golang 其他

Cpu Usage

package main

import (
    "fmt"
    "io/ioutil"
    "strconv"
    "strings"
    "time"
)

func getCPUSample() (idle, total uint64) {
    contents, err := ioutil.ReadFile("/proc/stat")
    if err != nil {
        return
    }
    lines := strings.Split(string(contents), "\n")
    for _, line := range lines {
        fields := strings.Fields(line)
        if fields[0] == "cpu" {
            numFields := len(fields)
            for i := 1; i < numFields; i++ {
                val, err := strconv.ParseUint(fields[i], 10, 64)
                if err != nil {
                    fmt.Println("Error: ", i, fields[i], err)
                }
                total += val // tally up all the numbers to get total ticks
                if i == 4 {  // idle is the 5th field in the cpu line
                    idle = val
                }
            }
            return
        }
    }
    return
}

func main() {
    idle0, total0 := getCPUSample()
    time.Sleep(3 * time.Second)
    idle1, total1 := getCPUSample()

    idleTicks := float64(idle1 - idle0)
    totalTicks := float64(total1 - total0)
    cpuUsage := 100 * (totalTicks - idleTicks) / totalTicks

    fmt.Printf("CPU usage is %f%% [busy: %f, total: %f]\n", cpuUsage, totalTicks-idleTicks, totalTicks)
}

執行 command

package main

import (
    "fmt"
    "os/exec"
)

func main() {
    text := "hello world"
    cmd := exec.Command(
        "echo",
        text,
        ">",
        "test.txt")

    fmt.Println(cmd.Args)

    out, err := cmd.Output()
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(string(out))
}

result :

[echo hello world > test.txt]
hello world > test.txt

progress bar

package main
import (
  "fmt"
  "strings"
  "time"
  "os"
)
func main() {
  for i := 0; i < 50; i++ {
    time.Sleep(100 * time.Millisecond)
    h := strings.Repeat("=", i) + strings.Repeat(" ", 49-i)
    fmt.Printf("\r%.0f%%[%s]", float64(i)/49*100, h)
    os.Stdout.Sync()
  }
  fmt.Println("\nAll System Go!")
}

讓字一個一個 print 出來

package main

import (
    "bufio"
    "fmt"
    "os"
    "time"
)

func main() {
    text := "快來玩玩吧! Try it out!"
    printSlowly(text, 200)

    bufio.NewReader(os.Stdin).ReadBytes('\n')
}

func printSlowly(text string, speed int) {
    text_rune := []rune(text)

    for i := 0; i < len(text_rune); i++ {
        time.Sleep(time.Duration(speed) * time.Millisecond)
        fmt.Printf("\r%s", string(text_rune[0:i+1]))
        os.Stdout.Sync()
    }
    fmt.Print("\n")
}

Dump (for debug) - spew

a := map[string]int64{}
a["A"] = 1
a["B"] = 2
debug.Dump(a)

執行結果 :

(map[string]int64) (len=2) {
 (string) (len=1) "A": (int64) 1,
 (string) (len=1) "B": (int64) 2
}

curl

_, str := curl.String("http://www.google.com")
fmt.Println(str)

gorequest

agent := gorequest.New().CustomMethod("POST", "url").Timeout(30 * time.Second)
agent.Header = header               // 將 HEADER 以 map 型態傳進去
agent.Send(post_data)               // POST 需要, GET 不用, 傳入 String
resp, body, errs := agent.End()
if errs != nil {
    // Do something
}

if resp.StatusCode != 200 {
    // Do something
}

MySQL

(last updated at 2016-12-21)

Connect

conn, err = sql.Open("mysql", "root:password@tcp(127.0.0.1:3306)/db_name")
或
conn, err = sql.Open("mysql", "root:password@/db_name")
if err != nil {
    os.Exit(1)
}
err = conn.Ping()
if err != nil {
    os.Exit(1)
}
defer db.Close()

Prepare (prevent sql injection)

有兩種寫法,第一種是用 Query;第二種是先 PrepareQueryExec

SELECT with Query (with Prepare)

rows, err := conn.Query("SELECT name FROM users WHERE age=?", req.FormValue("age)
defer rows.Close()
if rows.Next() {
    var name string
    err = rows.Scan(&name)
} else {
    // no data
}

SELECT with Prepare

stmt, err := conn.Prepare("SELECT name, address FROM user WHERE age = ? AND name = ?")
defer stmt.Close()
rows, err := stmt.Query(27, "jex") // replace the params
defer rows.Close()
for rows.Next() {
    var name, address string
    err = rows.Scan(&name, &address)
    fmt.Println(name, address)
}

使用 Query 要注意 :

// this is safe
conn.Query("SELECT name FROM users WHERE age=?", req.FormValue("age)

// this allows sql injection.
conn.Query("SELECT name FROM users WHERE age=" + req.FormValue("age"))

UPDATE with Prepare

stmt, err := conn.Prepare("UPDATE user SET name = ?, age = ? WHERE id = ?")
defer stmt.Close()
res, err := stmt.Exec("jex", 27, 123)   // replace the params
num, err := res.RowsAffected()          // 判斷是否有成功影響欄位的值
fmt.Println(num)
// where 不到,num 是 0
// where 到,但資料沒變也是 0
// where 到,但資料有變是 1

CRUD 操作

SELECT 取第一筆

var str string
err = conn.QueryRow("SELECT id FROM user").Scan(&str)

SELECT 多筆 (fetch every single row)

var id int
var name string
rows, err := conn.Query("SELECT id, name FROM user")
defer rows.Close()
for rows.Next() {
    err := rows.Scan(&id, &name)    // do check
    fmt.Println(id, name)
}
err = rows.Err()        // do check

Query Row 取第一筆

var name string
err = db.QueryRow("SELECT name FROM users WHERE id = ?", 1).Scan(&name)

stmt, err := db.Prepare("SELECT name FROM users WHERE id = ?")
var name string
err = stmt.QueryRow(1).Scan(&name)

INSERT (TODO)

stmt, err := db.Prepare("INSERT ... ")
res, err := stmt.Exec("A", "B")
id, err := res.LastInsertId()

UPDATE (TODO)

stmt, err = db.Prepare("UPDATE ...")
res, err = stmt.Exec("A", id)
affect, err := res.RowsAffected()

DELETE (TODO)

stmt, err = db.Prepare("DELETE ...")
res, err = stmt.Exec(id)
affect, err = res.RowsAffected()

ORM

package main

import (
    "database/sql"
    "github.com/coopernurse/gorp"
    _ "github.com/go-sql-driver/mysql"
    "log"
)

func main() {
    // initialize the DbMap
    dbmap := initDb()
    defer dbmap.Db.Close()

    err := dbmap.Insert(&Tt{Name: "test"})
    checkErr(err, "Insert failed")
}

type Tt struct {
    Name string
}

func initDb() *gorp.DbMap {
    // connect to db using standard Go database/sql API
    // use whatever database/sql driver you wish
    db, err := sql.Open("mysql", "root:password@/go_test")
    checkErr(err, "sql.Open failed")
    dbmap := &gorp.DbMap{Db: db, Dialect: gorp.MySQLDialect{}}
    dbmap.AddTableWithName(Tt{}, "tt")
    return dbmap
}

func checkErr(err error, msg string) {
    if err != nil {
        log.Fatalln(msg, err)
    }
}

unix

調整系統設定最大讀寫檔案數量 (for currency)

unix 預設一個 process 最多可開 1024 個檔案,也就是能出去的 network currency 數量是 1024

import "golang.org/x/sys/unix"

var rLimit unix.Rlimit
var max_rlimit uint64 = 50000
var cur_rlimit uint64 = 50000
rLimit.Max = max_rlimit
rLimit.Cur = cur_rlimit
err := unix.Setrlimit(unix.RLIMIT_NOFILE, &rLimit)
if err != nil {
    log.Println("Setting Rlimit Error:", err)
}

err = unix.Getrlimit(unix.RLIMIT_NOFILE, &rLimit)
if err != nil {
    log.Println("Getting Rlimit Error:", err)
.

fasthttp

效能比原生的 http 好

import (
    "github.com/buaazp/fasthttprouter"
    "github.com/valyala/fasthttp"
)

router := fasthttprouter.New()
router.GET("/", Index)
fasthttp.ListenAndServe(":8010", router.Handler)

func Index(ctx *fasthttp.RequestCtx, ps fasthttprouter.Params) {
    // `Get` num params
    n := string(ctx.FormValue("num"))

    // 輸出
    fmt.Fprintf(ctx, "hello, %s!n", ps.ByName("name"))
}

帶入 net.Listener 的方式

l, err := net.Listen("tcp", ":"+strconv.Itoa(port))
if err != nil {
    fmt.Println("net.Listen error: %v", err)
    os.Exit(1)
}
router := fasthttprouter.New()
router.GET("/", Index)
err = fasthttp.Serve(l, router.Handler)
if err != nil {
    fmt.Println(err)
    os.Exit(1)
}

go-logging

一套 log 的 package 特色如下 :

  • 可以自由的設計 log format
  • 可以自動地紀錄 call log 時當時是在什麼 package 及 func, 在 debug 時相當好用
  • 可以設定當 log 達到什麼層級時另外紀錄下來
  • 輸出不同 level 的 log 時有顏色

Example :

// 標準輸出的 log
logger = logging.MustGetLogger("example")
format := logging.MustStringFormatter(`%{color}%{time:15:04:05} %{shortpkg} %{shortfunc} [%{level}] %{color:reset} %{message}`)
backend := logging.NewLogBackend(os.Stderr, "", 0)
backendFormatter := logging.NewBackendFormatter(backend, format)

// 如果發生 Error 層級以上的 log 另外做處理
// 如果 buffer 有值就 post 到 slack
var buf bytes.Buffer
go func() {
    // 攔截 log 的內容送到 slack
    for {
        if buf.Len() > 0 {
            fmt.Println(buf.String())   // 這段可以改成 post 到 slack
            buf.Reset()                 // 將 buffer 清空
        }
        time.Sleep(1 * time.Second)     // 每秒檢查一次
    }
}()
// 將會進到 backend2 的 log 暫存到 buffer
backend2 := logging.NewLogBackend(&buf, "", 0)
format2 := logging.MustStringFormatter(`%{time:15:04:05} %{shortpkg} %{shortfunc} [%{level}] %{message}`)
backend2Formatter := logging.NewBackendFormatter(backend2, format2)
backend2Leveled := logging.AddModuleLevel(backend2Formatter)
// 設定什麼層級的 log 會進到 backend2
backend2Leveled.SetLevel(logging.ERROR, "")

// 註冊
logging.SetBackend(backendFormatter, backend2Leveled)

Mux - API Router

func main() {
    l, _ := net.Listen("tcp", ":3333")
    r := mux.NewRouter()
    r.HandleFunc("/", Index)
    log.Fatal(http.Serve(l, r))
}

func Index(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Time: " + time.Now().Format(time.RFC1123)))
}