go 語言特點
指令碼化的語法,容易上手。
靜態型別+編譯性,開發、執行效率都有保證
函式式 & 物件導向 兩種程式設計正規化,
原生支援併發程式設計支援,降低開發成本,維護成本,以及更好的相容性,效率。
劣勢:語法糖沒有 Python 和 Ruby 多。執行效率不及C,但已趕超C++,Java。第三方庫不多,就是輪子少(喜歡造輪子的可以加入golang輪子大軍)。
安裝
官方: golang.org/
官方映象站: golang.google.cn/
國內官方站點: go-zh.org/
Linux
golang.org/dl/ 下載最新Go語言二進位制包
wget https://dl.google.com/go/go1.13.15.linux-amd64.tar.gz
tar -C /usr/local -xzf go1.13.15.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin
go version
Mac
$ brew install go
## 安裝制定版本
$ brew install go@1.13
Windows
訪問 官方映象站 下載 msi
安裝檔案。
環境變數
安裝好 go 後。執行go env
就可以得到當前 go runtime 相關的環境變數了,這些變數可以透過
go env -w GO_ENV_NAME='new Value'
或是
$ env GO_ENV_NAME='new Value'
都能影響 go env
目錄結構
這裡講一下 $GOPATH
也就是 ~/go
下的目錄結果:
$ ls ~/go/
bin # go 可執行檔案目錄
pkg # go 歸檔檔案目錄
src # go 下載的程式碼包原始碼檔案,存放目錄
go命令
go run
go
自帶了 runtime
,透過 go run file.go
就能執行程式程式碼了。
引數:
-a : 強制編譯相關程式碼,不論他們的原始碼是否有改變,編譯結果是否最新。
-n : 列印編譯過程中所需要的執行命令,但不真正執行它們
-p n : 並行編譯,n為並行數量
-v : 列出被編譯的程式碼包的名稱
-work : 顯示編譯時建立的的臨時目錄,並且不刪除。
-x : 列印編譯過程中所需執行的命令
go build
編譯出目標檔案。
類比
nodejs
就是,npm build
, 類比 Java
go build main.go
編譯製定的 main.go 程式碼檔案
go build
執行命令就是將當前程式碼包作為程式碼包並編譯
引數:
-a 所有涉及、被引入的程式碼都會被重新編譯。
go install
編譯並安裝程式碼包或原始檔。
golang 安裝程式碼包動作就是將編譯結果移動到 pkg/平臺號編號
/歸檔檔案。
如果是可執行檔案,則移動到當前工作去的 bin
目錄,或者 $GOBIN
目錄.
go get
從遠端程式碼倉庫上下載、安裝程式碼包。
遠端倉科可以是:git
、mercurial(HG)
、svn
,bazaar
。
程式碼下載後會放到 $GOPATH/src
目錄中.
-d : 只執行下載動作,不執行安裝動作
-fix : 先執行`修正`動作,再進行編譯和安裝程式碼包。
-x : 展示命令執行過程
-u : 下載並更新程式碼包
修正: 指語法向上相容。
go 程式碼關鍵字
break //退出迴圈
default //選擇結構預設項(switch、select)
func //定義函式
interface //定義介面
select //channel
case //選擇結構標籤
chan //定義channel
const //常量
continue //跳過本次迴圈
defer //延遲執行內容(收尾工作)
go //併發執行
map //map型別
struct //定義結構體
else //選擇結構
goto //跳轉語句
package //包
switch //選擇結構
fallthrough //??
if //選擇結構
range //從slice、map等結構中取元素
type //定義型別
for //迴圈
import //匯入包
return //返回
var //定義變數
標示符
append | bool | byte | cap | close | complex | complex64 | complex128 | uint16 |
---|---|---|---|---|---|---|---|---|
copy | false | float32 | float64 | imag | int | int8 | int16 | uint32 |
int32 | int64 | iota | len | make | new | nil | panic | uint64 |
println | real | recover | string | true | uint | uint8 | uintptr |
語言特色
不要求縮排,不要求末尾加分號——;,同一行程式碼中有多個表示式,需要用 分號 分割。沒有使用的變數,包,會導致報錯。
包
每個go原始檔開頭必須是package開頭,定義自己的包
一個目錄下,只能有一個包名
一個可執行的檔案必須要有 main()
函式
import 引入包
兩種引入風格
import "package1"
import "package2"
import (
"package1"
pa2 "package2" // 包別名,別名為 pa2
. "fmt"
_ "mysql"
)
. "fmt"
方式引入包的化,使用fmt裡面的函式就可直接使用,不用帶 fmt 字首了
如果引入的包不使用,會報錯, 或者加個字首 _ 即可,這樣的下劃線會把引入的包的init函式執行一下。定義的變數不用,也會報錯。
包內初始化函式
定義 包內 初始化函式
func init() {
}
只匯入這個包部分,並執行init函式,由於匯入不全,所以在程式碼中就不能使用這個包了。
import _ "MyPackage"
資料型別
序號 | 型別和描述 |
---|---|
1 | 布林型 布林型的值只可以是常量 true 或者 false。一個簡單的例子:var b bool = true。 |
2 | 數字型別 整型 int 和浮點型 float32、float64,Go 語言支援整型和浮點型數字,並且支援複數,其中位的運算採用補碼。 |
3 | 字串型別: 字串就是一串固定長度的字元連線起來的字元序列。Go 的字串是由單個位元組連線起來的。Go 語言的字串的位元組使用 UTF-8 編碼標識 Unicode 文字。 |
4 | 派生型別: 包括: (a) 指標型別(Pointer) (b) 陣列型別 (c) 結構化型別(struct) (d) Channel 型別 (e) 函式型別 (f) 切片型別 (g) 介面型別(interface) (h) Map 型別 |
使用 int 時,根據當前作業系統來的,64位系統對應 int64, 32位作業系統,對應int32.
變數宣告
- 變數宣告: var <變數名> [變數型別]
- 變數賦值: <變數名> = <值,表示式,函式返回>
- 變數宣告賦值:var <變數名> [變數型別] = <值,表示式,函式返回>
- 變數宣告,型別推斷,並賦值 <變數名> := <值,表示式,函式返回>
分組宣告
var (
i int
foo float32
name string
)
分組批次宣告、賦值
var a,b,c,d int = 1,2,3,4
a,b := 1,2
特殊變數 _
變數作用域
- 函式內定義的變數稱為區域性變數
- 函式外定義的變數稱為全域性變數
- 全域性變數必須使用
var
宣告,區域性變數可省略
作用域可以分為以下四個型別:
- 內建作用域:不需要自己宣告,所有的關鍵字和內建型別、函式都擁有全域性作用域
- 包級作用域:必須函式外宣告,在該包內的所有檔案都可以訪問
- 檔案級作用域:不需要宣告,匯入即可。一個檔案中透過import匯入的包名,只在該檔案內可用
- 區域性作用域:在自己的語句塊內宣告,包括函式,for、if 等語句塊,或自定義的 {} 語句塊形成的作用域,只在自己的區域性作用域內可用
語句塊
語句塊是由花括弧({})所包含的一系列語句。
在 Go 中還有很多的隱式語句塊:
- 主語句塊:包括所有原始碼,對應內建作用域
- 包語句塊:包括該包中所有的原始碼(一個包可能會包括一個目錄下的多個檔案),對應包級作用域
- 檔案語句塊:包括該檔案中的所有原始碼,對應檔案級作用域
- for 、if、switch等語句本身也在它自身的隱式語句塊中,對應區域性作用域
型別轉換
- 不存在隱式轉換,必須是顯示
- 型別轉換必須是在兩種相容的型別之間
- <變數名稱> [:]= <目標型別>( <需要轉換的變數名> )
型別轉換精度丟失
型別斷言
斷言,顧名思義就是果斷的去猜測一個未知的事物。在 go 語言中,interface{} 就是這個神秘的未知型別,其斷言操作就是用來判斷 interface{} 的型別。
var foo interface{} = 22
f, ok := foo.(int)
if !ok {
t.Log("Guess wrong ...")
}
t.Logf("The type is : %T", f)
常量
- 顯示 const idenfity [type] = value
- 隱式 const identify = value () (無型別常量)
變數型別支援: bool, int, float, string
特殊常量 iota
運算
算術運算
運算子 | 描述 | 例項 |
---|---|---|
+ | 相加 | A + B 輸出結果 30 |
- | 相減 | A - B 輸出結果 -10 |
* | 相乘 | A * B 輸出結果 200 |
/ | 相除 | B / A 輸出結果 2 |
% | 求餘 | B % A 輸出結果 0 |
++ | 自增 | A++ 輸出結果 11 |
– | 自減 | A– 輸出結果 9 |
關係運算
運算子 | 描述 | 例項 |
---|---|---|
== | 檢查兩個值是否相等,如果相等返回 True 否則返回 False。 | (A == B) 為 False |
!= | 檢查兩個值是否不相等,如果不相等返回 True 否則返回 False。 | (A != B) 為 True |
> | 檢查左邊值是否大於右邊值,如果是返回 True 否則返回 False。 | (A > B) 為 False |
< | 檢查左邊值是否小於右邊值,如果是返回 True 否則返回 False。 | (A < B) 為 True |
>= | 檢查左邊值是否大於等於右邊值,如果是返回 True 否則返回 False。 | (A >= B) 為 False |
<= | 檢查左邊值是否小於等於右邊值,如果是返回 True 否則返回 False。 | (A <= B) 為 True |
邏輯運算
運算子 | 描述 | 例項 |
---|---|---|
&& | 邏輯 AND 運算子。 如果兩邊的運算元都是 True,則條件 True,否則為 False。 | (A && B) 為 False |
|| | 邏輯 OR 運算子。 如果兩邊的運算元有一個 True,則條件 True,否則為 False。 | (A || B) 為 True |
! | 邏輯 NOT 運算子。 如果條件為 True,則邏輯 NOT 條件 False,否則為 True。 | !(A && B) 為 True |
位運算
運算子 | 描述 | 例項 |
---|---|---|
& | 按位與運算子”&”是雙目運算子。 其功能是參與運算的兩數各對應的二進位相與。 | (A & B) 結果為 12, 二進位制為 0000 1100 |
| | 按位或運算子”|”是雙目運算子。 其功能是參與運算的兩數各對應的二進位相或 | (A | B) 結果為 61, 二進位制為 0011 1101 |
^ | 按位異或運算子”^”是雙目運算子。 其功能是參與運算的兩數各對應的二進位相異或,當兩對應的二進位相異時,結果為1。 | (A ^ B) 結果為 49, 二進位制為 0011 0001 |
<< | 左移運算子”<<”是雙目運算子。左移n位就是乘以2的n次方。 其功能把”<<”左邊的運算數的各二進位全部左移若干位,由”<<”右邊的數指定移動的位數,高位丟棄,低位補0。 | A << 2 結果為 240 ,二進位制為 1111 0000 |
>> | 右移運算子”>>”是雙目運算子。右移n位就是除以2的n次方。 其功能是把”>>”左邊的運算數的各二進位全部右移若干位,”>>”右邊的數指定移動的位數。 | A >> 2 結果為 15 ,二進位制為 0000 1111 |
賦值運算
運算子 | 描述 | 例項 |
---|---|---|
= | 簡單的賦值運算子,將一個表示式的值賦給一個左值 | C = A + B 將 A + B 表示式結果賦值給 C |
+= | 相加後再賦值 | C += A 等於 C = C + A |
-= | 相減後再賦值 | C -= A 等於 C = C - A |
*= | 相乘後再賦值 | C *= A 等於 C = C * A |
/= | 相除後再賦值 | C /= A 等於 C = C / A |
%= | 求餘後再賦值 | C %= A 等於 C = C % A |
<<= | 左移後賦值 | C <<= 2 等於 C = C << 2 |
>>= | 右移後賦值 | C >>= 2 等於 C = C >> 2 |
&= | 按位與後賦值 | C &= 2 等於 C = C & 2 |
^= | 按位異或後賦值 | C ^= 2 等於 C = C ^ 2 |
|= | 按位或後賦值 | C |= 2 等於 C = C | 2 |
優先順序
優先順序 | 運算子 | 功能 |
---|---|---|
9 | () [] -> . | 字尾運算 |
8 | ! *(指標) & ++ – +(正號) -(負號) | 單目運算 |
7 | * / % + - | 算術運算,加減乘除 |
6 | << >> | 位運算 |
5 | == != < <= > >= | 邏輯運算、不等、等 |
4 | & | ^ | 按位 邏輯與、或 |
3 | || && | 邏輯或、與 |
2 | = += -= *= 等等 | 賦值運算 |
1 | , | 逗號 |
一元
、賦值
這兩大運算子是 從右到左
關聯,其他都是 從左到右
關聯。
注意:優先順序 值越大則優先順序越高。為了方便理解、記憶,我對沒有嚴格按照優先順序製表,只是做了個大概!!
更詳細的
程式碼控制語句
if, else, else if
var number int = 37
if number += 4; 10 > number {
fmt.Print("less than 10:", number)
} else if 10 < number {
number -= 2
fmt.Print("greater 10:", number)
} else {
}
switch, select
package main
import (
"fmt"
"math/rand"
)
func main() {
ia := []interface{}{byte(6), 'a', uint(10), int32(-4), "CC"}
v := ia[rand.Intn(4)]
// 值 switch
switch v {
case 'a' :
fmt.Println("char: ", v)
case 10 :
fmt.Println("uint: ", v)
case -4 :
fmt.Println("int: ", v)
case 0.1 :
fallthrough
caes "0.1"
fmt.Println("float: ", v)
default :
fmt.Println("byte: ", v)
}
// 變數型別 switch
switch interface{}(v).(type) {
case string :
fmt.Printf("Case A.")
case byte :
fmt.Printf("Case B.")
case int :
fmt.Printf("Case B.")
default:
fmt.Println("Unknown!")
}
}
注意,go語言和其他語言不同的時,每個case程式碼末尾會自動加上break
操作, 如果你需要使用 fallthrough
來抵消預設的 break
select 用於管道
for
是的 golang
的 for
集 for
,foreach
,for in
,while
於一體。
do while
表示:golang你這麼繞,不優雅
package main
import (
"fmt"
"time"
)
func main() {
map1 := map[int]string{1: "Golang", 2: "Java", 3: "Python", 4: "C"}
n := 1
for { // 省略則預設是true
if n > 3 {
break;
}
fmt.Println("for true map item: ", map1[n])
time.Sleep(1)
n++
}
for i := 1; i < 4; i++ {
fmt.Println("for i map item: ", map1[i])
}
for k,v := range map1 {
fmt.Print(k, ":", v)
}
}
goto, break, continue
goto 是跳過程式碼塊
package main
import (
"fmt"
"time"
)
func main() {
code:
fmt.Println("do some thing~")
time.Sleep(1)
goto code
}
break 跳出並結束迴圈
continue 跳過當前迴圈
雖然不能和
PHP
那樣break 2
跳出多層, 單隻要有goto就能幹很多事了。
golang給 迴圈 就分配了一個 for,語句跳轉語句卻整了那麼多花樣
複合資料
內建方法 make & new
內建方法就是不需要引入包就能用的
make 可以建立 slice、map、chan,返回指標型別
- slice 是可變長的陣列
- map 是key-map 資料陣列
- chan 是go獨有的 管道
一股c程式設計風格撲面而來, char *ptr = (char *)malloc(sizeof(char) * 5);
內建方法 new
記憶體置0,返回傳入型別的指標地址
package main
import fmt
import reflect
func main() {
mSlice := make([]string, 3)
mSlice[0] = "dog"
mSlice[1] = "cat"
mSlice[2] = "pig"
fmt.Println("animals: ", mSlice)
mMap := make(map[int]string)
mMap[10] = "dog"
mMap['2'] = "cat"
fmt.Println(reflect.TypeOf(mMap))
fmt.Println("animals :: ", mMap)
nMap := new(map[int]string)
fmt.Println(reflect.TypeOf(nMap))
}
append copy delete
slice可以使用copy,append 函式
delete 是專門用來刪除 map
- append(src, ele) 追加元素
- copy(dst, src) 把src元素賦值到dst上,
- delete() 刪除元素
例子:
package main
import "fmt"
func main() {
mSlice := make([]string, 3)
mSlice[0] = "dog"
mSlice[1] = "cat"
mSlice[2] = "pig"
fmt.Println("animals: ", mSlice)
// append(mSlice, "id-3") // 這樣寫會導致報錯: append(mSlice, "id-3") evaluated but not used
mSlice = append(mSlice, "id-3")
fmt.Println("animals update:", mSlice)
fmt.Println("animals len :", len(mSlice))
fmt.Println("animals cap:", cap(mSlice))
// newSlice := make([]string) // 這樣寫導致報錯:missing len argument to make([]string)
// newSlice := make([]string, 2) // 這樣寫會導致資料丟失2個,不會自動擴容
newSlice := make([]string, 3) // 不要多次定義初始化:no new variables on left side of :=
copy(mSlice, newSlice) // 這樣反向copy,會導致前面的幾個陣列元素被置為空
// copy(newSlice, mSlice)
fmt.Println("animals dst:", mSlice)
fmt.Println("animals copy:", newSlice)
delete(mMap, 50)
fmt.Println(mMap)
}
panic & recover
異常處理
panic() 丟擲異常
recover() 獲取異常
報錯會導致程式程式碼中斷,不會再執行後續操作
例子:
package main
import "fmt"
import "errors"
func panicFunc() {
defer func() {
// recover()
message := recover() // 宣告瞭message 變數就需要使用哦,不然報錯
fmt.Println("panice msg: ", message)
switch message.(type) {
case string:
case error:
fmt.Println("panice error msg: ", message)
default:
}
}()
// panic("報錯啦")
panic(errors.New("I am error."))
}
func main() {
panicFunc()
}
len & cap & close
len可以計算 string, array, slice, map, chan
cap 可以計算 slice, map, chan
len()
獲取陣列長度cap()
獲取佔用空間分配close()
用於關閉管道——chan
當宣告一個陣列時,go會預先分配一部分空間給當前陣列,獲取實際空間佔用大小,使用cap()
不用像PHP那樣,strlen(), count(), length 傻傻分不清楚了。
例子:
package main
import "fmt"
func main() {
mSlice := make([]string, 3)
mSlice[0] = "dog"
mSlice[1] = "cat"
mSlice[2] = "pig"
fmt.Println("animals: ", mSlice)
fmt.Println("animals update:", mSlice)
fmt.Println("animals len :", len(mSlice))
fmt.Println("animals cap:", cap(mSlice))
mChan := make(chan int, 1)
close(mChan)
mChan <- 1 // 會導致報錯: panic: send on closed channel
}
defer
定一個當前方法關閉時,執行的程式碼, 壓棧設計,先宣告的後執行。
結構體
package main
import "fmt"
type Dog struct {
ID int
Name string
Age int32
}
func main() {
var dog Dog
dog.ID = 1
dog.Name = "haha"
dog.Age = 3
fmt.Println("print Dog Struct", dog)
dog2 := Dog{ID:2, Name:"san", Age:4}
fmt.Println("print Dog 2 Struct", dog2)
dog3 := new(Dog)
dog3.ID = 3
dog3.Name = "Tom"
dog3.Age = 5
fmt.Println("print Dog 3 Struct", dog)
}
輸出
print Dog Struct {1 haha 3}
print Dog 2 Struct {2 san 4}
print Dog 3 Struct &{3 Tom 5}
屬性 & 函式
介面
/* define an interface */
type interface_name interface {
method_name1 [return_type]
method_name2 [return_type]
...
method_namen [return_type]
}
/* define a struct */
type struct_name struct {
/* variables */
}
/* implement interface methods*/
func (struct_name_variable struct_name) method_name1() [return_type] {
/* method implementation */
}
...
func (struct_name_variable struct_name) method_namen() [return_type] {
/* method implementation */
}
併發
指標
json
需要引入包 encoding/json
, 兩個函式分別是 json.Marshal()
, json.Unmarshal()
.
注意,最後一個是英文字母小寫的
L
,不是1
json 序列化
package main
import "fmt"
import "encoding/json"
type ServerInfo struct {
SerName string
SerIp string
SerPort uint16
}
func main() {
server := new(ServerInfo)
server.SerName = "http-nginx"
server.SerIp = "127.0.0.1"
server.SerPort = 8080
re,err := json.Marshal(server)
if nil != err {
fmt.Println("error: ", err.Error())
} else {
fmt.Println("struct json bytes: ", re)
fmt.Println("struct json string: ", string(re))
}
mServer := make(map[string]interface{})
mServer["serverName"] = "apache2-http"
mServer["serIp"] = "192.168.30.133"
mServer["serPort"] = "3033"
mRe,err := json.Marshal(mServer)
if nil != err {
fmt.Println("error: ", err.Error())
} else {
fmt.Println("map json string: ", string(mRe))
}
}
輸出
struct json bytes: [123 34 83 101 114 78 97 109 101 34 58 34 104 116 116 112 45 110 103 105 110 120 34 44 34 83 101 114 73 112 34 58 34 49 48 46 49 48 48 46 49 55 46 50 55 58 51 48 48 48 49 34 44 34 83 101 114 80 111 114 116 34 58 56 48 56 48 125]
struct json string: {"SerName":"http-nginx","SerIp":"10.100.17.27:30001","SerPort":8080}
map json string: {"serIp":"192.168.30.133","serPort":"3033","serverName":"apache2-http"}
ps: 我也不知道
10.100.17.27:30001
是怎麼回事
json 反序列化
可以使用 tag 來做 mapping,
package main
import "fmt"
import "encoding/json"
type ServerInfo struct {
SerName string `json:"name"`
SerIp string `json:"ip"`
SerPort uint16 `json:"port"`
}
func main() {
// jsonStr := "{\"SerName\":\"http-nginx\",\"SerIp\":\"10.100.17.27:30001\",\"SerPort\":8080}" \\ 雙引號注意轉義
jsonStr := "{\"name\":\"http-nginx\",\"ip\":\"10.100.17.27:30001\",\"port\":8080}"
sServer := new(ServerInfo)
jsonBytes := []byte(jsonStr)
uerr := json.Unmarshal(jsonBytes, &sServer)
if nil != uerr {
fmt.Println("error: ", err.Error())
} else {
fmt.Println("uns struct: ", sServer)
}
jsonStr3 := `{"serIp":"192.168.30.133","serPort":"3033","serverName":"apache2-http"}` \\ 使用鍵盤1旁邊的 ` 符號包裹雙引號就不用轉義了
uSer := make(map[string]interface{})
uErr := json.Unmarshal([]byte(jsonStr3), &uSer)
if nil != uErr {
fmt.Println("error: ", uErr.Error())
} else {
fmt.Println("unmar map: ", uSer)
}
}
輸出
uns struct: &{http-nginx 10.100.17.27:30001 8080}
unmar map: map[serIp:192.168.30.133 serPort:3033 serverName:apache2-http]
tag
tag
這個東東把,就是json的別名,感覺這個功能是go的特色,與encoding/json
包緊密結合。
為什麼會有這個東西,我估計是這個和 go命名規則 有關,go命名規則,要求public的變數開頭要大寫,小寫開頭的變數是private的,所以,json中的變數就會影響一個介面體變數的訪問許可權,為了不像java那樣複雜,提供了方便的tag功能。
package main
import "fmt"
import "encoding/json"
type ServerInfo struct {
SerName string `json:"name"`
SerIp string `json:"ip"`
SerPort uint16 `json:"port"`
}
func main() {
server := new(ServerInfo)
server.SerName = "http-nginx"
server.SerIp = "127.0.0.1"
server.SerPort = 8080
re,err := json.Marshal(server)
if nil != err {
fmt.Println("error: ", err.Error())
} else {
fmt.Println("struct json string: ", string(re))
}
}
輸出
struct json string: {"name":"http-nginx","ip":"10.100.17.27:30001","port":8080}
map json strin
go 特色語法
_
- _ 變數
這就好比是Linux 裡的 /dev/null
, 由於go語言要求宣告的變數必須被使用,返回的變數必須被接收,那麼真有個變數沒用但必須要接受怎麼辦呢,就把返回的引數給他。例如:
package main
import "fmt"
var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
func main() {
for _, v := range pow {
fmt.Printf("value is %d\n", v)
}
}
這裡我們只要值,不要key的資訊,返回的key不能不收不是,但我也不像把它輸出出來,就讓 _
來接收好了。
- _ 包
引入包, 並不直接使用這個包,執行時執行一次它的 init()
函式,
import (
_ "github.com/go-sql-driver/mysql"
"github.com/jinzhu/gorm"
)
參考
- www.runoob.com/go/go-operators.htm...
- www.imooc.com/learn/968
- c.biancheng.net/view/21.html
- www.imooc.com/learn/1163
- juejin.im/post/6844904166192611335
- chai2010.cn/advanced-go-programmin...
本作品採用《CC 協議》,轉載必須註明作者和本文連結