超實用的Go語言基礎教程,讓你快速上手刷題!!

嘉沐發表於2023-05-12

背景?

工欲善其事,必先利其器。掌握Go的基礎語法還不夠,還需要勤加練習,修習“外功”,才能達到出奇制勝的效果。

在大致瞭解Go語言的基本語法後,我就迫不得已地想使用這門語言。可是我發現程式設計思路不是問題,很大的問題是“手慢”,不熟悉常用寫法(可能這就是快速過語法的缺點吧,腦子會了,手沒會)φ(* ̄0 ̄)。

在我看來,用Go語言刷演算法題是一個非常好的練習“外功”的法門,可以幫助我提高思維的靈敏性和解決抽象化問題的能力。更重要地是複習我學習過的語法知識,不然真的很容易忘。雖然它和C語言有點像,但是我也並不經常使用C,兩者不太好建立起清晰的關聯圖。因此,我會一邊勤能補拙,一邊總結一些語法知識,一邊建立語言之間的聯絡,方便我加深記憶。

我刷的不是Leetcode形式的題目,而是ACM形式的題目。因為ACM形式需要處理輸入輸出,這對我的要求會更高點。

刷題平臺:洛谷

基礎知識?

輸入處理

Go接收輸入的方式有四類,分別是 fmt 包中的 Scan 、Scanf 和Scanln函式以及bufio.Scanner物件實現。

  • Scan函式

使用場景:可以用於讀取一段空格分隔的字串或多個數值型別的輸入,例如讀取數字或時間等;

示例一:計算浮點數相除的餘。

輸入格式:輸入僅一行,包括兩個雙精度浮點數a和b。

輸入樣例:

13.55 24.88

處理方式:

func main() {
	// 接收兩個雙精度浮點數a,b
	var a, b float64
	_, err := fmt.Scan(&a, &b)
	if err != nil {
		fmt.Println(err)
	}
}
  • Scanf函式

使用場景:適用於需要按特定格式讀取和處理輸入資料的場景,例如讀取時間、日期、金額等;

示例二:數字排序

輸入格式:輸入三個數字,數字之間用逗號隔開。

輸入樣例:

1,4,6

處理方式:

package main

import (
	"fmt"
)

func main() {
	var a, b, c int
	fmt.Scanf("%d,%d,%d", &a, &b, &c)
	fmt.Println(a, b, c)
}

如果輸入不止三個數字,輸入很長怎麼辦?

我想到的是直接當字串儲存,然後用“,”分割每一個元素,獲得一個字串陣列,最後利用Atoi函式將字串轉為整數,儲存到一個新的int型別陣列中。

具體做法如下:

package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func main() {
	var input string
	scanner := bufio.NewScanner(os.Stdin)
	if scanner.Scan() {
		input = scanner.Text()
	} else {
		fmt.Println("Error")
	}
	strArray := strings.Split(input, ",")
	intArray := make([]int, len(strArray))  // 根據strArray的長度確定intArraye的長度
	for i, v := range strArray {
		var err error
		intArray[i], err = strconv.Atoi(strings.TrimSpace(v))  // strings.TrimSpace 函式去掉字串中的多餘空白字元
		if err != nil {
			fmt.Println("Error")
		}
	}
	fmt.Printf("The input integers are: %v\n", intArray)
}
  • Scanln函式

使用場景:適用於讀取空格或換行分隔的字串或多個數值型別的輸入,例如讀取單詞或名稱等。用法和Scan相似,就不舉例子了。(~ ̄▽ ̄)~

  • bufio.Scanner物件

使用場景:這個物件可以從標準輸入中逐行讀取輸入,直到遇到檔案結尾或輸入流關閉為止。特別適合迴圈讀入資料!

示例三:字串讀取,並列印

輸入格式:輸入多行英文句子。

輸入樣例:

wow!
you are pretty good at printing!
you win.

處理方式:

package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	var strArray []string
	scanner := bufio.NewScanner(os.Stdin)
	for scanner.Scan() {
		input := scanner.Text()
		if input == "" {
			break
		}
		strArray = append(strArray, input)
	}
	if err := scanner.Err(); err != nil {
		fmt.Printf("Error reading standard input: %s\n", err.Error())
	}

	fmt.Printf("Read %d lines:\n", len(strArray))
	for i, line := range strArray {
		fmt.Printf("%d: %s\n", i+1, line)
	}
}

輸出處理

Go處理輸出的方式根據場景的不同,可以分為以下幾種:

  • 終端或控制檯中輸出一些資訊,使用fmt包中的函式。
package main

import (
	"fmt"
)

func main() {
	name := "Tom"
	age := 18
	fmt.Println("name:", name, "age:", age) // Println()函式會自動新增空格
	fmt.Printf("name: %s age: %d\n", name, age)
	str1 := fmt.Sprintf("name: %s age: %d\n", name, age) // Sprintf()函式會返回一個字串
	fmt.Printf(str1)
}
  • 記錄程式執行過程中的日誌資訊時,可以使用log包中的函式。
package main

import (
	"fmt"
	"log"
)

func main() {
	log.Println("Starting the application...")
	fmt.Println("Hello, World!")
	log.Println("Terminating the application...")
}
  • 讀寫檔案或網路連線時,可以使用os包中的函式。
package main

import (
	"fmt"
	"log"
	"os"
)

func main() {
	file, err := os.Open("test.txt")
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()

	buffer := make([]byte, 1024) // read 1024 bytes at a time
	for {
		bytesRead, err := file.Read(buffer) // read bytes from file
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println("bytes read: ", bytesRead)
		fmt.Println("bytes:", buffer[:bytesRead])
		if bytesRead < 1024 {
			break
		}
	}
	fmt.Printf("File contents: %s", buffer) // print file contents
}
  • 執行系統命令或建立程式時,可以使用os包中的函式。
package main

import (
	"fmt"
	"log"
	"os"
	"os/exec"
)

func main() {
	cmd := exec.Command("whoami")
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	err := cmd.Run()
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("Done")
}

ACM形式的題目更多考察的是第一種在終端/控制檯輸出資訊的格式。這個就要涉及到Go語言格式化字串的方式的知識點。在我看來,格式化字串在每種語言裡都享有很高的地位。畢竟更美觀的列印資料,也有助於我們更好的理解資訊。

  • 格式化字串
格式 描述
%v 表示按照值的預設格式輸出,可以輸出任意型別的資料。
%s 表示輸出字串型別的資料。
%d 表示輸出十進位制整數型別的資料。
%f 表示輸出浮點數型別的資料。
%t 表示輸出布林型別的資料,true和false分別對應輸出1和0。
%p 表示輸出指標型別的資料。
%c 表示輸出字元型別的資料。
%q 表示輸出帶引號的字串型別的資料。
%b 表示輸出二進位制數型別的資料。
%x 表示輸出十六進位制數型別的資料。
%o 表示輸出八進位制數型別的資料。
%05d 表示輸出5位,不足的位數用0補齊。
%.2f 表示輸出小數點後兩位。
%10s 輸出10個字元長度,不足的位數用空格補齊
package main

import "fmt"

func main() {
    name := "Tom"
    age := 18
    height := 1.75

    fmt.Printf("My name is %s, I'm %d years old, and I'm %.2f meters tall.\n", name, age, height)
    fmt.Printf("My name is %10s, I'm %05d years old, and I'm %.2f meters tall.\n", name, age, height)
}
My name is Tom, I'm 18 years old, and I'm 1.75 meters tall.
My name is        Tom, I'm 00018 years old, and I'm 1.75 meters tall.

陣列?切片?

在Go語言中,陣列是一種固定長度的資料結構,一旦定義了陣列的長度,就無法再向陣列中新增新的元素。如果想動態更改,可以考慮使用切片。根據使用方法可以大致分個類:

共性 差異
下標訪問 定義方式不同
迴圈遍歷 切片可以新增/刪除元素
長度計算
切片[start:end]
package main

import (
	"fmt"
)

func main() {
	var n int
	fmt.Scan(&n)
	var arr []int
	var max int = -10000000
	var sum int = 0
	for i := 0; i < n; i++ {
		var x int
		fmt.Scan(&x)
		sum += x
		if max < x {
			max = x
		}
		arr = append(arr, x)
	}
	var count int = 0
	// 找到陣列裡面最大的數及它出現的次數
	for i := 0; i < n; i++ {
		if max == arr[i] {
			count++
		}
	}
	fmt.Println(sum - max*count)
}
package main

import (
	"fmt"
)

func main() {
	var n int
	fmt.Scan(&n)
	var used [110]int
	for i := 0; i < n; i++ {
		var x int
		fmt.Scan(&x)
		used[x]++
		if used[x] < 2 {
			fmt.Print(x, " ")
		}
	}
}

字串處理

  • 字串長度計算

在Go語言中,字串的長度是指字串中位元組的個數,而不是字元的個數。對於包含非ASCII字元的字串,一個字元可能會佔用多個位元組。

package main

import (
	"fmt"
)

func main() {
	str := "hello world"
	fmt.Println(len(str)) // 輸出11
	str = "hello 世界"
	fmt.Println(len(str)) // 輸出12
}
  • 字串遍歷

既可以使用傳統的下標遍歷,也可以使用range遍歷。建議使用range遍歷,因為當字串中出現中文時,下標遍歷獲取的是byte型別的值,也就意味著它是將一個漢字拆成了3個byte型別位元組分別輸出。

package main

import (
	"fmt"
)

func main() {
	str := "hello world"
	for i, v := range str {
		fmt.Printf("字串中下標為 %d 的字元是 %c\n", i, v)
	}
}
  • 字串切片

需要注意的是,在使用字串切片時,下標是按位元組計算的,而不是按字元計算的。

str := "hello world"
slice := str[1:5]  // 獲取str中下標為1到4的字元,不包括下標為5的字元
fmt.Println(slice)  // 輸出"ello"
  • 字串連線

可以使用加號運算子或fmt.Sprintf函式來連線字串。

str1 := "hello"
str2 := "world"
str3 := str1 + " " + str2  // 使用加號運算子連線字串
fmt.Println(str3)  // 輸出"hello world"

str4 := fmt.Sprintf("%s %s", str1, str2)  // 使用fmt.Sprintf函式連線字串
fmt.Println(str4)  // 輸出"hello world"
  • 字串查詢

使用strings包中的函式來查詢字串中的子串。

str := "hello world"
index := strings.Index(str, "world")  // 查詢子串"world"在str中的位置
fmt.Println(index)  // 輸出6
  • 字串替換

使用strings包中的函式來替換字串中的子串。

str := "hello world"
newstr := strings.Replace(str, "world", "golang", -1) // 將子串"world"替換為"golang", -1表示全部替換
fmt.Println(newstr)                                   // 輸出"hello golang"
  • 字串轉換

使用strconv包中的函式進行轉換。

str := "123"
num, err := strconv.Atoi(str) // 將字串轉換為整型
if err != nil {
    fmt.Println("轉換失敗")
} else {
    fmt.Printf("轉換結果是 %T\n", num)
}

num = 123
str = strconv.Itoa(num) // 將整型轉換為字串
fmt.Printf("轉換結果是 %T\n", str)
  • 正則匹配(✨✨✨✨)
預定義字符集 描述
\d 匹配一個數字字元。等價於字符集 [0-9]。
\s 匹配一個空白字元(空格、製表符、換行符等)。等價於字符集 [ \t\n\r\f\v]。
\w 匹配一個單詞字元。等價於字符集 [a-zA-Z0-9_]。
\W 匹配一個非單詞字元。等價於字符集 [^a-zA-Z0-9_]。
\S 匹配一個非空白字元。等價於字符集 [^ \t\n\r\f\v]。
\D 匹配一個非數字字元。等價於字符集 [^0-9]。
\b 表示單詞邊界,我的理解是能準確匹配到某個單詞,不把包含這個單詞的字首詞算在內。比如gotest就無法匹配test。

匹配一個由漢字組成的字串(資料清洗時常用!):

^[\u4e00-\u9fa5]+$

匹配一個由郵箱地址組成的字串(匹配惡意URL、匹配釣魚郵箱常用):

^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$

演示1:匹配一個字串是否符合某個正規表示式。

import (
	"fmt"
	"regexp"
)

func main() {
	// 定義一個正規表示式
	pattern := "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$"
	// 編譯正規表示式
	reg := regexp.MustCompile(pattern)
	// 要匹配的字串
	str := "abc123@11-2.com"
	// 判斷字串是否匹配
	matched := reg.MatchString(str)
	fmt.Println(matched)
}

演示2:利用正則進行查詢和替換字串

// 查詢
str := "hello world"
re := regexp.MustCompile(`\b\w+o\w+\b`) // 匹配包含字母o的單詞
newstr := re.FindAllString(str, -1)     // 將查詢所有匹配的字串
fmt.Println(newstr)

// 替換
str := "hello world"
re := regexp.MustCompile(`\b\w+o\w+\b`)  // 匹配包含字母o的單詞
newstr := re.ReplaceAllString(str, "golang")  // 將所有匹配的字串替換為"golang"
fmt.Println(newstr)  // 輸出"golang golang"
package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	scanner := bufio.NewScanner(os.Stdin)
	scanner.Scan()
	sentence := scanner.Text()
	var count int = 0
	for _, v := range sentence {
		if v >= '0' && v <= '9' {
			count++
		}
	}
	fmt.Println(count)
}

結構體

Go語言的結構體和C語言很相似。

  • 結構體定義
type Person struct {
    Name string
    Age int
    Height float32
}
  • 結構體初始化
p1 := Person{Name: "Alice", Age: 20, Height: 1.65}  // 定義一個Person型別的結構體變數p1並初始化
p2 := new(Person)  // 定義一個指向Person型別的指標變數p2,並分配記憶體空間
  • 結構體元素訪問("."號訪問)

指標和普通的物件型別都是使用“.”號訪問。

p1.Name = "Alice"  // 給p1的Name賦值為"Alice"
p1.Age = 20  // 給p1的Age賦值為20
p1.Height = 1.65  // 給p1的Height賦值為1.65

分界線:———————————————————————————————————————

Go還支援一些物件導向的程式設計特性,非常的靈活和強大!!!

func (p *Person) GetInfo() string {
    return fmt.Sprintf("Name: %s, Age: %d, Height: %.2f", p.Name, p.Age, p.Height)
}

p1.GetInfo()  // 呼叫p1的GetInfo方法,返回"Name: Alice, Age: 20, Height: 1.65"

這個方法定義了一個指標型別為Person的方法GetInfo,用來返回一個包含Person物件資訊的字串。我們可以透過呼叫結構體變數的方法來實現對結構體物件的操作。這種使用方法就很棒!這就有點像類方法,GetInfo函式就是Person結構體的類方法。想要使用這個方法,那麼就需要先構造一個Person的結構體物件,然後透過物件呼叫。

此外,Go還支援封裝、繼承、多型的特性,用來實現複雜的物件模型和資料結構。

  • 封裝
type Person struct {
    name string
    age int
}

func (p *Person) SetName(name string) {
    p.name = name
}

func (p *Person) GetName() string {
    return p.name
}

這個結構體定義了一個名為Person的結構體型別,包含了兩個私有的成員變數name和age,以及兩個公有的方法SetName和GetName,用來設定和獲取name成員變數的值。不同於其它語言使用Public,Private定義公有和私有,Go使用程式設計規範來定義這個概念。變數名首字母大寫代表公有,對外可見;變數名首字母小寫代表私有,對外不可見。(經過實驗,上面的說法是有一個大前提的。同一個包內,無論是公有變數還是私有變數,在任何地方都可以訪問!!!!,只有在不同的包裡,才有上面變數名大小寫來控制可見性的說法。???)Go的變數命名主要使用駝峰命名法,也算是約定俗成吧。

  • 繼承和組合
type Person struct {
    name string
    age int
}

type Student struct {
    Person  // 匿名巢狀Person結構體
    id string
}

func (s *Student) SetId(id string) {
    s.id = id
}

這個結構體定義了一個名為Student的結構體型別,透過匿名巢狀Person結構體,實現了從Person結構體繼承了name和age成員變數和方法,並新增了一個id成員變數和SetId方法。這樣,我們就可以透過Student結構體來訪問和操作Person結構體的成員變數和方法。匿名巢狀是繼承,不匿名就是組合的使用方法了。

  • 介面多型

宣告一個Shape型別的介面,該介面裡定義了Area()函式。Rectangle和Circle實現了Shape型別介面裡的Area()的方法,可以認定為是一個實現類。PrintArea方法接受一個Shape型別的資料,然後輸出面積。這個形參是Shape型別,因此,就有了一個“向上轉型”的效果。

package main

import (
	"fmt"
	"math"
)

type Shape interface {
	Area() float64
}

type Rectangle struct {
	Width  float64
	Height float64
}

func (r Rectangle) Area() float64 {
	return r.Width * r.Height
}

type Circle struct {
	Radius float64
}

func (c Circle) Area() float64 {
	return math.Pi * c.Radius * c.Radius
}

func PrintArea(s Shape) {
	fmt.Println(s.Area())
}

func main() {
	r := Rectangle{Width: 3, Height: 4}
	c := Circle{Radius: 5}

	PrintArea(r) // 輸出 12
	PrintArea(c) // 輸出 78.53981633974483
}
package main

import (
	"fmt"
	"math"
)

type coordinate struct {
	x, y     int
	isMarked bool
}

func distence(x1 int, y1 int, x2 int, y2 int) float64 {
	return math.Sqrt(math.Pow(float64(x1-x2), 2) + math.Pow(float64(y1-y2), 2))
}

func main() {
	var n, k, t int
	fmt.Scan(&n, &k, &t)
	coordinates := make([]coordinate, n)
	for i := 0; i < n; i++ {
		fmt.Scan(&coordinates[i].x, &coordinates[i].y)
	}

	for i := 0; i < k; i++ {
		var x, y int
		fmt.Scan(&x, &y)
		for j := 0; j < n; j++ {
			if x == coordinates[j].x && y == coordinates[j].y {
				coordinates[j].isMarked = true
				break
			}
		}
	}

	// 記錄最遠距離的座標,以及最遠距離
	var maxDistence float64 = 0.0
	var maxDistenceid int = -1
	var res int = 0
	for i := 0; i < t; i++ {
		var x, y int
		fmt.Scan(&x, &y)
		for j := 0; j < n; j++ {
			if distence(x, y, coordinates[j].x, coordinates[j].y) > maxDistence {
				// fmt.Println(x, y, coordinates[j].x, coordinates[j].y)
				// fmt.Println("distence:", distence(x, y, coordinates[j].x, coordinates[j].y))
				maxDistence = distence(x, y, coordinates[j].x, coordinates[j].y)
				maxDistenceid = j
			}
		}
		if coordinates[maxDistenceid].isMarked {
			res++
		}
		// 更新最遠距離
		maxDistence = 0.0
		maxDistenceid = -1
	}
	fmt.Println(res)
}

補充

函式的內容並沒有專門拎出來講是兩個原因。第一個原因是有程式設計基礎的人瞄一眼語法就會用了,不太需要刻意的寫。第二個原因是每部分的程式碼都或多或少的用到了函式,不用解釋也能看得懂。當然,函式要講起來還是比較多的,比如傳值和傳址,陣列指標,指標陣列這種,這確實是重難點。如果有機會的話,我可能會專門去總結這部分的內容。

如果我總結的東西對你能產生幫助,那麼請幫我點個推薦,讓更多想要學習Go的人也能獲得幫助。

相關文章