老虞要學GoLang-函數(上)


 

不可或缺的函數,在Go中定義函數的方式如下:

func (p myType ) funcName ( a, b int , c string ) ( r , s int ) {
    return
}

通過函數定義,我們可以看到Go中函數和其他語言中的共性和特性

共性

  • 關鍵字——func
  • 方法名——funcName
  • 入參——— a,b int,b string
  • 返回值—— r,s int
  • 函數體—— {}

特性

Go中函數的特性是非常酷的,給我們帶來不一樣的編程體驗。

為特定類型定義函數,即為類型對象定義方法

在Go中通過給函數標明所屬類型,來給該類型定義方法,上面的 p myType 即表示給myType聲明了一個方法, p myType 不是必須的。如果沒有,則純粹是一個函數,通過包名稱訪問。packageName.funcationName

如:

//定義新的類型double,主要目的是給float64類型擴充方法
type double float64

//判斷a是否等於b
func (a double) IsEqual(b double) bool {
    var r = a - b
    if r == 0.0 {
        return true
    } else if r < 0.0 {
        return r > -0.0001
    }
    return r < 0.0001
}

//判斷a是否等於b
func IsEqual(a, b float64) bool {
    var r = a - b
    if r == 0.0 {
        return true
    } else if r < 0.0 {
        return r > -0.0001
    }
    return r < 0.0001
}

func main() {
    var a double = 1.999999
    var b double = 1.9999998
    fmt.Println(a.IsEqual(b))
    fmt.Println(a.IsEqual(3))
    fmt.Println( IsEqual( (float64)(a), (float64)(b) ) )

}

上述示例為 float64 基本類型擴充了方法IsEqual,該方法主要是解決精度問題。 其方法調用方式為: a.IsEqual(double) ,如果不擴充方法,我們只能使用函數IsEqual(a, b float64)

入參中,如果連續的參數類型一致,則可以省略連續多個參數的類型,只保留最后一個類型聲明。

func IsEqual(a, b float64) bool 這個方法就只保留了一個類型聲明,此時入參a和b均是float64數據類型。 這樣也是可以的: func IsEqual(a, b float64, accuracy int) bool

變參:入參支持變參,即可接受不確定數量的同一類型的參數

func Sum(args ...int) 參數args是的slice,其元素類型為int 。經常使用的fmt.Printf就是一個接受任意個數參數的函數 fmt.Printf(format string, args ...interface{})

支持多返回值

前面我們定義函數時返回值有兩個r,s 。這是非常有用的,我在寫C#代碼時,常常為了從已有函數中獲得更多的信息,需要修改函數簽名,使用out ,ref 等方式去獲得更多返回結果。而現在使用Go時則很簡單,直接在返回值后面添加返回參數即可。

如,在C#中一個字符串轉換為int類型時邏輯代碼

int v=0; 
if ( int.TryPase("123456",out v) )
{
    //code
}

而在Go中,則可以這樣實現,邏輯精簡而明確

if v,isOk :=int.TryPase("123456") ; isOk {
    //code
}

同時在Go中很多函數充分利用了多返回值

  • func (file *File) Write(b []byte) (n int, err error)
  • func Sincos(x float64) (sin, cos float64)

那么如果我只需要某一個返回值,而不關心其他返回值的話,我該如何辦呢? 這時可以簡單的使用符號下划線”_“ 來忽略不關心的返回值。如:

_, cos = math.Sincos(3.1415) //只需要cos計算的值

命名返回值

前面我們說了函數可以有多個返回值,這里我還要說的是,在函數定義時可以給所有的返回值分別命名,這樣就能在函數中任意位置給不同返回值復制,而不需要在return語句中才指定返回值。同時也能增強可讀性,也提高godoc所生成文檔的可讀性

如果不支持命名返回值,我可能會是這樣做的

func ReadFull(r Reader, buf []byte) (int, error) {
    var n int
    var err error

    for len(buf) > 0  {
        var nr int
        nr, err = r.Read(buf) 
        n += nr
        if err !=nil {
            return n,err
        }
        buf = buf[nr:]
    }
    return n,err
}

但支持給返回值命名后,實際上就是省略了變量的聲明,return時無需寫成return n,err 而是將直接將值返回

