天天看點

go小咖 第2關 基礎文法

第2關 基礎文法

  • ​​2-1 變量定義​​
  • ​​2-2 内建變量類型​​
  • ​​2-3 常量與枚舉​​
  • ​​2-4 條件語句​​
  • ​​2-5 循環​​
  • ​​2-6 函數​​
  • ​​2-7 指針​​

2-1 變量定義

‌先寫一個 hello world 的程式,好,我們現在來進入我們的代碼來實際操作一下:

package main
import "fmt"

func main(){
    fmt.Println("hello world")
}      

使用fmt這個庫‌‌去print列印,我們運作可以在IDE裡直接運作,也可以去指令行運作。

‌好,接下來我們來示範一下怎麼來定義go語言的變量,‌‌變量的定義變量,我們用​

​var​

​關鍵字說明這是一個變量,比如說a int,‌‌然後s string,我們就定義了一個int和一個string,‌‌那麼大家注意在go語言中變量名是寫在前面的,變量的類型是寫在後面的,‌‌ta為什麼會這樣來設計?‌

‌我們可以回想一下,我們使用其他語言來定義變量的時候,‌‌是先想到變量的名字,還是先想到變量的類型,可能兩種情況都有,但想到名字的情況多一些,‌‌另外把變量的名字寫在前,類型寫在後,在符合變量的定義上會有一定的優勢,‌‌我們以後會慢慢體會到,現在隻要記住它的變量名都是在類型之前的,‌‌好,我們定義完這個變量之後,它就有了初值,我們把它們打出來。當然 a 肯定是0了,s 肯定是空串了,是以我們叫zero value。‌【關注除零錯誤即分母不能為零】

go語言的變量定義了以後,它要求一定是要有一個合理的初始值 zero value,‌‌它不像c語言定義完這個int它的值是不确定的,go語言它都是有一個值的。‌

到這裡代碼進度:

package main
import "fmt"
func variableZeroValue(){
    var a int
    var s string
    fmt.Println(a,s) // 0
    fmt.Printf("%d %q\n",a,s) // 0 ""
}
func main(){
    fmt.Println("hello world") // hello world
    variableZeroValue()
}      

空串它打不出來,空串我們怎麼讓它打出來有一個技巧,Printf格式化列印。

Println隻能跟變量的名字,‌‌那Printf可以跟這個格式。

這個字元串它不叫‌‌百分号s,百分号s就是一個空串,我們叫百分号q‌‌,加了q它就會把引号打出來。‌

‌變量當然可以賦初值‌‌,然後我們來示範一下變量賦初值,代碼如下:

package main
import "fmt"

func variableZeroValue(){
    var a int
    var s string
    fmt.Printf("%d %q\n",a,s) // 0 ""
}

func variableInitialValue(){
    var a,b int = 3,4
    var s string = "abc"
    
    fmt.Println(a,b,s) // 3 4 abc
}

func main(){
    fmt.Println("hello world") // hello world
    variableZeroValue()
    variableInitialValue()
}      

我們這個變量其實它是可以定義多個的,比如說我再定一個b也是int,goland編輯器畫紅線了,因為b‌‌ 它也要賦初值,比如說給個4,這樣就對了,它還是畫紅線,為什麼?

go語言非常的嚴格,‌‌它的變量一旦定義了,我就一定要用到,不用是不行的,‌‌在我們練習的時候可能會覺得有點束縛手腳,但是真正我們做一個項目的時候,‌‌能夠有效的避免我們定義了很多這種無用的變量,有的時候還要去維護它們,我們把b用起來,這樣就對‌‌當然我們同時定義多個變量的時候,不賦初值也行,賦初值也行,這都可以。‌

‌我們再來‌‌ variableInitialValue(),輸出結果見注釋。‌‌

我們仔細看這個定義,其實我們想一下的話, int‌‌是多餘的, string是多餘的,編譯器完全可以從後面的3 , 4 或者 abc 判斷出來,‌‌在早期的c和c加加是不能的,但是後來的c加加它也有auto關鍵字它也能夠判斷出來,‌‌其他語言中更是能夠判斷出來。‌

‌是以go語言它也同樣的可以省略 type的名字,‌‌它可以推斷我們 type。

怎麼推斷?‌‌這個int我們可以不用,string也可以不用,我們不一定是一行都是‌‌同樣的類型,比如說一個布爾的true,這都可以。‌

‌像上面我們規定的類型,它就不可以寫在一行,但我們不規定類型,‌‌那就是可以寫在一行,這樣也能運作。

// ... 省略其他代碼
func variableTypeDeduction(){
    var a,b,c,s = 3,4,true,"def"
    fmt.Println(a,b,c,s) // 3 4 true def
}
func main(){
    // ... 省略其他代碼
    variableTypeDeduction()
}
‌      

‌這樣寫‌‌ var a,b,c,s = 3,4,true,“def” a比較的繁瑣,

// ... 省略其他代碼
func variableShorter(){
    a,b,c,s := 3,4,true,"def"
    b = 5
    fmt.Println(a,b,c,s) // 3 5 true def
}      

我們還有一種更加簡單的寫法,我們省略var,我們用冒号等于,‌‌我們用冒号等于它的效果和上面的用法是一樣的,冒号等于就是定義一個變量,‌‌那定義完了之後我們還可以指派,之後的再指派,我們就不可以用冒号了,比如說我們的b=5,‌‌不能再說b:=5,那就重複定義變量了,但是我們第一次一定要用冒号,第一次出現的時候不用冒号,‌‌它會編譯出ta說這些變量沒有定義,這個大家要記住,第一次使用變量的時候,我們要用冒号等于來定義,‌‌是以一般情況下我們這個var可以能就不用,最好定義變量就是用這種形式,會簡單一點,輸出結果見注釋,因為後面我們把b變成了5,‌‌我們這些變量的定義,它的作用域是在這個函數裡面,我們在函數外面也能定義變量,‌‌

var aa = 3
var ss = "keagen"
// 錯誤寫法
// name := "keagen"
// 正确寫法
var name = "keagen"      

但是我們在這個函數的外面定義變量,我們不能用冒号等于。比如說name := “keagen”,‌‌這個是不可以的,在函數的外面,它每一行都必須有var關鍵字或者func關鍵字,‌‌或者其他的一些關鍵字來開始,是以我們必須用var name = “keagen”,‌‌我用了var,我們就不能冒号等于。

這些aa,ss,name這些變量它的作用域是什麼?‌

‌它不是一個全局變量,它是一個包内部的變量,‌‌我看到go語言所有的檔案都是在包裡面的,它沒有全局變量這樣的一個說法,那麼作用于隻是包内部。‌

‌好,這樣一行一行的var比較啰嗦,我們可以這樣把它們放在一個括号裡面:

var (
    aa = 3 
    ss = "keagen"
    name = "CSDN代碼寫注釋"
)

func main(){
    // ... 省略其他代碼
    fmt.Println(aa,ss,name) // 3 keagen CSDN代碼寫注釋
}      

go可以用括号的‌‌來省略我們不斷的寫var,我們最後再加一行列印語句。

回顧變量的定義:

‌首先我們使用var關鍵字,比如說var a,b,c bool,‌‌

或者 var s1,s2 string = “hello”,“沒關系”

var關鍵字後面可以等于變量的初值,而且初值可以一下子賦很多個(跟聲明的變量個數對應啦~),‌‌

使用var關鍵字可以放在函數内或者直接放在包内,包内就是看上去像全局變量,但其實不是‌‌。

不管放在函數内還是放在包内,我們都可以用var括号集中的定義,它們放在一塊裡面,‌‌

然後編譯器可以自動的決定類型,‌‌比如說這裡我們很多各種類型的變量混在一起,使用冒号等于能夠寫得短一點,我們的原則能夠寫得短一點,就盡量把它寫得短一點,‌‌是以能用冒号等于我們盡量的去使用冒号等于,但是冒号等于隻能在函數内使用,‌‌它在包内也就是函數的外面是不能用的。‌

2-2 内建變量類型

這裡我們來做一件事情,‌‌這件事情我們學習任何一門語言一上來都要做,我們看看它的内建變量有哪些類型?‌

‌布爾bool和string,‌‌這個不多說了,

然後是整數類型,

(u)int

(u)int8

(u)int16

(u)int32

(u)int64

uintptr

那整數‌‌它每種整數前面可以加個u,加u就是無符号整數,那不加油u就是有符号整數,有符号整數它還分兩類,‌‌一類是規定長度的,比如說int8、int32、int64,還有一類是不規定長度的,‌‌不規定長度的話,它的長度是多少?‌

‌它跟着作業系統來,‌‌在32位系統裡面,它32位,在64位系統,它是64位的‌‌,go語言,它沒有什麼long這些東西,你要長一點,你就說你這個是int64就可以了。‌

‌最後還有一個叫uintptr,ptr大家注意就是一個指針,‌‌go語言也是有指針的,但go語言的指針比 c語言的指針要友善很多,好用很多了。‌‌指針的長度當然也是跟着作業系統來的。‌

‌接下來‌‌這兩種類型, byte,rune。

byte大家都知道,這個rune是什麼東西呢?‌‌

這個rune是go語言的一個字元型,go語言不叫char叫rune,‌‌因為char隻有一位元組,我們現在都是全球化的應用,它要應對多國語言一位元組的char造成的坑非常的多,‌‌是以我們就用 rune這個rune 就是go語言的char類型。

rune的長度是32位的,‌‌可能有夥伴會說 unicode的不是兩個位元組嗎?

這不一定的,‌‌我們在網際網路上一般我們會用到utf8的編碼,在utf8裡面很多字元都是3位元組的,是以我們采用一個4位元組的int32‌‌來代表 rune。‌

‌byte當然是8位的,rune是32位的,它們跟整數都可以混用的,‌‌對整數來說就是一個别名,

好,下面是浮點數類型,‌‌

float32

float64

complex64

complex128

它也沒有float和double。

float32,float64。它很明确說出來我們的長度‌‌。

後面兩種比較奇怪,complex64,complex128,這complex是個什麼東西?‌‌complex是複數,它有實部和虛部就是那什麼有個根号-1的那個東西,‌‌

是以complex64它的實部和虛部分别是32位,complex128的實部和虛部分别是64位。‌

‌在這裡我們看到這個go語言它所想涉及的範圍除了系統程式設計并發程式設計,‌‌還有我們比較複雜的一些資料的處理,甚至是工程上的一些模組化等等,‌‌它把複數直接就作為了一個内件變量的類型。‌

‌我首先來看一下複數,大家接觸比較少,那麼說到複數類型,

‌這裡我們先回顧一下,既然我們go語言是一個内建類型,我們也看一下它到底是個怎麼樣的數。‌‌

我們的數我們都知道實數,float其實也是代表了一個實數,‌‌實數是我們在數軸上 x軸上的任意點都對應了一個實數,‌‌複數我們就不僅僅是在x軸上的數,而是在一個二維平面裡的數,‌‌這個是怎麼做到的?‌

‌我們很關鍵的一點就是規定了i等于根号負1,根号負1我們知道是不存在的,‌‌它是不存在這樣的實數。‌

‌我們想象了一個數叫i它等于根号負1,i是什麼意思?

i就是我們想出來的數,我們想出來的i以後,我們就可以做很多事情,

比如說我們一個複數‌‌就叫3+4i,

那其中3是它的實部,它的實部是3,‌‌虛數部分叫做虛部是4i。

3+4i這樣一個數,到底是怎樣的一個數呢?‌‌

go小咖 第2關 基礎文法

就是這樣,我們沿着實數的數軸走三,‌‌然後4i是豎過來的,我們往上走4格,那3-4i就是往下走4格,‌‌3+4i往上走4格。‌

‌接下來我們看這條斜線長度是多少?5。

5怎麼算出來呢?‌‌

go小咖 第2關 基礎文法

我們也是用絕對值符号,3+4i我們用這個絕對值符号,當然我們通常叫做模不叫絕對值,‌‌3+4i的模等于3平方加4平方再開根号,這個勾股定理出來就是5。‌

‌那麼 i‌‌它非常有意思,大家看這樣一系列的結果,

go小咖 第2關 基礎文法

i的平方等于多少?等于-1,‌‌因為i等于根号-1,是以i平方=-1,i的3次方的負i就是-1乘i,i的4次方就是兩個i平方就是2個-1就是1,那i的5次方又是i,i的6次方又是-1,我們看到‌‌乘一個i就是逆時針轉90度,那麼5乘i就是5,沿逆時針轉90度就是5i。

這就是複數。‌

‌‌

‌接下來我們還有說到類型,我們總要強制類型轉換,‌‌go語言隻有強制類型轉換,它沒隐式類型轉換。‌‌

我們再來舉一個例子來說明這什麼意思?‌

‌比如說我說a,b是兩個整數,它們分别是3,4,

var a,b int = 3,4

go小咖 第2關 基礎文法

我搭一個直角三角形,我想求斜邊5,我們來寫一段小代碼,把5求出來,‌‌

代碼如下:

func triangle(){
    var a,b int = 3,4
    var c int
    c = int(math.Sqrt(float64(a*a + b*b)))
    fmt.Println(c) // 5
}      
go小咖 第2關 基礎文法

但是它編譯錯誤了,因為‌‌類型轉換是強制的, Sqrt我們看到ta要的是一個float64,但是傳進來的‌‌這是什麼東西?這是一個int,它int不能是隐式的轉成float64,我們必須強制轉,‌‌強制轉就像構造函數一樣寫。‌

go小咖 第2關 基礎文法
go小咖 第2關 基礎文法

‌我們說float64後面的東西它還是出錯,‌‌ Sqrt這個參數是float64,它算出來還是float64,但是我們c是int,‌‌int直接等于float64它等不了,其他語言它都是可以等的,它會隐式的轉成 int,‌‌但是go語言不行,它比較嚴格,我們必須顯示的轉int,這樣才可以‌‌。

即​

​c = int(math.Sqrt(float64(a*a + b*b)))​

這個我們練習的時候可能會覺得煩,‌‌但是在我們一個大型的系統中,我們最終是會感謝編譯器做了這樣一些‌‌嚴格的規定,但是這裡還有一個小的瑕疵,因為我們之前說到 float都是不準,浮點數它在任何語言中都是不準的,這裡有可能算出來是四點九九九幾,‌‌4.999轉int,它轉出來是4,它不是5‌‌。

3,4它正好沒有遇到這個問題,如果這個數大一點的話,有可能會出現轉出來不對的情況。‌

2-3 常量與枚舉

‌好,接下來我們看完了變量,我們來看常量,go語言的常量定義比較有特點,我們來看一下。

const就是常量,‌‌比如說我們const,filename等于abc點txt,‌‌或者

const a,b = 3,4

我們就定義了一些常量,‌‌這些常量我們可以規定這個類型,比如說這個是int,

const a,b int = 3,4

我們同樣也可以不規定‌‌。

不規定類型那a,b是什麼類型嘞?它不确定的。‌

‌同樣來算 c。

我們 c‌‌ 它在算的時候我們這裡就不用轉float,常量隻是相當于一個像文本替換一樣的動作,‌‌(見上一小節截圖的example)

如果a,b我們不定義它的類型,那麼它可以做int,它又可以做float,

‌當然我們如果說了ta們是int,我們就要轉,不說ta們就是一個文本,列印一個filename再列印一個 c‌‌。

這個常量當然它是可以定義在這個函數外面,也就是包内部的,當然也是可以的,‌‌定義在包内就是所有的函數都能用。

同樣‌‌我們這個常量它也能夠用括起來,表示一組常量,它定義在一個組裡面,‌‌這樣可以省一些代碼。‌

func consts(){
    const (
        filename = "abc.txt"
        a,b = 3,4
    )
    var c int
    c = int(math.Sqrt(a*a + b*b))
    fmt.Println(filename,c) // 輸出:abc.txt 5
}
func main(){
    // ... 略去部分代碼
    consts()
}      

‌大家注意我們常量的名字,‌‌我沒有全部大寫,其他語言我們都會全部大寫來表示常量,go語言我們要全部大寫,當然也可以,‌‌但是我們一般在go語言中的常量不會去把它全部大寫,因為go語言的大小寫它是有含義的,‌‌首字母大寫它代表public,我以後會講到,是以在這裡我們先不要一口氣把它全部大寫,‌‌就像普通變量一樣的命名規則就可以了。‌

‌好,這就是常量,比如說 filename、abc,‌‌然後常量的數值可以作為各種類型使用,它就好像是在編譯的時候直接把這個數字換過去,‌‌是以const a,b = 3,4以後,我們就不用把a,b強制轉化成float,ta自己會當做float來用,‌‌當然我們要規定類型肯定是可以的,常量當然也可以用括起來作為合在一起定義。‌

‌接下來我們來講一種特殊的常量,枚舉類型,好,那麼我們來定義一些‌‌枚舉類型,go語言沒有特殊的枚舉關鍵字,我們一般就是用const來表示一組常量的定義。‌

‌比如說我們的一些語言,比如說c加加,Java,‌‌Python,然後Golang,這裡它畫了紅線,因為const我一定要給它值,比如說等于0,‌‌

go小咖 第2關 基礎文法
go小咖 第2關 基礎文法

這就是go語言的枚舉類型,一般我們直接定義成const就可以了。‌

func enums(){
    const(
        cpp = 0
        java = 1
        python = 2
        golang = 3
    )
    fmt.Println(cpp,java,python,golang) // 0 1 2 3
}
func main(){
    // ...
    enums()
}      

‌這一組 const 組在一起也能當做一個枚舉類型來使用。‌‌go語言為我們這種一組const的定義方法做了一個簡化,大家看有一個元素叫做iota‌‌,表示這組const是自增值的,我們用了iota之後,我們再看看它們的值是多少呢?‌‌

func enums(){
    const(
        cpp = iota
        java
        python
        golang

    )
    fmt.Println(cpp,java,python,golang)
    // 0 1 2 3
}      

也是0 1 2 3,那是對的,有了自增值以後接下去就好做,

func enums(){
    const(
        cpp = iota
        _
        python
        golang
        javascript
    )
    fmt.Println(cpp,javascript,python,golang)
    // 0 4 2 3
}      

比如說JavaScript,‌‌然後Java我不要了,這樣也可以,大家看看0 4 2 3,因為JavaScript是4‌‌,中間 Java被跳掉了。‌

‌我們有了 iota,它其實是個表達式,我們還可以有‌‌更加複雜的用法。‌

‌我們想定義一些這樣的常量,比如說b,kb,mb,gb,tb,pb‌‌。

我們看看這些變量我們怎麼定義,我還是用iota,iota還可以參與運算,比如說b等于左移多少位?10×iota位,然後就可以了。‌‌

後面我隻要照抄,照抄的話,它們都會使用這個公式。

func enums(){
    const (
        b = 1 << (10 * iota)
        kb
        mb
        gb
        tb
        pb
    )
    fmt.Println(b,kb,mb,gb,tb,pb)
    // 1 1024 1048576 1073741824 1099511627776 1125899906842624
    // 2的10*0次方 2的10*1次方 2的10*2次方 ...
}
// ...      

‌好,我們來看一下,

一個位元組,

一k,

一兆,‌‌

然後一gb ,什麼tb pb它就出來了,‌‌

這個iota可以作為一個自增值的種子,隻要你寫得出這個表達式,你這些值就能夠定義出來。‌

‌好,這個就是go語言的一個枚舉類型,‌‌它通過了一個const塊來定義,然後這塊裡面可以通過iota實作一個自增值,‌‌這就是我們做的普通枚舉類型和自增值的枚舉類型,我們來看看變量定義的一個要點,go語言它是反的,‌‌變量類型寫在後面,變量名寫在前面,編譯器是可以推測變量的類型,它沒有char隻有rune,這個rune是32位的。‌

而且它也比較特别‌‌,原生支援的一個複數的類型。

2-4 條件語句

來看這段代碼:

func bounded(v int)int{
    if v > 100{
        return 100
    }else if v < 0{
        return 0
    }else {
        return v
    }
}      

‌‌大家看看文法有什麼特點, if的條件裡它是不需要括号的,我們來實際操作一下。‌

代碼如下:

package main

import (
    "fmt"
    "io/ioutil"
)

func main(){
    const filename = "abc.txt"
    contents,err := ioutil.ReadFile(filename)
    if err != nil{
        fmt.Println(err)
    }else{
        fmt.Printf("%s\n",contents)
    }
    
}      

‌我們再來建一個檔案,‌‌要不然去我們先定義一個filename,abc點txt,‌‌我們想把這個檔案裡的内容讀出來,我們有一個庫函數叫ioutil點ReadFile,‌‌

然後傳入filename,這個函數傳回什麼東西?

go小咖 第2關 基礎文法

它傳回了兩個值,‌‌go語言的函數是可以傳回兩個值的,第一個中括号byte,‌‌就是一個byte,數字其實就是檔案的内容。‌

‌第二個 error是它的一個出錯資訊,‌‌這個檔案有可能是讀不到的,我們來接一下參數。

既然它傳回了兩個,我們就要用兩個變量來接,‌‌第一個我們就叫它contents,然後第二個叫它err,

那麼我們err不等于nil,‌‌如果出錯怎麼辦?我們去fmt點Println,把這個err給打出來。

否則我們去print檔案,print檔案用百分号s換行,然後 contents,‌‌contents是一個byte數組,是以我們用百分号s能夠打出它的内容來,我們來運作一下試試看。‌

報錯:open abc.txt: no such file or directory

go小咖 第2關 基礎文法

解決方法:

go小咖 第2關 基礎文法

運作結果:

go小咖 第2關 基礎文法

abc.txt:

go小咖 第2關 基礎文法

ta就把這個檔案内容打出來,這個 if的使用‌‌這樣寫得比較的麻煩。

go語言的if可以像其他語言的for一樣寫,給大家看看,‌‌if後面可以跟個分号,然後再是err不等于nil,因為我們把fmt.Println(err)這行可以放到裡面去,‌‌要是再是print檔案的内容。‌

‌它先運作前半句,‌‌運作完了就有 err,然後再判斷err是不是空,如果不空的話說明有錯誤,出錯了,‌‌否則就打出檔案内容。

我們來試一下,

package main

import (
    "fmt"
    "io/ioutil"
)

func main(){
    const filename = "abc.txt"
    if contents,err := ioutil.ReadFile(filename);err != nil{
        fmt.Println(err)
    }else{
        fmt.Printf("%s\n",contents)
    }
    
}      

果然打出來了:

go小咖 第2關 基礎文法

如果我們出了if語句塊之外還能通路 contents或者err嗎?

不能。‌

‌因為我 contents是在if裡面定義的,它生存期隻在if的 block裡才有,這就是我們的 if,‌‌ if後面是可以跟多個語句的條件,你可以指派,賦完值再去判斷它是不是等‌‌條件,你指派的作用域,它就在這個if語句裡面,我出了這個if的block,我們定義的這些變量當然就沒了。‌

if的條件裡,指派的變量的作用域就在這個if語句裡。

‌接下來我們來進入switch,

func eval(a,b int,op string)int{
    var result int
    switch op {
        case "+":
            result = a + b
        case "-":
            result = a - b
        case "*":
            result = a * b
        case "/":
            result = a / b
        default:
        panic("unsupported operator:" + op)
    }
    return result
}      

這個是一個很簡單的 switch,我們傳進了a,b兩個int,‌‌然後它的operation是一個字元串,然後我們去switch op,它有加減乘除,我們就把它算出來,‌‌

最後 return result‌‌。

大家仔細的來看這段程式‌‌和我們熟悉的語言比有哪些不同,我們不需要加break,它預設是每個case後面都有break的,‌‌你要不break反而你要用fallthrough,這個其實是非常人性化的。‌

‌我們用c語言,‌‌我們每個case後面都要打break,打得非常的累,但在go語言中我們就不需要打break,‌‌你什麼都不說,ta就自己會break掉,這個很人性化。‌

‌ default裡面這個panic是什麼意思,這個就是相當于報錯,讓程式停下來,因為我們會有傳不認識的操作符的情況。

我們再來進入代碼,

func grade(score int)string{
    g := ""
    switch {
        case score < 0 || score > 100:
            panic(fmt.Sprintf("wrong error score:%d",score))
        
        case score < 60:
            g = "F"
        
        case score < 80:
            g = "C"
        
        case score < 90:
            g = "B"
        
        case score <= 100:
            g = "A"
        // default:
        // panic(fmt.Sprintf("wrong error score:%d",score))
    }
    return g
}      

給大家示範另外一種switch的做法。‌

定義一個函數叫做grade的,‌‌我們傳了一個百分制的分數,傳回了一個string,‌‌go語言的定義都是反的,我們函數的名寫在前面,類型寫在後面,這個思路是一樣的。‌

‌我們希望這個分數如果高的話,比如說九十幾分我們傳回a,不及格傳回f,我們用switch去怎麼做呢?‌‌

我們不是switch score,啥都不說,就寫一個switch,‌‌然後case過小于60這樣。

‌我們這grade一開始讓它等于一個空字元串,‌‌當然我們直接return也可以,

這裡為了示範它能夠自動break,當score < 60的時候,我們就把g設成F。‌

default的話我們就報個錯,‌‌ 我們把這個score傳進去,‌‌既然我們想到了判斷錯誤的思考情況,我們還要判斷負數的情況,是以我們寫在這裡,‌‌如果score小于0 || 大于100,這個都是我們要報錯的,你把報錯‌‌給貼它到這裡,它就沒有default,所有情況我們都已經處理了。‌

‌‌我們不是加了panic,panic到底是幹嘛用的?

我們加了panic的用處,‌‌ta就會中斷我們程式的執行,來報個錯。‌

2-5 循環

小試牛刀:

sum := 0
for i:= 1;i<= 100;i++{
    sum += i
}      

for的條件裡不需要加括号;

for的條件裡可以省略初始條件,結束條件,增值表達式。

來看另一段代碼:

功能:把整數轉換成二進制

方法:短除法

條件:餘數是0就算結束

細節:短處之後得到的數倒過來就是正确的二進制

func convertToBin(n int) string{
    result := ""
    for ; n>0;n/=2{
        lsb := n % 2
        result = strconv.Itoa(lsb) + result //strconv.Itoa()轉字元串
    }
    return result
}
func main(){
    fmt.Println(
        convertToBin(9),
        convertToBin(9),
        convertToBin(6),
    )
    // 1001 1001 110
}      

