iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 13
1

寫在前面

全名golang的GO是Google為了改善實務面問題而開發的語言(這裡指的服務當然是網路服務)
因此在語法上會盡量以工程師開發的角度來設計
所以golang雖然是編譯語言,但相較我們學過的java,C#等等已經簡化了許多
看看java要讀取一段字還要先準備一個scanner(以純物件的角度來看這樣做是完全合理的)
而C#則更像是微軟推出的產品,生態,框架都幫你準備好了(看看init幫我們生出的那堆東西)
只是有時候我們真的不需要那麼多

另外golang還提供了官方的程式風格及工具,因此所有工程師的程式碼風格看起來都一樣(不一樣的話甚至不會讓你編譯)
編譯器甚至還會幫你檢查你的程式有沒有多東西,也就是如果你的程式有多餘的程式碼會提醒你,沒刪掉就不讓你編譯
而且相較於我們之前介紹的其他語言(全都誕生於2000年或以前),golang是屬於晚期的語言(誕生於2009年),
但是他不像晚期的語言就取消了指標(先不要問什麼是指標,很可怕)這個強大的功能,反而將他保留了下來並附於各種函式庫中

另外golang的保留字也相對較少只有25個(C# 97個,javascript 63個,java 50個,C++ 49個,python 33個)
如果要直接用保留字來認識這個語言也是可以並且相對簡單的
而且吉祥物很可愛
golang

先去安裝golang吧
Ready?

go

在你的工作目錄下面建立一個main.go的檔案,然後貼上下面的程式碼

package main

import (
	"fmt"
	"math"
	"strconv"
	"strings"
)

func main() {
	var input string
	fmt.Println("Tell me what you want to do:")
	fmt.Println("(1)T to H (2)H to T")
	fmt.Scanln(&input)
	switch input {
	case "1":
		var number string
		fmt.Println("Please enter the number:")
		fmt.Scanln(&number)
		fmt.Println(t2h(number))
	case "2":
		var number string
		fmt.Println("Please enter the number:")
		fmt.Scanln(&number)	
		fmt.Println(h2t(number))
	default:
		fmt.Println("Wrong selection")
	}
}

func h2t(number string) (output float64){
	inarr := strings.Split(number, "")
	i:=0
	for i < len(inarr) {
		output += AEreturn(inarr[i]) * math.Pow(16, float64(len(inarr)-(i+1)))
		i++
	}
	return
}

func AEreturn(number string) (output float64) {
	num, err := strconv.Atoi(number)
	if err != nil {
		switch number {
		case "A":
			num = 10
		case "B":
			num = 11
		case "C":
			num = 12
		case "D":
			num = 13
		case "E":
			num = 14
		case "F":
			num = 15
		default:
			num = 0
		}
	}
	output = float64(num)
	return
}

func t2h(number string)string {
	tenum, _ := strconv.Atoi(number)
	for i := 0; i < 16; i++ {
		for j := 0; j < 16; j++ {
			for k := 0; k < 16; k++ {
				if int(math.Pow(16, 2))*i+int(math.Pow(16, 1))*j+int(math.Pow(16, 0))*k == tenum {
					return fmt.Sprintf("%s%s%s",returnAE(i),returnAE(j),returnAE(k))
				}
			}
		}
	}
	return ""
}

func returnAE(remain int) string {
	if remain < 10 {
		return strconv.Itoa(remain)
	} else {
		switch remain {
		case 10:
			return "A"
		case 11:
			return "B"
		case 12:
			return "C"
		case 13:
			return "D"
		case 14:
			return "E"
		case 15:
			return "F"
		default:
			return "wrong"
		}
	}
}

golang的行尾是不需要;的,當然你想加也是可以

使用

go run main.go

就可以將你的程式執行起來了

結構

golang最大的結構稱為package(甚至可以橫跨好幾個檔案)
但是一個資料夾內只能有一種package(你可以把package分成多個檔案,但是package名稱必須相同)

宣告的方式如下

package main

所有的方法變數都會包裝在package裡面
(不像C#使用{}來包裝namespace,當package在第一行宣告時,整個檔案都包在這個package裡面,因此基本上是不可能超出去的)

而由於golang屬於編譯式語言,我們必須宣告出程式的進入點
因此當我們執行程式時golang會自動去找工作資料夾內package為main的main方法
也就是這裡

package main
...
func main()

這裡我們建立了一個main的package表示這裡是我們的主程式
而主程式裡面的main方法就是主程式的進入點

package也能當作是一個函式庫
如果你需要在一個專案內引入package可以使用import
以下我們引入的這些函式庫都是一個個由官方提供的package喔

import (
	"fmt"
	"math"
	"strconv"
	"strings"
)

當然,你也可以使用自定義的package,不過使用上會建議直接學go mod幫助你管理package
(當年剛開始學的時候go mod還沒開始流行,每次都要被gopath搞死)
今天我們先不學如何使用package,畢竟只是單純介紹語言

以上就是golang最基本的結構拉(其實import嚴格上不算,不過誰寫程式不會用到函式庫呢?)

  • package
  • import
  • func main()

方法

一般而言golang的main方法是不會有輸入值跟回傳值的

func main() {}

但是其他的副程式可能會有,往下看到這裡

func t2h(number string)string {}

這裡定義了輸入值與回傳值的型別,都是string

golang比較特別的是型別宣告位於變數名稱之後而不是之前

當你不需要輸入值或回傳值的時候就空著就好

golang也能在方法宣告時就把回傳值的變數名稱給定義好
比方說

func h2t(number string) (output float64){ //這裡定義了回傳值的變數名稱與型別
	inarr := strings.Split(number, "")
	for i := 0; i < len(inarr); i++ {
		output += AEreturn(inarr[i]) * math.Pow(16, float64(len(inarr)-(i+1)))
	}
	return //這裡會直接把output回傳
}

相當於

func h2t(number string) float64{ //這裡只定義型別,沒有定義名稱
	var output float64 = 0 //這裡建立了一個準備要回傳的變數
	inarr := strings.Split(number, "")
	for i := 0; i < len(inarr); i++ {
		output += AEreturn(inarr[i]) * math.Pow(16, float64(len(inarr)-(i+1)))
	}
	return output //因此回傳時就需要回傳變數
}

這樣會讓你的程式比較精簡,也能一眼看出回傳值的位置與名稱

接著讓我們回到main方法的內容吧

輸入及輸出

var input string
fmt.Println("Tell me what you want to do:")
fmt.Println("(1)T to H (2)H to T")
fmt.Scanln(&input)

這個就是golang輸入及輸出的方法,都來自這個叫fmt的package
(其實輸出還有其他種方式,不過我們就先介紹這種)

我注意到input之前有一個&, 這是什麼意思?

一般而言我們將變數帶入方法時,是傳遞變數的名稱,但是在這個方法裡面我們需要帶入變數在記憶體的位置
你可以使用

fmt.Println(&input)

看看我們到底丟了什麼進去,下面是類似的結果

0xc0000961e0

這個是你電腦記憶體實際上的位置,所以每個人應該都會不太一樣
給方法記憶體位置之後,方法會根據記憶體的位置將值塞進去而不是根據變數名稱
如果聽不懂也沒關係,之後會有重點介紹指標的章節.那時候會再拿出來討論
現在就先這樣記著就好

邏輯控制

switch input {
	case "1":
		var number string
		fmt.Println("Please enter the number:")
		fmt.Scanln(&number)
		fmt.Println(t2h(number))
	case "2":
		var number string
		fmt.Println("Please enter the number:")
		fmt.Scanln(&number)	
		fmt.Println(h2t(number))
	default:
		fmt.Println("Wrong selection")
}

注意到golang的switch是不需要break的喔

而且不管是if for 還是switch, {都必須放置在第一行
如果你的程式像下面這樣

switch input
{
	//something
}

會直接報錯

另外一點跟C#不一樣的是在golang每個case都是各自獨立的
還記得C#我們的switch這樣寫嗎?

case "1":
    Console.WriteLine("Please enter the number:");
    String number = Console.ReadLine();
    t2h(number);
    break;
case "2":
    Console.WriteLine("Please enter the number:");
    number = Console.ReadLine();
    h2t(number);
    break;

在C#內的case並非完全獨立,在case2你可以使用到case1中宣告出來的變數
但是在golang中如果把case2中的

var number string

給拿掉可是會報錯的喔

型別

func h2t(number string) (output float64){
	inarr := strings.Split(number, "")
	i:=0
	for i < len(inarr) {
		output += AEreturn(inarr[i]) * math.Pow(16, float64(len(inarr)-(i+1)))
		i++
	}
	return
}

注意到這一段

inarr := strings.Split(number, "")

:=是golang提供的語法糖,可以自動幫你推斷型別

否則你需要這樣宣告

var inarr []string = strings.Split(number, "")

如同我說的,golang很注重實務面,因此能省幾個字就盡量省
基本上在方法內也很少有人會真的寫var,畢竟:=太好用了
而下一行的

i:=0

因為沒有小數點,因此自動推斷成整數型別(int)
如果你的宣告長這樣

i:=0.0

那他反而會自動推斷成浮點數型別(floag64)

回到第一行
以往我們都可以直接用number[i]來取出字串中的字元
雖然golang也能這麼做,不過golang的字串要當作陣列來使用相對比較麻煩
因此我們用strings(就是我們上面import的那個)提供的方法將字串給切割成單個字串組成的陣列
Split的用法是會根據第二個參數當作切割點去切割字串,這裡我們選擇給空字串則會把字串割成字串陣列(長度為1)
如果你需要用別的切法也可以這樣寫

inarr := strings.Split("1,2,3,4", ",")//會依照","為區隔切開

切完後:=會自動推斷,後面Split輸出的型別為字串陣列[]string,因此他會自動幫前面的inarr宣告成字串陣列型別

你可以有另外一種把字串當作字元陣列的寫法

func h2t(number string) (output float64){
	i:=0
	for _, char := range number{ //這裡會遍歷number的所有元素(字元),並且放入到char這個變數中,型別為rune
		output += AEreturn(string(char)) * math.Pow(16, float64(len(number)-(i+1)))
		i++
	}
	return
}

不過for range的詳細用法要請各位自己去查拉,這裡就不深入

如果你已經試了的話可能會遇到下面的問題

./TenToHex.go:7:2: imported and not used: "strings"

這是由於golang不允許有宣告或引入的函式庫沒有被使用,
因此你可以把import的strings那行給註解或刪掉,就可以正常執行了

迴圈

接著我們遇到了迴圈

for i < len(inarr) {
	output += AEreturn(inarr[i]) * math.Pow(16, float64(len(inarr)-(i+1)))
	i++
}

while勒? 怎麼只剩水箭龜 怎麼是for?

在golang中,迴圈一律使用for,裡面放的東西跟while一模一樣
所以如果你需要無限迴圈可以這樣寫

for { 
	//something
}

因為沒有條件所以一定符合,然後就會不斷執行

轉型

output += AEreturn(inarr[i]) * math.Pow(16, float64(len(inarr)-(i+1)))

在大部分的語言中,當你使用pow計算時,會自動幫你把輸入的整數型別轉型成double或float型別
但是golang不會這麼做,因此你可以看到我們必須自己將他們轉型

float64(len(inarr)-(i+1))

轉形成float64之後才能放入pow中當參數使用

那為什麼16明明是整數型別卻可以當作pow的參數呢?

理由在於16可以當作是float64或是int,這裡golang自動當作float64了

而型別的理由也讓我們最初宣告要回傳的output時就宣告成float64型別,這樣才能與冪次計算出來的float64型別做計算

錯誤處理

我們往下看AEreturn這個方法吧

func AEreturn(number string) (output float64) {
	num, err := strconv.Atoi(number)
	if err != nil {
		switch number {
		case "A":
			num = 10
		case "B":
			num = 11
		case "C":
			num = 12
		case "D":
			num = 13
		case "E":
			num = 14
		case "F":
			num = 15
		default:
			num = 0
		}
	}
	output = float64(num)
	return
}

再複習一下,這裡方法的輸入為字串,輸出為float64

然後注意到這一行

num, err := strconv.Atoi(number)

strconv也是我們最初有import的函式庫,提供了一個Atoi的方法將字串轉型成整數
接著因為轉型是有可能錯誤的(比方說"A"就沒辦法轉型),因此輸出的回傳值會有兩個
第一個num就是轉型成功的值
第二個err則是錯誤的訊息,型別為error(沒錯,在golang錯誤是有型別的)
如果沒有錯誤的話會回傳nil,也就是空值
因此我們在下一行

if err!=nil

利用確認err裡面是否有東西來確認是否有正確轉型

後續的作法跟之前一樣,轉型成功時就直接丟入num
若是轉型失敗則進入到switch階段,繼續判斷到底是哪一個字串,再根據字串去決定最終的數字

最後在回傳時由於pow需要float64型別,因此我們將int轉型並回傳

output = float64(num)
return

而有時你因為確保輸入值是正確的而不做錯誤處理時你可以像是在t2h裡面這麼做

func t2h(number string) string {
	tenum, _ := strconv.Atoi(number) //沒有err

還記得我們之前說過golang不能有值被宣告出來卻沒有任何作用吧
這時如果你不想處理err,可以用 _ 代替,golang不會要你處理

說到這裡我們往下看到t2h吧

func t2h(number string) string {
	tenum, _ := strconv.Atoi(number)
	for i := 0; i < 16; i++ {
		for j := 0; j < 16; j++ {
			for k := 0; k < 16; k++ {
				if int(math.Pow(16, 2))*i+int(math.Pow(16, 1))*j+int(math.Pow(16, 0))*k == tenum {
					return fmt.Sprintf("%s%s%s",returnAE(i),returnAE(j),returnAE(k))
				}
			}
		}
	}
	return ""
}

for 迴圈基本上跟我們以往使用的一樣,只是原本的var i=0 被換成了i:=0

if 的用法也跟一般的程式語言相同,差別在於不需要使用()將條件包起來

if int(math.Pow(16, 2))*i+int(math.Pow(16, 1))*j+int(math.Pow(16, 0))*k == tenum {
	return fmt.Sprintf("%s%s%s",returnAE(i),returnAE(j),returnAE(k))
}

if裡面我們反過來將pow算出來的float64型別轉成int

int(math.Pow(16, 2))*i

如此才可以跟整數型別的tenum做比較

回傳值的

fmt.Sprintf("%s%s%s",returnAE(i),returnAE(j),returnAE(k))

則是fmt提供並非輸出的方法Sprintf,用來將內容格式化成字串
還記得C#的

Console.WriteLine("{0} {1} {2}",returnAE(i),returnAE(j),returnAE(k));

使用的填充字元是{0}
而在golang則是使用%s
不同型別還有不同用法,詳細可以看這裡

最後補充一下

go fmt .

在golang有明確的規定你的程式碼風格,如果不照著走甚至會不讓你編譯
這方面有人覺得好有人覺得壞
好的話是不管你是誰來自哪裡, { } ( ) 的位置都是一樣的
不好的話是有人就希望照自己的風格( { 要自己一行之類的)
現在去打亂你的程式碼(不是要你程式碼亂搬位置,就把空格或tab亂按一通就好)
接著叫go fmt . 自動幫你整理
. 的目的是不指定檔案,所有的go檔都會幫你整理

以上就是golang的基本語法拉

讓我們稍微複習一下

  • 基本結構
    • package import main為基本御三家
    • package是最大的結構體
  • 印出/讀取
    • 使用fmt提供的方法來輸入及輸出
    • 記得Scanln要丟入的是變數的記憶體位置&
    • Sprintf可以格式化輸出成字串(但是不會印出來)
  • 方法的結構
    • 靜態型別,方法必須提宣告回傳值的型別
    • 可以提前命名回傳值的變數名稱
  • 邏輯控制
    • switch 不需要break跟()
    • if 不需要()
  • 迴圈控制
    • for 同時具有while的功能,不需要()
  • 型別
    • 對數字較為嚴格,不會自動轉換int跟float
  • 冪次計算 輸入值的型別必須是float64

因為出於私心的關係,golang的介紹比其他的語言都還要長(其實是其他語言不像golang這麼熟)
如果你有興趣,可以看隔壁的golang介紹

明天我們來介紹golang的殺手級應用docker


上一篇
物件 萬物皆物件
下一篇
docker 不是跨平台,我就是平台
系列文
你會十五種程式語言?不,我會十五種HelloWorld.為了避免這種狀況,因此寫了這篇:淺入淺出十五種程式語言30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
skycover
iT邦新手 4 級 ‧ 2020-09-13 11:34:31

如果有任何寫不清楚或是觀念沒有很明白的話請留言告知我
會盡快補上

如果有任何寫錯的地方也麻煩留言告知我
會盡快修正

感謝各位

我要留言

立即登入留言