func ReadFull(r Reader, buf []byte) (n int, err error) {
    for len(buf) > 0 && err == nil {
        var nr int
        nr, err = r.Read(buf)
        n += nr
        buf = buf[nr:]
    }
    return
}

函數也是“值”

和Go中其他東西一樣,函數也是值,這樣就可以聲明一個函數類型的變量,將函數作為參數傳遞。

聲明函數為值的變量(匿名函數:可賦值個變量,也可直接執行)

//賦值
fc := func(msg string) {
    fmt.Println("you say :", msg)
}
fmt.Printf("%T \n", fc)
fc("hello,my love")
//直接執行
func(msg string) {
    fmt.Println("say :", msg)
}("I love to code")

輸出結果如下,這里表明fc 的類型為:func(string)

func(string) 
you say : hello,my love
say : I love to code

將函數作為入參(回調函數),能帶來便利。如日志處理,為了統一處理,將信息均通過指定函數去記錄日志,且是否記錄日志還有開關

func Log(title string, getMsg func() string) {
    //如果開啟日志記錄,則記錄日志
    if true {
        fmt.Println(title, ":", getMsg())
    }
}
//---------調用--------------
count := 0
msg := func() string {
    count++
    return "您沒有即使提醒我,已觸犯法律"
}
Log("error", msg)
Log("warring", msg)
Log("info", msg)
fmt.Println(count)

這里輸出結果如下,count 也發生了變化

error : 您沒有即使提醒我,已觸犯法律
warring : 您沒有即使提醒我,已觸犯法律
info : 您沒有即使提醒我,已觸犯法律
3

函數也是“類型”

你有沒有注意到上面示例中的 fc := func(msg string)... ,既然匿名函數可以賦值給一個變量,同時我們經常這樣給int賦值 value := 2 ,是否我們可以聲明func(string) 類型 呢,當然是可以的。

//一個記錄日志的類型:func(string)
type saveLog func(msg string)

//將字符串轉換為int64,如果轉換失敗調用saveLog
func stringToInt(s string, log saveLog) int64 {

    if value, err := strconv.ParseInt(s, 0, 0); err != nil {
        log(err.Error())
        return 0
    } else {
        return value
    }
}

//記錄日志消息的具體實現
func myLog(msg string) {
    fmt.Println("Find Error:", msg)
}

func main() {
    stringToInt("123", myLog) //轉換時將調用mylog記錄日志
    stringToInt("s", myLog)
}

這里我們定義了一個類型,專門用作記錄日志的標准接口。在stringToInt函數中如果轉換失敗則調用我自己定義的接口函數進行日志處理,至於最終執行的哪個函數,則無需關心。

defer 延遲函數

defer 又是一個創新,它的作用是:延遲執行,在聲明時不會立即執行,而是在函數return后時按照后進先出的原則依次執行每一個defer。這樣帶來的好處是,能確保我們定義的函數能百分之百能夠被執行到,這樣就能做很多我們想做的事,如釋放資源,清理數據,記錄日志等

這里我們重點來說明下defer的執行順序

func deferFunc() int {
    index := 0

    fc := func() {

        fmt.Println(index, "匿名函數1")
        index++

        defer func() {
            fmt.Println(index, "匿名函數1-1")
            index++
        }()
    }

    defer func() {
        fmt.Println(index, "匿名函數2")
        index++
    }()

    defer fc()

    return func() int {
        fmt.Println(index, "匿名函數3")
        index++
        return index
    }()
}

func main() {
    deferFunc()
}

這里輸出結果如下,

0 匿名函數3
1 匿名函數1
2 匿名函數1-1
3 匿名函數2

有如下結論:

  • defer 是在執行完return 后執行
  • defer 后進先執行

另外,我們常使用defer去關閉IO,在正常打開文件后,就立刻聲明一個defer,這樣就不會忘記關閉文件,也能保證在出現異常等不可預料的情況下也能關閉文件。而不像其他語言:try-catch 或者 using() 方式進行處理。

file , err :=os.Open(file)
if err != nil {
    return err
}
defer file.Close() 
//dosomething with file

后續,我將討論: 作用域、傳值和傳指針 以及 保留函數init(),main()

本筆記中所寫代碼存儲位置:

上篇-控制語句


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM