Go 语法 语法基础 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package mainimport ( "fmt" "math" "reflect" "strings" ) func main () { fmt.Println("hello world" ) fmt.Println(math.Floor(2.67 )) fmt.Println(strings.Title("hello zcy" )) fmt.Println('a' ) fmt.Println(reflect.TypeOf(5.2 )) fmt.Println(reflect.TypeOf(1 )) var numA int = 4 fmt.Println(numA) }
零值
短变量
常量
使用 const
关键字定义,不是 var
必须在声明常量时进行赋值,并且不可以改变常量的值
常量没有 :=
语法
1 const StudentName string = "zhang"
命名规则 对于变量、函数、类型的命名规则
【强制】名称必须以字母开头,可以有任意数量的字母或数字
【强制】如果变量、函数、类型是以大写字母开头 ,则认为它是可以导出的(可以在 main 包或者其他包中被引用),可以在当前包之外的包中被访问;如果是小写字母开头,则认为是未导出,只能在当前包中使用
【强制】命名时避免和 Go 的保留关键字重复,会造成对 Go 本身的类型无法使用
【约定】遵守驼峰式 命名
【约定】当名称的 含义在上下文中很明显时,可以用缩写来代替,例如:用 i 代替 index
【约定】包名应该全部小写,含义相当明确时可缩写
【约定】多个单词的包名应该全部小写,不是下划线或者驼峰式
变量命名不要与包名冲突
转换
命令
go fmt
:自动重新格式化源文件以便使用 Go 标准格式
go build
:将 go 源代码编译成计算机可执行的二进制文件
go run
:编译并运行一个程序,而不将可运行文件保存在当前目录
方法 方法是与特定类型的值关联的函数
时间方法
1 2 3 var now = time.Now()fmt.Println(now.Year()) fmt.Println(now.String())
键盘输入
1 2 3 4 fmt.Print("请输入内容:") reader := bufio.NewReader(os.Stdin) //从标准输入(键盘)读取 input, _ := reader.ReadString('\n') //以字符串形式返回用户所有输入内容;换行符前的所有内容将被读取 fmt.Println(input)
_ :上面代码块中出现的“_”,表示空白标识符 ,空白标识符接收的值会被丢弃掉;Go 不允许定义变量却不使用,这种情况使用空白标识符来处理;
正常情况下要对程序的异常(错误)返回进行处理,否则可能会对后面程序的运行造成意外情况
1 2 3 4 5 6 7 fmt.Print("请输入内容:" ) reader := bufio.NewReader(os.Stdin) input, err := reader.ReadString('\n' ) if err != nil { log.Fatal(nil ) } fmt.Println(input)
块和变量的作用域 变量的作用域由其声明所在块和嵌套在该块中的其他块组成,声明的变量可以在其作用域任何地方被访问,在域外无法访问
函数 函数定义和参数 1 2 3 4 5 6 func calculateArea (width float64 , height float64 ) (float64 , error) { if width < 0 || height < 0 { return 0 , fmt.Errorf("输入异常:width{%.2f}, height{%.2f}" , width, height) } return width * height, nil }
Go 函数可以有多个返回值,Go 是一种值传递 语言,函数的形式参数从调用中接收实参的副本
1 2 3 4 5 6 7 8 9 10 11 12 13 func main () { aa := 5 paramChange(aa) fmt.Println(aa) } func paramChange (num int ) { num = 10 fmt.Println(num) }
即在调用的函数体中不会改变出入参数的值,这种和 java
的引用传递不同
指针 可以利用 & 符号获取变量的地址,即变量在内存中的地址,也称为指针
1 2 3 4 func paramAddress (num int ) { fmt.Println(&num) }
指针类型 指针类型表示为 *变量类型 ,列入指向一个 int 类型变量的指针类型是 *int,声明的指针变量也只能保存一种类型的值的指针,例如将。int 指针赋值给 float 指针会编译错误
可以利用 *指针变量 获取指针指向的值,还可以通过 * 改变指针指向的值,此处是指针(内存地址)不变,但是该内存处的值被改变,所有引用该内存地址的变量的值都会被改变。这里可以做到在上述函数参数传递中改变原参数的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func paramPoint () { var myInt = 10 myIntPointer := &myInt fmt.Println(myIntPointer) fmt.Println(*myIntPointer) *myIntPointer = 20 fmt.Println(*myIntPointer) fmt.Println(myInt) var myFloat float64 var myFloatPointer *float64 myFloatPointer = &myFloat fmt.Println(myFloatPointer) }
函数指针 1 2 3 4 5 6 7 8 9 10 11 func main () { aa := 5 fmt.Println(*funcPoint(&aa)) fmt.Println(aa) } func funcPoint (numPoint *int ) *int { var result = *numPoint * 10 *numPoint = 10 return &result }
数组 var 变量名 [数组大小]数据类型{字面量}
,与变量一样,数组在创建时会给数组中每一项初始化为对应数据类型的零值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 var ageArray [2 ]int ageArray[1 ] = 15 fmt.Println(ageArray[0 ]) var nameArray = [2 ]string {"aa" , "bb" }fmt.Println(nameArray[0 ]) fmt.Printf("%#v\n" , nameArray) for i := 0 ; i < len (nameArray); i++ { fmt.Print(nameArray[i], "--" ) } for index, value := range nameArray { fmt.Println(index, "---" , value) } for _, value := range nameArray { fmt.Println(value) }
使用for ... range
遍历数组,index 保存了索引,value 保存了值;这种方式不会引起无效数组的访问,当下文代码块不需要使用index 或者 value 时,可以用_
空白标识符。
切片 var 变量名 []数据类型{字面量}
,与数组不同的是切片在声明时,不指定大小。声明切片变量不会创建初始化该切片,一般使用内建函数 make()
来创建切片。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 var mySlice = []int {1 , 2 , 3 }notes := make ([]string , 10 , 20 ) notes[0 ] = "a" notes[1 ] = "b" fmt.Println(mySlice) fmt.Println(notes) var myArray = []int {1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 }sliceArray := myArray[1 :5 ] fmt.Println(sliceArray) sliceArray = append (sliceArray, 11 , 12 , 13 ) fmt.Println(sliceArray) sliceArray = append (sliceArray, 11 , 12 , 13 ) fmt.Println(sliceArray) newSlice := append (mySlice, 4 , 5 , 6 ) fmt.Println(mySlice) fmt.Println(newSlice)
通过数据创建的切片,切片是底层数组内容的视图,对于数组的修改会反映给所有的切片
Go 通过内建函数 append() 在一个切片尾部追加一个或者多个值,返回包含了老元素与新元素的新切片。切片的底层数组不能增长大小,如果在尾部添加元素数组空间不够时,会自动开辟新的空间将所有元素拷贝过来,返回新的切片指向地址。所以一般要用原切片变量接收 append() 函数的返回值,或者用其他变量接收。
切片变量的零值nil
。
可变长函数参数 1 2 3 func paramFun (paramOne string , paramTwo ...string ) {}
函数可以同时设置一个或者多个可变长参数,仅函数定义的最后一个参数可以是可变长
映射 var 变量名 map[键数据类型]值数据类型
{字面量},与切片相同,声明映射变量不会初始化创建映射变量 ,需要调用 make()
函数。
1 2 3 var myMap map [string ]int myMap = make (map [string ]int , 10 ) initMap := map [string ]int {"a" : 1 , "b" : 2 }
零值 映射变量的零值是 nil,初始化后的映射访问一个不存在的键时,得到零值时对应数据类型的零值。这种情况会造成无法判断该舰是已经存在于 map 中(不存在或者已经存在键值就是默认值)
1 2 value, ok := initMap["a" ] delete (initMap, "b" )
此时解决这个问题,在访问键值时返回了两个参数,第一个时该键对应的值,第二个时布尔值,键存在返回 true,不存在返回 false
map 和切片在作为函数参数传递时是引用传递,不同于其他类型的值传递
struct 结构体 可以定义 struct 的变量,也可以定义 struct 的类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 package mainimport "fmt" var myParamStruct struct { name string age int schoolName string grade float64 gender bool } type myTypeStruct struct { name string age int schoolName string grade float64 gender bool family } type family struct { father string mather string } func main () { myParamStruct.name = "zcy" myParamStruct.age = 22 myParamStruct.schoolName = "小学" myParamStruct.grade = 12.33 myParamStruct.gender = true fmt.Println(myParamStruct) var studentOne myTypeStruct studentOne.name = "李四" studentOne.age = 21 studentOne.schoolName = "小学" studentOne.grade = 32.33 studentOne.gender = false fmt.Println(studentOne) changeAge(studentOne) fmt.Println(studentOne) modifyStudent(&studentOne) fmt.Println(studentOne) fmt.Println((*&studentOne).name) studentTwo := myTypeStruct{ name: "赵六" , age: 33 , grade: 33.22 , schoolName: "aa" , gender: true , } studentTwo.father = "AA" studentTwo.mather = "BB" fmt.Println(studentTwo) } func changeAge (student myTypeStruct) { student.age++ fmt.Println(student) } func modifyStudent (student *myTypeStruct) { student.name = "李四-王五" fmt.Println(student) }
在函数的参数中使用 struct 类型,如果是指针访问其中的字段注意格式:(*指针变量).fieldName
,“&变量”表示指向了变量的地址即指针,“*指针变量”中 * 相当于指针运算符可以获取指针指向的值;通常可以省略 *,直接通过指针访问属性字段
如果要在其他的包中使用定义的 struct,则类型名称和对应需要导出的字段名称首字母都要大写
定义类型 基础类型定义 1 2 3 4 5 6 type MyType string type StrName string var strName StrNamestrName = StrName("zcc" ) typeTest := MyType("aaa" )
可以把任何基础类型的值转换为定义的类型
定义方法 1 2 3 4 5 6 7 8 func (m MyType) typeMethod() { fmt.Println("hello ", m) } value := MyType("zcy") value.typeMethod() //输出:hello zcy valueTwo := MyType("lisi") valueTwo.typeMethod() 输出:hello li si
func (接收器参数名 接收器类型) typeMethod() {}
方法定义是在函数名称前增加一个接收器参数类型和接收器参数名,一个方法被定义了某个类型后,可以被这个类型创建的所有变量调用。类似于其他语言中的 this 或 self 关键字(可隐式使用),但在 Go 中是显式声明调用。几点要求:
接收器参数名一般使用接收器类型名称的首字母小写
方法和接收器参数类型必须要定义在同一个包中,不能跨包定义。确保了不会为一些基础数据类型定义新方法
指针类型接收器参数 1 2 3 4 5 6 7 8 9 10 11 func (m *MyType) typeMethodPointer() { fmt.Println("hello ", m) } value := MyType("zcy") value.typeMethod() //输出:hello zcy valueTwo := MyType("lisi") &valueTwo.typeMethod() 输出:hello li si value.typeMethodPointer() (&valueTwo).typeMethodPointer()
与函数参数传递类似,如果不使用指针接收器接收的是拷贝值不会改变原值。接收器参数定义时可以利用指针类型的接收器参数。在调用时可以省略显式声明指针变量调用,Go 会做自动转换,当然也可以定义指针变量调用,但是代码中应该保持风格统一
封装嵌入 自定义类型后,内部数据并不想让包外程序直接访问到,破坏内部封闭原则。需要将部分数据封装,提供统一的对外访问方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 package calendarimport ( "errors" "fmt" ) type Date struct { year int month int day int } func (d *Date) SetDate (year int , month int , day int ) error { err := d.SetDay(day) if err != nil { return err } err = d.SetMonth(month) if err != nil { return err } err = d.SetYear(year) if err != nil { return err } return nil } func (d *Date) SetYear (year int ) error { if year < 1 { return errors.New("输入年无效" ) } d.year = year return nil } func (d *Date) SetMonth (month int ) error { if month < 1 || month > 12 { return errors.New("输入月份无效" ) } d.month = month return nil } func (d *Date) SetDay (day int ) error { if day < 1 || day > 31 { return errors.New("输入天数无效" ) } d.day = day return nil } func (d *Date) Year () int { return d.year } func (d *Date) Month () int { return d.month } func (d *Date) Day () int { return d.day } func (d *Date) Display () { fmt.Println(*d) }
例如上述代码如果 Date 的属性是可以导出的,则会破坏数据的有效性校验。未导出的变量、struct字段、函数、方法、等仍然能够被相同包的导出的函数或方法访问。Go 的未导出数据是包内可见,所以要做到包内数据安全。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package mainimport ( "HeadFirstGo/src/chapterEight/calendar" "fmt" "log" ) func main () { date := calendar.Date{} err := date.SetDate(2021 , 8 , 10 ) if err != nil { log.Fatal(err) } date.Display() event := calendar.Event{} event.Title = "测试标题" err = event.SetDate(2 , 3 , 5 ) if err != nil { log.Fatal(err) } fmt.Println(event.Year(), event.Month(), event.Day()) event.DisplayEvent() }
这里也可以定义 get 方法输出封装的属性值,一般情况下使用属性字段名首字母大写来定义 get 方法名,不是 Get 开头。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package calendarimport "fmt" type Event struct { Title string Date } var eventTest = Event{"aa" , Date{1 , 2 , 4 }}func (e Event) DisplayEvent () { fmt.Println(eventTest) }
接口类型 1 2 3 type 类型名称 interface { 接口名称(传递参数) 返参 }
接口是特定类型具有的一组方法,一个类型完全拥有了接口定义的所有方法称为满足接口,然后可以通过该类型使用该类型满足的所有接口。这里比较拗口,相当于这个类型要完全实现这个接口。
类型中的方法名、参数类型、返回值必须都和接口定义一样,类型可以有其他的方法,但是不能比接口定义中的少,否则就不满足那个接口。当类型的方法比接口定义多时,在调用时,如果类型参数被声明为接口类型,则不能调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 type MyInterface interface { PlayMusic() MusicName() } type Piano struct { Name string } func (p Piano) PlayMusic () { fmt.Println(p.Name, "演奏钢琴曲" ) } func (p Piano) MusicName () { fmt.Println("我是钢琴" ) } func (p Piano) Other () { fmt.Println("钢琴可以演奏其他的吗?" ) } func main () { var piano music.MyInterface = music.Piano{Name: "钢琴" } piano.MusicName() piano.PlayMusic() pianoType, ok := piano.(music.Piano) if ok { pianoType.Other() } }
如果定义了一个不需要任何方法的接口,type AnyThing interface{}
它会被任何类型满足。
goroutine 和 channel goroutine 实现 Go 程序的并发执行,main 函数的执行也是启动一个 goroutine
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func main () { go playA() go playB() time.Sleep(1 * time.Second) fmt.Println("执行完毕" ) } func playA () { for i := 0 ; i < 10 ; i++ { fmt.Println("A" , i) } } func playB () { for i := 0 ; i < 10 ; i++ { fmt.Println("B" , i) } }
channel goroutine 之间的通信通道,使用内建 make() 函数来创建 channel 变量:myChannel := make(chan float64, 3)
,3 表示创建缓冲区的大小,此时只有缓冲区被填满时继续发送会产生阻塞。
当前有 goroutine A 通过channel C 向 goroutine B发送数据:当 A 开始发送数据到 C 后,A 的执行就会被阻塞,B 开始接收数据时,B的执行也会被阻塞,当 B 处理完成后通过 C 发送数据,A 完成接收,A 和 B 继续执行后面的程序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 func abcPlay (channel chan string ) { channel <- "a" channel <- "b" channel <- "c" } func defPlay (channel chan string ) { channel <- "d" channel <- "e" channel <- "f" } func channelPlay () { c1 := make (chan string ) c2 := make (chan string ) go abcPlay(c1) go defPlay(c2) fmt.Println(<-c1) fmt.Println(<-c2) fmt.Println(<-c1) fmt.Println(<-c2) fmt.Println(<-c1) fmt.Println(<-c2) }
特性 Tips defer defer 代码
defer 关键字确保函数调用发生,即使函数因为异常提前退出。延迟函数或者方法的调用,当程序出现 panic 异常奔溃时,延迟的函数仍然会被调用,多个延迟调用函数时,延迟函数执行的顺序与被延迟的顺序(代码顺序)相反(入栈出栈)。defer 是在当前函数生命周期结束后调用。如果函数正常执行结束,也是先执行 return 语句,在执行 defer 语句。
recover() 内置的 recover()
函数阻止程序奔溃,只返回 nil。可以在可能引发函数奔溃的代码之前延迟调用。但是一般不直接 defer recover() 声明调用,可以包装为其他函数。这样可以保证程序能正常执行完。
1 2 3 4 5 6 7 8 func freakOut () { defer calmDown() panic ("程序奔溃" ) } func calmDown () { recover () }
函数作为类型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func viewHandler (writer http.ResponseWriter, request *http.Request) { message := []byte ("hello web!" ) _, err := writer.Write(message) if err != nil { log.Fatal(err) } } func main () { http.HandleFunc("/hello" , viewHandler) err := http.ListenAndServe("localhost:8080" , nil ) log.Fatal(err) } var myFunc func (http.ResponseWriter, *http.Request) //定义函数类型,如果函数有返参,这里也需要声明myFunc = viewHandler myFunc(nil , nil )
函数类型变量也可以作为函数或者方法的参数传递,但是这里要注意参数声明必须要和传递函数的参数数量、类型、返参数量、返参类型相同。