但是lsb我們要去把它(數字)轉成字元串,‌‌這個result是個字元串,lsb是個數,它們不能加,strconv.Itoa()轉字元串,這樣就可以了。

2-6 函數

‌這一節我們來說說go語言的函數的一個定義,‌‌比如說這樣的代碼:

// ... 略
func eval(a,b int,op string)int{
    switch op{
    case "+":
        return a + b
    case "-":
        return a - b
    case "*":
        return a * b
    case "/":
        return a / b // b不能為0
    default:
        panic("unsupport operation:" + op)
    }
}

func main(){
    fmt.Println(eval(996,1,"*")) // 996
}      

說明:

go語言的函數定義和變量的定義思路是一樣的,

函數名在前,類型在後,而且同類型的參數它是可以a逗号b‌‌。‌

如果我們不認識的operation,我們隻能報錯了,‌‌報錯就是panic(“unsupport operation:” + op),‌‌再加上我們報錯的時候要把參數傳進來,這樣調試的時候我們就知道到底傳了什麼不對的東西進去。‌‌

那麼比如說對 996 和1 做乘法,這個結果當然是996了,‌‌這是我們go語言的函數的定義。‌

‌那函數它除了傳回一個int之外,它還可以傳回多個值。‌

比如說做除法,

功能描述:

比如 13 / 4 = 3 ······ 1

餘了一個1,‌‌把它餘數求出來。‌‌

好,我們來實作一下。

func div(a,b int)(int,int){
    return a / b,a % b
}
func main(){
    // ... 略
    fmt.Println(div(13,4)) // 3 1
}      

傳回兩個值就出來了,見注釋。

‌在傳回多值的情況下,我們還可以給傳回值起一個名字。‌

‌比如說我這裡如果沒有注釋的話,我們光看int,我們也不知道它傳回什麼東西:

func div(a,b int)(int,int){…}

我們說q,r int,q就是商,r就是餘數,

func div(a,b int)(q,r int)

我們取了名字之後,‌‌這個還是a除b,a模b一樣,但是取了名字之後外面就可以好用了。‌

‌比如說q,r := div(13,4),‌‌我們讓編輯器去自動生成它的傳回值,

編輯器就自動給它們起名為q,r:

go小咖 第2關 基礎文法

q,r它起的是這個函數體裡面的名字,‌‌對于我們接傳回值的值,其實我們無所謂叫什麼,我們叫a,b也可以的,它都是可以的,‌‌但是既然我們函數體裡面的名字叫q,r,我們寫的時候也保持一緻。‌

func div(a,b int)(q,r int){
    q = a / b
    r = a % b
    return 
}
func main(){
    q,r := div(13,4) // 3 1
}      

大家注意,像這樣的一個‌‌ return 的方法,如果我的這個函數體比較長,比如說有十幾行的話,看的人他是搞不清楚‌‌到底哪一行給q賦了值,哪一行給r賦了值,是以這個函數體隻要一長,‌‌用這種方法就是不太好,看的人會搞不清楚。‌

‌是以我們比較建議的還是這樣的方法:

func div(a,b int)(q,r int){
    return a / b,a % b
}      

‌我們把 return 的 value 非常明顯的寫出來,傳回兩個值,‌‌我們到底怎麼來收這兩個傳回值?

我們用逗号隔開兩個參數,然後用冒号等于來定義它們:

q,r := div(13,4)

這樣我們就可以把兩個傳回值能收到,如果我們隻想收一個傳回值怎麼辦?

看代碼:

func eval(a,b int,op string)int{
    switch op {
    case "+":
        return a + b
    case "-":
        return a - b
    case "*":
        return a * b
    case "/":
        q,_ := div(a,b)
        return q
        //return a / b
    default:
        panic("unsupport operation:" + op)
    }
}      

因為div函數傳回的是兩個值,eval函數隻能傳回一個int,‌‌這當然不行,我們要傳回這兩個值裡面的第一個。‌

‌你可能想我們給它去賦個值,‌‌ q,r冒号等于,然後return q,這樣也可以嗎?‌

go小咖 第2關 基礎文法

‌大家發現它編譯錯誤了,它 r‌‌ 你沒有用到,go語言我定義的變量一定要用到,你不用到就是編譯錯誤,它非常嚴格,‌‌我不想用r怎麼辦?用下劃線忽略它,下劃線就說我第二個傳回值我不想要,我隻想要第一個,‌‌這樣就可以達到目的。‌

go小咖 第2關 基礎文法

兩個傳回值除了傳回除法中的商和餘數之外,還有什麼用呢?‌‌

大家記住多傳回值不要亂用。

一般的用法‌‌我們之前也看到過,用來傳回錯誤,比如說

go小咖 第2關 基礎文法

在這裡它會傳回一個byte類型的切片,‌‌一個error。‌

‌‌一般都是這樣用的,傳回一個值,然後再加一個error,如果出錯的話,我們就把error傳回出來。‌

‌我們照這個思路的話,我們這裡是不是不要用panic,panic是‌‌中斷執行。

panic中斷掉了很難看,我們用go語言的思路,‌‌我們去傳回一個int,一個error,

如果成功的話,我們傳回這個數值,如果不成功,‌‌大家去檢查 error,

沒有錯,傳回nil。

