本篇會透過 io.Reader 為例,用該介面來接收不同型別的值,看看實際運用效果。
以下範例會寫出兩個任務相同的函式,用來解碼筆JSON格式文字,但這兩個函式的參數型別不同,
package main
import (
"encoding/json"
"fmt"
"io"
"os"
"strings"
)
type Person struct { //用於json資料的結構
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
s := `{"Name":"Joe", "Age":18}` //第一筆資料
s2 := `{"Name":"Jane", "Age":21}` //第二筆資料
p, err := loadPerson(s)
if err != nil {
fmt.Println(err)
}
fmt.Println(p)
//第二筆資料
// strings.NewReader() 會回傳一個 string.Reader的結構,符合 io.Reader 介面
p2, err := loadPerson2(strings.NewReader(s2))
if err != nil {
fmt.Println(err)
}
fmt.Println(p2)
//第三筆資料
f, err := os.Open("data.json") //開啟同資料夾下的文字檔
if err != nil {
fmt.Println(err)
}
p3, err := loadPerson2(f)
if err != nil {
fmt.Println(err)
}
fmt.Println(p3)
}
// 第一個Json 解析函式,接收字串參數
func loadPerson(s string) (Person, error) {
var p Person
err := json.NewDecoder(strings.NewReader(s)).Decode(&p)
if err != nil {
return p, err
}
return p, nil
}
// 第二個 Json 解析函式,接收io.Reader 介面參數
func loadPerson2(r io.Reader) (Person, error) {
var p Person
err := json.NewDecoder(r).Decode(&p)
if err != nil {
return p, err
}
return p, err
}
json 套件的 NewDecoder() 能夠解析 JSON 資料,他實際上會接收一個io.Reader介面參數,並回傳解碼過的Decoder結構 :
//https://golang.org/pkg/encoding/json/#NewDecoder
func NewDecoder(r io.Reader) *Decoder
然後程式會直接呼叫Decoder的Decode()方法,將資料寫入結構變數的個各欄位。
上面的範例,函式 loadPerson(),會接收一個 string 型別的引數,再將其用 strings.NewReader(),把字串轉成 string.Reader 結構傳給json.NewDecoder(); string.Reader 就是實作了 io.Reader 介面的結構型別。至於在功能完全相同的函式 loadPerson2()中,我們直接接收一個io.Reader介面參數,然後一樣的流程。
有關 io.Reader 介面定義 :
//https://golang.org/pkg/encoding/json/#Reader
type Reader interface {
Read(p []byte) (n int, err error)
}
而在看看 strings.Reader 和 os.File 定義會發現他們都實作了這個方法:
:
strings.Reader
//https://golang.org/pkg/strings/#Reader
func (r *Reader) Read(b []byte) (n int, err error)
os.File
//https://golang.org/pkg/os/#File
func (r *File) Read(b []byte) (n int, err error)
所以也就是這樣,就解釋了為什麼函式裡的 json.NewDecoder()能接收這些不同的值。
之後當我們在開發API時,使用介面型別作為參數,就意味著使用者傳入的參數資料不會受限於特定型別,可以更加彈性打造物件,只要他們符合規範就好。
func someFunc() Speaker() { // 傳回值是 Speaker 介面
// 程式碼
}
範例說明 :
任何型別只要實作 Error() string 方法,就符合Go語言error介面,事實上每個套件都會定義自己的error,以下我們來看不同套件回傳的error值實際上是什麼型別。
package main
import (
"encoding/json"
"fmt"
"os"
)
type Person struct { //用於json資料的結構
Name string `json:"name"`
Age string `json:"age"` //故意把欄位型別改錯
}
func main() {
p, err := loadPerson("data.json") //開啟同資料夾下的文字檔
if err != nil {
fmt.Printf("%v", err)
fmt.Printf("%T", err)
}
fmt.Println(p)
}
// 第一個Json 解析函式,接收字串參數
func loadPerson(fname string) (Person, error) {
var p Person
f, err := os.Open(fname)
if err != nil {
return p, err //傳回檔案開啟錯誤
}
err = json.NewDecoder(f).Decode(&p)
if err != nil {
return p, err //傳回json解析錯誤
}
return p, nil
}
結果 :
loadPerson()接收了一個檔,並同時完成讀取和解析JSON檔,但程式中Person結構中Age使用錯誤型別,因此會回傳JSON解析錯誤(*json.UnmarchalTypeError 型別)
data.json改為data1.json,但沒有這個檔案,因此會回傳錯誤(*fs.PathError 型別)
open data1.json: The system cannot find the file specified.*fs.PathError{ }
這顯示Go透過 error 介面傳回不同型別的錯誤。
分為兩派:
accept interfaces, return structs.
接受介面,返回結構,代表的是接收介面能增加使用者的實作彈性,但傳回值就不該如此,原因為使用者有可能得作額外的型別判斷,花時間查詢文件,才能了解不同欄位及行為差異。
不過另一派抱持相反意見,他們認為API將來可以修改其傳回型別,只要其行為符合介面即可,反而具備更大開發彈性。
實際上,我們還是視情況而定,以下列出一些可以協助我們判斷是否要是用介面作為傳回值的考量點 :
沒有任何方法集合的介面。
interface{}
我們知道在Go中介面實作是隱性的,而空介面又未指定任何方法,這就意味著 :
Go語言的任何型別都會自動實現空介面,也就是任何型別都能滿足空介面的規範。
這個範例演示函式無如何透過空介面來接收任意型別。
package main
import (
"fmt"
)
type cat struct {
name string
}
func main() {
i := 99
b := false
str := "test"
c := cat{name: "oreo"}
printDetails(i, b, str, c)
}
func printDetails(data ...interface{}) { //接收數量不定
for _, i := range data {
fmt.Printf("%v, %T\n", i, i) //印出質和型別
}
}
printDetails() 接受不定數量參數 data(成為一個切片),其型別為空介面型別,又剛剛提到任何型別都會自動實作了interface{},於是都可以傳入。
以上就是本篇內容~