1. 程式人生 > >Go語言2-基本數據類型和操作符

Go語言2-基本數據類型和操作符

包含 imp define 符號 全局變量 參數 技術 中文 如何

主要內容:

  • 文件名、關鍵字、標識符
  • Go程序的基本結構
  • 常量和變量
  • 數據類型和操作符
  • 字符串類型

文件名、關鍵字、標識符

所有go源碼以.go結尾
標識符以字母或下劃線開頭,大小寫敏感
_是特殊標識符,用來忽略結果
保留關鍵字(25個):

break       //退出循環
default     //選擇結構默認項(switch、select)
func        //定義函數
interface   //定義接口
select      //channel
case        //選擇結構標簽
chan        //定義channel
const       //常量
continue    //跳過本次循環
defer       //延遲執行內容(收尾工作)
go      //並發執行
map         //map類型
struct      //定義結構體
else        //選擇結構
goto        //跳轉語句
package     //包
switch      //選擇結構
fallthrough     //switch裏繼續檢查後面的分支
if      //選擇結構
range       //從slice、map等結構中取元素
type        //定義類型
for         //循環
import      //導入包
return      //返回
var     //定義變量

Go程序的基本結構

下面就是一段最簡單的 Hollo World。看一下go程序的基本結構:

package main  // 聲明包

import "fmt"  // 導入包

func main() {
    fmt.PringLn("Hello World")
}

package: 任何一個代碼文件都隸屬於一個包
import: 引用其他包

// 導入多個包可以這麽寫
import("fmt")
import("os")
// 通常習慣這麽寫
import(
    "fmt"
    "os"
)

可執行程序的包名必須是main,並且一個程序只能由一個main函數。main函數是程序的入口函數。
練習


寫一個程序,對於給定的一個數字n,求出所有兩兩相加等於n的組合。把結果在終端打印出來。

package main

import "fmt"

func list(n int){
    for i := 0; i <= n; i++ {
        fmt.Printf("%d+%d=%d\n", i, n-i, n)
    }
}

func main(){
    list(10)
}

包中函數的調用:

  • 同一個包中的函數,可以直接調用
  • 不同包中的函數,通過包名+點+函數名進行調用

包訪問控制規則:

  • 大寫意味著這個函數/變量是可導出的
  • 小寫意味著這個函數/變量是私有的,包外部不能訪問

示例
一個程序包含兩個包add和main,其中add包中有2個變量:Name(string)和age(int)。如何在main包中調用Name和age:

// go_dev\day2\get_var_in_add\add\add.go
package add

var Name string = "Gordon"
var age int = 10

// go_dev\day2\get_var_in_add\main\main.go
package main

import (
    "go_dev/day2/get_var_in_add/add"
    "fmt"
)

func main(){
    fmt.Println("Name =", add.Name)
    fmt.Println("age =", add.age)
}

上面的代碼是有問題的,運行後會報錯:

H:\Go\src\go_dev\day2\get_var_in_add\main>go run main.go
# command-line-arguments
.\main.go:10:17: cannot refer to unexported name add.age
.\main.go:10:17: undefined: add.age

原因是age是小寫,表示這個變量是包私有的,在包外部是不能訪問的。把兩個文件裏的age都改成大寫Age再試一下:

H:\Go\src\go_dev\day2\get_var_in_add\main>go run main.go
Name = Gordon
age = 10

包的別名

把上面的main函數的修改一下,導入包,包命換成別名,然後用別名來訪問包:

package main

import (
    // "go_dev/day2/get_var_in_add/add"
    a "go_dev/day2/get_var_in_add/add"
    "fmt"
)

func main(){
    fmt.Println("Name =", a.Name)
    fmt.Println("age =", a.Age)
}

init 函數

每個原文件都可以包含一個(也可以是多個)init函數。init函數會自動被go的運行框架調用。調用的時機是在main函數執行之前。
把上面的add函數修改一下,先聲明變量,然後再為變量賦值:

// init函數
package add

var Name string
var Age int

// 這些這種寫法是不對的
Name = "Goldie"
Age = 18

執行後報錯:

H:\Go\src\go_dev\day2\get_var_in_add2\main>go run main.go
# go_dev/day2/get_var_in_add2/add
..\add\add.go:8:1: syntax error: non-declaration statement outside function body

go是一門編譯型語言,非聲明語句都需要在函數裏。
所以要麽在聲明變量的時候,就給一個初始值,要麽可以在init函數裏為變量賦值:

// init函數
package add

var Name string
var Age int

// 這些這種寫法是不對的
// Name = "Goldie"
// Age = 18

// 把變量初始化寫在init函數裏
func init(){
    Name = "Goldie"
    Age = 18
}

只初始化不引用包
Go不允許引用不使用的包。但是有時你引用包只是為了調用init函數去做一些初始化工作。這就需要使用空標識符即下劃線:

// go_dev\day2\init_only\test\test.go
package test

import "fmt"

func init(){
    fmt.Println("init test.go")
}

// go_dev\day2\init_only\main\main.go
package main

import (
    // "../test"  // 導入不使用的包會報錯
    _ "../test"
    "fmt"
)

func main(){
    fmt.Print("This is main")
}

如果不用下劃線會報錯:

H:\Go\src\go_dev\day2\init_only\main>go run main.go
# command-line-arguments
.\main.go:4:5: imported and not used: "_/H_/Go/src/go_dev/day2/init_only/test"

函數聲明和註釋

函數聲明func 函數名 (參數列表) (返回值列表) {}

// 無參數無返回值
func add () {}

// 有參數,一個返回值
func add (a int, b int) int {}

// 有參數,多個返回值
func add (a, int, b int) (int, int) {}

註釋有兩種,單行註釋:// 和多行註釋:/* */

//這樣可以寫單行註釋

/* 多行註釋也一行也是可以的 */

/* 像這樣
就可以
註釋掉
整塊的內容
*/

常量

常量使用 const 修飾,代表只讀,是不能修改的。
const 只能修飾 boolean、number(int相關類型、浮點類型、complex)、string
語法: const identifier [type] = value ,其中type可以省略

const a string = "Hello World"
const a = "Hello World"
const Pi = 3.1415926
const b = 9/3  // 表達式也是可以的

優雅的寫法:

const (
    a = 0
    b = 1
    c = 2
)

// 更加專業的寫法
const (
    a = iota
    b
    c
)

例子裏有個iota。iota是golang語言的常量計數器,只能在常量的表達式中使用(也就是必須在const裏面使用)。
簡單講就是,iota在const關鍵字出現時被重置為0(const內部的第一行之前),const中每新增一行常量聲明將使iota計數自增1(從0開始)。所以第一個值就是0,之後的值依次就是1、2。配合表達式還有下劃線等等,也可以有很多高級的寫法,效果就是讓代碼定義常量變的更加優雅。以後在定義一組有規律的常量的時候,可以再深入一下,看看如何用iota優雅的實現。
練習
定義兩個常量male=1和female=2,獲取當前時間的秒數(time.Now().Second()),如果能被female整除,則終端打印FEMALE,否則打印MALE:

package main

import (
    "time"
    "fmt"
)

const (
    _ = iota
    male
    female
)

func main() {
    now := time.Now()
    fmt.Println("當前時間:", now)
    second := now.Second()
    fmt.Println("秒數:", second)
    if second % female == 0 {
        fmt.Println("FEMALE")
    } else {
        fmt.Println("MALE")
    }
}

變量

語法: var identifier type

var a int  // 默認為0
var b string  // 默認為空,即""
var c bool  // 默認為false

// 聲明的同時進行初始化
var d int = 8
var e string = "Hello World"

一次定義多個變量,可以寫在一個var裏:

// 效果和上面一樣
var (
    a int
    b string
    c bool
    d = 8  // 這個有初始值,可以省略類型,go自己會做類型推導
    e = "Hello World"
)

練習:
寫一個程序,獲取當前運行的操作系統的名稱和PATH環境變量的值,打印到終端:

package main

import (
    "fmt"
    "os"
)

func main() {
    os := os.Getenv("os")
    fmt.Println("OS", goos)
    path := os.Getenv("PATH")
    fmt.Println("PATH", path)
}

值類型和引用類型

值類型:變量直接存儲值,內存通常在棧中分配。
基本數據類型int、float、bool、string,以及數組和struct,這些都是值類型。
引用類型:變量存儲的是一個地址,這個地址存儲最終的值。內存通常在堆上分配,通過GC回收。
指針、slice、map、chan等都是引用類型。
上面提到了棧(棧是後進先出的)和堆。如果是值類型,那麽數值就直接存在棧裏。如果是引用類型,這個變量也是在棧裏的,但是棧裏的值存的是堆裏對應的地址,通過棧裏的地址可以查找到堆裏對應的地址空間裏的值。
技術分享圖片

下面用int和指針演示一下,a是int值類型,b是指針引用類型。在modify裏修改了兩個變量的值,在main裏打印的結果看,只有b的值被改變了。a的值沒變,是因為調用函數傳參的時候,是把值類型的值再復制一份給調用的函數的,所以在那個函數裏的變化不會影響到原來的變量的值。

package main

import "fmt"

func modify(x int, y *int){
    x = 15
    *y = 25
}

func main(){
    var(
        a = 10
        b = 20
    )
    modify(a, &b)
    fmt.Println(a, b)
}

變量的作用域

在函數內部聲明的變量叫做局部變量,生命周期僅限於函數內部。
在函數外部聲明的變量叫做全局變量,生命周期作用於整個包,如果是大寫的,則還可以在外部訪問。

數據類型和操作符

bool類型
只能存true或false,默認值是false。
相關操作符:!、&&、||

數字類型
主要有int、int8、int16、int32、int64、uint8、uint16、uint32、uint64、float32、float64。uint開頭的這些是無符號×××。
類型轉換:type(variable),比如:var a int=8; var b int32=int32(a)
邏輯操作符:==、!=、<、<=、>、>=
數學操作符:+、-、*、/ 等等

練習
使用 math/rand 生成10個隨機整數,10個小於100的隨機整數,10個隨機浮點數。
先去官網(https://go-zh.org/ )查一下這個包的用法。右上角有個包點進去找到要查的包。
具體在這裏:https://go-zh.org/pkg/math/rand/

// go_dev\day2\random\create\create_num.go
package create

import (
    "math/rand"
    "fmt"
)

func CreateInt(){
    for i := 0; i < 10; i++{
        fmt.Print(rand.Int(), ", ")
    }
    fmt.Print("\n")
}

func CreateIntn(){
    for i := 0; i < 10; i++{
        fmt.Print(rand.Intn(100), ", ")
    }
    fmt.Print("\n")
}

func CreateFloat32(){
    for i := 0; i < 10; i++{
        fmt.Print(rand.Float32(), ", ")
    }
    fmt.Print("\n")
}

// go_dev\day2\random\main\main.go
package main

import "../create"

func main(){
    create.CreateInt()
    create.CreateIntn()
    create.CreateFloat32()
}

執行結果:

H:\Go\src\go_dev\day2\random\main>go run main.go
5577006791947779410, 8674665223082153551, 6129484611666145821, 4037200794235010051, 3916589616287113937, 6334824724549167320, 605394647632969758, 1443635317331776148, 894385949183117216, 2775422040480279449,
94, 11, 62, 89, 28, 74, 11, 45, 37, 6,
0.20318687, 0.3608714, 0.5706733, 0.8624914, 0.29311424, 0.29708257, 0.752573, 0.20658267, 0.865335, 0.69671917,

H:\Go\src\go_dev\day2\random\main>

執行幾次發現,每次結果都是一樣的。因為生成隨機數要有一個種子,具體要看一下 func Seed(seed int64) 這個方法,默認就是Seed(1),種子一樣,每次生成的序列就是一樣的。在生成數字之前先設置一下種子,把main改一下:

// go_dev\day2\random\main\main.go
package main

import (
    "../create"
    "math/rand"
    "time"
)

func main(){
    rand.Seed(time.Now().Unix())  // 把時間作為種子,每秒生成的隨機數是不同的
    create.CreateInt()
    create.CreateIntn()
    create.CreateFloat32()
}

上面的做法不是很專業,設置種子可以認為是一個初始化操作,可以放到init函數裏。最後完整的代碼如下:

// go_dev\day2\random\create\create_num.go
package create

import (
    "math/rand"
    "fmt"
    "time"
)

func init(){
    fmt.Println("設置隨機數種子...")
    rand.Seed(time.Now().Unix())
}

func CreateInt(){
    for i := 0; i < 10; i++{
        fmt.Print(rand.Int(), ", ")
    }
    fmt.Print("\n")
}

func CreateIntn(){
    for i := 0; i < 10; i++{
        fmt.Print(rand.Intn(100), ", ")
    }
    fmt.Print("\n")
}

func CreateFloat32(){
    for i := 0; i < 10; i++{
        fmt.Print(rand.Float32(), ", ")
    }
    fmt.Print("\n")
}

// go_dev\day2\random\main\main.go
package main

import "../create"

func main(){
    create.CreateInt()
    create.CreateIntn()
    create.CreateFloat32()
}

字符類型
byte 字符類型就是1個字符:var a byte
byte要用單引號表示,單引號只能有一個字符。輸出會返回這個字符的ascii碼,如果想輸出為字符需要用string()函數轉換一下:

package main

import "fmt"

func main() {
    var (
        b1 byte
        b2 byte
        b3 byte
    )
    b1 = ‘a‘
    b2 = ‘A‘
    b3 = 98  // 直接用ascii碼也可以定義
    fmt.Println(b1, string(b1))
    fmt.Println(b2, string(b2))
    fmt.Println(b3, string(b3))
    var b4 int = 99
    fmt.Println(b4, string(b4))
}

字符串類型
string 字符串類型,是由0個或多個字符組成的:var str string
字符串有2種表示方式:

  • 雙引號,內部會做轉義
  • 反引號,內部不會做轉義。效果和python裏的3個引號似乎一樣

示例代碼:

package main

import "fmt"

func main() {
    str1 := "Hello \n How are you"
    str2 := `Fine \n Thank you`
    fmt.Println(str1)
    fmt.Println(str2)
    str3 := `百日依山盡,
黃河入海流。
欲窮千裏目,
更上一層樓。`
    fmt.Println(str3)
}

字符串操作

字符串輸出,主要是fmt模塊,之前已經用了很多次了。要格式化輸出就使用 fmt.Printf()
各種格式化詳細的寫法可以翻一下官方中文文檔:https://go-zh.org/pkg/fmt/

  • fmt.Print() : 按默認格式輸出
  • fmt.Printf() : 格式化輸出
  • fmt.Println() : 按默認格式輸出,且總在最後追加一個換行符。

另外還有3個對應的S開頭的命令 Sprint、Sprintf、Sprintln,結果不輸出到終端,而是return返回。

把數值轉化成字符串

用Sprint()方法可以把屏幕結果輸出到變量裏去。顯示在屏幕上的一定是字符串形式,結果不定向到標準輸出,而是定向到變量:

package main

import "fmt"

func main(){
    var n int
    n = 99
    s1 := string(n)
    fmt.Println(s1)  // 轉類型輸出的是對應的ascii字符
    fmt.Print(n, "\n")  // 直接終端打印,輸出的正確的形式
    s2 := fmt.Sprint(n)  // 把結果返回給變量s2
    fmt.Println(s2)  // 現在s2裏存的是數字的字符串形式
}

打印變量類型
%T 的效果是,相應值的類型的Go語法表示

package main

import "fmt"

func main(){
    var b5 byte
    b5 = ‘B‘
    fmt.Printf("%T\n", b5)
    b6 := ‘B‘
    fmt.Printf("%T\n", b6)
    f1 := true
    fmt.Printf("%T\n", f1)
    s1 := "abc"
    fmt.Printf("%T\n", s1)
}

// 運行結果:
H:\Go\src\go_dev\day2\examples>go run show_type.go
uint8
int32
bool
string

H:\Go\src\go_dev\day2\examples>

這裏的字符類型,並沒有當做字符類型來記錄,而且兩種定義方法所對應的類型也不同。

字符串切片

直接上例子:

package main

import "fmt"

func main(){
    str := "This is Golang"
    fmt.Println(str[0:4], str[:4])
    fmt.Println(str[5:])
    fmt.Println(str[0:len(str)])  // 缺省的起始位置是0,結束位置是len(str)
    fmt.Println(str[len(str)-6:len(str)])  // 不支持負數,要截最後幾位應該該是這樣
    fmt.Println(str[5:5+2])  // 要截取多少位,貌似也只能做下加法了
    fmt.Printf("%c\n", str[6])  // 只取一個字符,str[6]取到的是字符,就是byte類型
    fmt.Println(str[6:6+1])  // 這樣可以取到string類型的一個字符,也就是長度為1的字符串
}

// 執行結果
H:\Go\src\go_dev\day2\examples>go run slice.go
This This
is Golang
This is Golang
Golang
is
s
s

H:\Go\src\go_dev\day2\examples>

字符串拼接

字符串拼接有好幾種方法,主要是要考慮性能,各種實現方式結論如下(性能依次提高):

  • 性能要求不太高的場合,直接使用運算符,代碼更簡短清晰,能獲得比較好的可讀性
  • 如果需要拼接的不僅僅是字符串,還有數字之類的其他需求的話,可以考慮 fmt.Sprintf()
  • 在已有字符串數組的場合,使用 strings.Join() 能有比較好的性能
  • 在一些性能要求較高的場合,盡量使用 buffer.WriteString() 以獲得更好的性能

目前只會前2種。
練習
寫一個方法,實現字符串的反轉(輸入“123456”,返回“654321”):

// go_dev\day2\reverse\reverse\reverse.go
package reverse

import "fmt"

func Reverse(a string) string {
    length := len(a)
    var result string
    fmt.Printf("%T\n", a[1])  // a[1]的類型是byte,輸出:uint8
    fmt.Printf("%T\n", a[1:1+1])  // a[1:1+1]是1個字符的字符串,輸出:string
    for i := length-1; i >= 0 ; i-- {
        //result += string(a[i])  // 用a[i]的話還要轉類型
        result += a[i:i+1]
    }
    return result
}

// go_dev\day2\reverse\main\main.go
package main

import (
    "fmt"
    "../reverse"
)

func main(){
    fmt.Println(reverse.Reverse("123456"))
}

課後作業

一、判斷 101-200 之間有多少個素數(也叫質數),並輸出所有素數。

二、打印出100-999中所有的“水仙花數”。
所謂“水仙花數”是指一個三位數,其各位數字立方和等於該數本身。
例如:153 是一個“水仙花數”,1**3=3,5**3=125,3**27,sum(1, 125, 27) = 153。

三、對於一個數n,求n的階乘之和,即: 1! + 2! + 3! + ... + n!

Go語言2-基本數據類型和操作符