func eval(a,b int,op string)(int,error){
    switch op {
    case "+":
        return a + b,nil
    case "-":
        return a - b,nil
    case "*":
        return a * b,nil
    case "/":
        q,_ := div(a,b)
        return q,nil
        //return a / b
    default:
        return 0,fmt.Errorf(
            "unsupport operation:%s", op)
    }
}
func main(){
    fmt.Println(eval(3,4,"x")) // 0 unsupport operation:x
}      

來到default,我們就return一個0,但是我們error要說一句話,‌‌這句話怎麼說呢?

fmt點Errorf,我們看一下結果是:

0 unsupport operation:x

我們外面如果要判斷這個錯的話,我們之前也寫過,‌‌if result逗号error,冒号等于eval(3,4,“x”),然後動作叫x,然後分号error不等于nil,不等于nil就是出錯了。‌‌

為啥?

error為空,就是沒有異常,

有異常,error就不為空。

func main(){
    if result,err := eval(3,4,"x");err!=nil{
        fmt.Println("Error:",err)
    }else{
        fmt.Println(result)
    }
}      

‌‌這裡我們示範了兩個傳回值的一個常用的場景,就用來return error的情況,‌‌如果出錯的話,我們就把錯誤資訊進行相應的一個處理。‌

‌這裡我們說到了 div 函數,它可以傳回兩個值或者多個值,它可以三個也可以‌‌,

而且要傳回多個值時還可以起名字,‌‌但這個起名字大家不要濫用,隻用于非常簡單的函數,

但這個不起名字對于調用者而言沒有差別,調用者可以無論取什麼名字來接這兩個參數都是可以的,‌‌但是它有個提示作用,能提示你這個是q和r,

你如果起不出名字就用q和‌‌r,

多個傳回值,我們通常使用的一個場景會是傳回一個error,第一個值是一個‌‌正常情況下傳回值,第二個值是一個error,然後讓調用者來具體的處理 error。‌

go語言是一個函數式程式設計的語言,‌‌它的函數是一等公民,‌‌是以函數裡面函數的參數,函數的傳回值,甚至函數體内‌‌,它都可以有函數,這個我們來看這段代碼:

// 匿名函數
// op func(int,int)int 相當于 xxx int
func apply(op func(int,int)int,a,b int) int{
    return op(a,b)
}      

‌‌

‌我們把op放在第一個參數,op是一個函數,‌‌它是個怎麼樣的函數?

它有兩個參數,然後傳回是一個int,‌‌然後我們再接兩個參數叫a,b int,它的傳回是一個int,

我們把收到的‌‌a,b兩個參數用于op函數,然後傳回 int,

return op(a,b), op是一個函數,‌‌我們看到這個函數是輸入兩個參數,輸出一個參數 int類型的,這兩個參數來源于誰?

來源于函數apply 的參數a,b‌‌ ,然後它的傳回值是int,它傳回值就是從op這個結果來‌‌。

go語言這種參數名寫在前,類型寫在後的這種寫法,定義複合函數特别的容易。‌

小結:

函數的參數也可以是一個函數,我們可以寫個匿名函數來調用它,

傳回值的類型寫在最後面,先名字再類型,‌‌

go語言的函數可以傳回多個值。

2-7 指針

看看這段代碼:

var a int = 2
var pa *int = &a
*pa = 3
fmt.Println(a)      

pa是一個對a的指針,‌‌星号是代表指針,go語言是星号代表一個指針,‌‌指向int的指針很明确,然後等于 &a 就是等于 a 的位址,星号*pa等于3,這a的值就會被改成3。‌

go語言的指針它簡單在什麼地方?

簡單在它不能運算,‌‌ta不是像C語言一樣拿了個數組的頭指針,然後會不斷的一直加下去,ta不能做加法的,‌‌我一開始指向a,當然我可以指向b,但是我不能說加一,它是不能做的。‌‌

C語言指針的複雜,就因為運算裡面而不知道我指針的size是多少。‌

go語言指針不能運算。

參數傳遞

C語言和C++支援值傳遞和引用傳遞。

什麼是值傳遞?什麼是引用傳遞?

看一段C++的代碼:

// pass_by_val 值傳遞
void pass_by_val(int a){
    a++;
}
// pass_by_ref 引用傳遞
void pass_by_ref(int &a){
    a++;
}
int main(){
    int a= 3;
    
    pass_by_val(a);
    printf("pass_by_val:%d\n",a); // pass_by_val:3
    
    pass_by_ref(a);
    printf("pass_by_ref:%d\n",a); // pass_by_ref:4
}      

運作結果見注釋。

大家看調完了 pass_by_val(a);

雖然 void pass_by_val(int a){…}裡面把a加加,但其實main函數裡面 的 a 沒有變,‌‌

pass_by_val(a);就是所謂的值傳遞,值傳遞它會把a的值‌‌拷一份,

從main函數拷到void pass_by_val(int a){…}裡面去,它是真正的是做了一個拷貝,‌‌我拷貝進去的 a我加了一,那麼我外面main函數裡的 a我還是沒有動,還是3。

‌pass_by_ref(a);它不一樣,它不拷貝, void pass_by_ref(int &a){…}函數裡面的 a和main函數裡的a‌‌其實是ta們引用了同一個變量,變量的值原來是3,‌‌在void pass_by_ref(int &a){…}裡面給它加1,變量的值就變成4,‌‌是以調完了‌pass_by_ref(a);

a的值就變成了4,

這就是值傳遞和引用傳遞。‌

值傳遞它做了一份拷貝,拷貝完了之後,我main函數資料的值就不會變了。‌

‌引用傳遞我不拷貝,但是不拷貝的話後果是main函數裡的這個值,它調用的函數會被它改變掉的。‌

‌那麼c加加是這樣,其他程式設計語言,一般來說所有的參數都是引用傳遞,‌‌除了系統的内建參數以外,比如說Java和Python都是這樣,‌‌基本上我們所有的自定義的類型,它都是引用傳遞。

系統自定義的類型,比如說int它才是值傳遞,‌‌我們看go語言它是使用值傳遞還是引用傳遞?

go語言,它隻有值傳遞一種方式,‌‌我們不用去考慮值傳遞,引用傳遞參數變不變這種問題,‌‌凡是我們調函數,我們的參數都要去拷貝一份,‌‌那麼都要拷貝一份性能是不是就下降了呢?‌

‌但我們有指針,接下來我們就來看看我們值傳遞和指針到底應該怎麼配合。‌‌

說明:左邊是調用函數func,右邊是定義函數func。

go小咖 第2關 基礎文法

比如說我有一個a,它是一個int,我們要傳給function,‌‌function裡面參數也是一個int,值傳遞是怎麼傳的?‌

‌拷過去。

把a拷到function裡面去,‌‌這個function裡面對a的改變,重新指派什麼的,當然不會展現到外面。

我外面調用它的時候是多少,調完還是多少,‌‌因為它是拷過去的,拷過去那份随便你改,副本的變化一輩子都不會影響到原本,老死不相往來,這也是值傳遞的好處,我調了你之後,我傳的a不會被你變掉。

go小咖 第2關 基礎文法

‌再來看第二種傳遞的方法, a是一個int,然後右邊‌‌我是一個function,它要一個int類型的指針,名字叫做pa,我怎麼做的呢?‌‌

左邊調它的函數我有一個a類型,是一個整數,int a的位址即 &a,‌‌它被拷了一遍,拷到了func f 裡面去,但是 &a和 pa 同時都指向了變量a‌‌,

是以我在func f 裡面,我去修改pa所指向的位址裡面的内容,新pa我給它改為 3——>4,ta當然會反映到調它的人裡面去,我在 func f 裡面把這個pa指向另外的人,‌‌它不會影響到調它的人。

‌這就是通過指針的傳遞‌‌來實作了一種相當于引用傳遞的一個效果,我把 a的位址給了你之後,能夠讓你改變我的值。

go小咖 第2關 基礎文法

‌我們再來看第三個例子,我們這個是一個object,object名字叫做cache,它的類型也叫Cache。

我們把 cache 傳給func f,‌‌大家說這樣的一個傳遞,我這個cache裡面應該放了很多的内容是一個緩存,‌‌我能夠做值傳遞嗎?我把整個cache拷一份給你f,這顯然是不可能的,這裡我為什麼沒有取一個位址呢?‌

‌大家看cache的結構,cache一般都是這樣的結構,cache object它本身‌‌不帶有data,它本身通常隻是一個指向data的指針,這data我是放在外面一個很大的一塊記憶體塊的,‌‌cache既然是這樣的結構,我拷貝一份也很正常,我拷貝了一份cache過去,他們同樣也有一個PData,‌‌但這兩個pData指向的是同一份data,‌‌隻是說我這cache結構裡面的指針被拷了一份,它們指向的還是同一份内容,這張圖說明什麼呢?‌

go語言我們的這些自定義的類型,‌‌它在定義的時候我就要考慮到,

我們用它是當做一個指針來用,還是當做一個值來用,‌‌像這裡我們的cache 的結構就是可以作為一個值來用的,‌‌我們可以很安全的被拷過去,然後讓人家來改我們data指針所指向的這塊真正的data,‌‌什麼情況下不能呢?

不能的話,我除了PData,我還要維護一些狀态,比如說我這一共有多少個data,cache 裡面有多少的資料,這樣的話我就不能作為一個值傳遞的類型了,我就要要求用的人‌‌使用一個指針來傳一個cache 指針過去,這兩種用法在go語言裡面都有,

把對象封裝成一個指針類型或者封裝成一個值類型。

交換兩個變量的值。

能交換:

func swap(a,b *int){
    *b,*a = *a,*b
}

func main(){
    a,b = 99,6
    swap(&a,&b)
    fmt.Println(a,b) // 6 99
}      

不能交換:

它沒有傳回值,‌‌怎麼交換,這個不需要定義什麼臨時變量,我們b逗号a 等于a逗号b,

func swap(a,b int){
    b,a = a,b
}
func main(){
    a,b = 99,6
    swap(a,b)
    fmt.Println(a,b) // 99 6
}      

不能這樣用,因為a,b是值傳遞傳過去,你改了沒用,那不行,

是以我們要把a,b定義成指針類型,把它們全變成指針。‌

‌b所指向的内容等于a所指向的内容,a所指向的内容等于b所指向的内容,那這樣就換過來了。‌‌

‌另外還有一種做法,‌‌我們在程式設計中我們更加希望的是一些不可變量,‌‌我們把指針去取位址傳進去,看上去不太舒服,我們可以這樣子,‌‌讓它傳回int,int,把它交換的結果傳回出去,這樣就可以了,

func swap(a,b int)(int,int){
    return b,a
}

func main(){
    a,b = 99,6
    a,b = swap(a,b)
    fmt.Println(a,b) // 6 99
}      

我們要用a,b 去接受傳回值,‌‌

這種swap的定義方法才是更好的。‌‌