Go,又称Golang,是google开源的编程语言,原生支持并发编程
- 没有对象的概念,不支持继承、多态,没有泛型、try…catch
- 有接口、函数式编程、csp并发模型(goroutine+channel)
.go文件基本结构
/*
一个包可以对应多个目录,一个目录只能对应一个包,同一目录下包名需要保持一致
*/
package main
/*
如果导入的包依赖其它包,会先导入其它包
导入包时,会先初始化包中的公共常量、变量,如果有init()方法,会自动执行init()方法
*/
import (
"fmt"
complex "math/cmplx" //定义别名,可通过别名来引用
)
/*
同一包中,不能有同名的公共常量、变量、自定义的方法名,只能有一个main()方法
*/
const name = "chy"
var age = 1
func init() {
fmt.Println("执行init()...")
fmt.Println(name)
fmt.Println(age)
}
/*
执行入口是main包下的main()方法
执行main时,会先导入依赖的包,再初始化常量、变量,如果有init()还会执行init(),最后执行main()
*/
func main() {
fmt.Println("执行main()...")
complex.Abs(1 + 1i)
}
变量
//go是先写变量名,再写数据类型
var name1 string = "chy"
//可以缺省类型,会自动推断类型
var name2 = "chy"
//可以同时声明多个变量,进行多个变量的赋值
var name3, age3 = "chy", 20
name3, age3 = "chy1", 21
//可以指定数据类型,这个数据类型是给前面所有的变量指定的
//var age4, age5 int = 20, 20
//也可以这样写
var (
name6 = "chy"
age6 = 20
)
func f1() {
//函数外可以定义未被使用的变量,函数内不能随便定义变量,定义了变量就必须使用
var age1 = 20
//直接用 = 赋值不能算是使用
age1 += 1
//在函数内,可以用 := 表示声明并赋值,相当于冒号相当于var
name1 := "chy"
name2, age2 := "chy", 20
fmt.Println(name1, name2, age2)
}
- 函数的作用域是当前包中,函数外声明的变量作用域也是当前包中
- 函数内声明的变量的作用域是局部变量,作用域是当前函数内
常量
- golang中常|变量名的大小写有特殊含义,常量名一般不全大写
- golang没有专门的枚举类型,可以用常量实现类似的功能
- iota是一个内置的自然数递增器,需要和const搭配使用
//常量的用法和变量差不多,但常量声明时就需要赋值,且后续不能修改值;变量可以先声明再赋值,但声明时必须指定数据类型
var name1 string
const name1 = "chy"
//如果要定义多个常量,更推荐这种写法
const (
name2 = "xxx"
port2 = 9000
)
//自增型常量,0,1,2... 可以用_跳过当前值
const (
java = iota
_
python
golang
)
//支持表达式,1,3,5...
const (
a = iota*2 + 1
b
c
)
//1(2^0),1024(2^10),1024*1024(2^20)...
const (
byte = 1 << (10 * iota)
kb
mb
gb
)
数据类型
内建数据类型
- (u)int8、(u)int16、(u)int32、(u)int64、(u)int:go没有short、int、long之类的整型,以数字标识位数,/8即是该类型的字节数,以对应不同长度的整型。(u)int由操作系统位数决定,32位操作系统分配32位,64位操作系统分配64位。u表示无符号。
- uintptr 无符号整型,用于存放一个指针
- float32、float64:分别对应其它语言中的float、double。不用像其它语言一样需要在值末尾加f。
- complex64、complex128:复数,64是32位实数+32位虚数,128是64位实数+64位虚数。
- byte:uint8的别名
- rune:int32的别名,相当于其它语言中的char
- bool
- string:工具类strings提供了很多操作字符串的方法
//双引会解析其中的转义字符,反引号则不解析其中的转义字符
var str1, str2 = "hello\nworld\n", `hello\nworld\n`
fmt.Println(str1, str2)
//复数,cmplx库也提供了很多操作复数的函数
var c1, c2 = 1 + 1i, 2 + 2i
fmt.Println(c1 + c2)
已声明但未赋值,也有默认的初始值,整型、浮点型初始值为0,字符串初始值为空串,bool型初始值为false。
指针 Pointer
参数传递方式
- 值传递:把实参的值copy一份传给形参,如果是对象,copy的是对象的引用。java中只有值传递。
- 指针传递:传入的实参的指针|地址,对形参的操作就是对实参的操作。
- 引用传递:形参是实参的别名,对形参的操作就是对实参的操作。
//值传递
func f1(a int) {
a = 1
}
//指针传递。参数的数据类型是*int,在原类型前加*。可以把 *形参变量 当做 实参变量
func f2(pa *int) {
fmt.Println(*pa)
*pa = 100
}
//指针传递,交换2个变量的值
func swap(a, b *int) {
*a, *b = *b, *a
}
//引用传递,此处以c++为例。&a可以理解为 给实参取别名a
void swap(int &a, int &b) {
int temp = a;
a = b;
b= temp;
}
int main() {
int x = 1;
int y = 2;
swap(x, y);
return 0;
}
结构体
//结构体。里面定义字段即可
type node struct {
value int
pre, next *node
}
//自定义的工厂函数,golang允许返回局部变量的地址给外部使用
func createNode(value int) *node {
return &node{value: value}
}
//调用方式是node.getValue()
func (node node) getValue1() int {
return node.value
}
//调用方式是getValue2(node)
func getValue2(node node) int {
return node.value
}
//参数不加*是传值,copy整个实参对象传给形参,对形参的修改不会影响到实参;加*是传地址,对形参的修改就是对实参的修改
func (node *node) setValue1(value int) {
node.value = value
}
//xxx(*xxx,xxx)调用,*作为参数时,需要&取地址传入
func setValue2(node *node, value int) {
node.value = value
}
func main() {
//各字段都会初始化为对应类型的默认值,指针类型的默认初始值是nil
var head node
//0
fmt.Println(head.value)
//nil
fmt.Println(head.pre)
//报错,需要先判断,head.pre != nil 才进行操作
//fmt.Println(head.pre.value)
//可以用new直接创建,效果和var head node一样,都是初始化各字段为该类型的默认初始值
head.pre = new(node)
//可以调用自定义的工厂函数创建
head.next = createNode(10)
//也可以node{}创建。node{}和var head node效果差不多,各字段都是使用默认的初始值
head = node{}
//可以只给部分字段赋值,但需要指定字段名
head = node{value: 10}
//给全部字段赋值时,可以缺省字段名
head = node{10, nil, nil}
//可以直接(访问|修改)操作字段。指针类型赋的是地址
head.value = 0
head.pre = &node{}
head.next = &node{100, nil, nil}
//如果定义函数时:参数是指针(加*),会自动取head的地址(&head)传入;如果参数没有加*,会自动copy整个对象传入
head.setValue1(111)
//如果定义函数时:参数是指针(加*),需要&取地址传入;如果参数没有加*,直接传入head即可
setValue2(&head, 111)
fmt.Println(head.getValue1())
fmt.Println(getValue2(head))
//数组形式
nodeArr := []node{
{},
{value: 1},
{1, nil, nil},
}
fmt.Println(nodeArr)
}
- golang只支持封装,不支持继承和多态。
- golang中,局部变量不一定在栈上创建,不一定随着方法调用的结束而销毁。如果局部变量暴露出去给外部使用,这个局部变量会在堆上创建,由gc回收。
- 如果要修改实参,参数使用指针;如果结构体变量很大,copy开销大,也可以考虑直接传入指针。
接口
接口的定义和实现
//定义一个接口,里面包含一些方法
type UserService interface {
findUsernameById(id int) string
}
//定义一个结构体,实现接口中的所有方法
//实现接口中的所有方法,这个结构体就算实现了接口,只实现了部分方法则不算
type UserServiceImpl struct {
}
//方法要写成xxx.xxx()的调用形式
func (userServiceImpl UserServiceImpl) findUsernameById(id int) string {
return "xxx"
}
func f1(userService UserService, id int) {
username := userService.findUsernameById(id)
fmt.Println(username)
}
func main() {
userServiceImpl := UserServiceImpl{}
f1(userServiceImpl, 1)
}
任何类型
interface{ }表示任意类型,相当于java中的Object
func f2(i interface{}) {
fmt.Println(i)
}
func main() {
var a interface{} = 1
//可以更改值的类型
a = "xxx"
f2(a)
}
接口的组合
Get接口
type Get interface {
Get(url string) string
}
type GetImpl struct {
}
func (get GetImpl) Get(url string) string {
return "this is get response:" + url
}
Post接口
type Post interface {
Post(url string) string
}
type PostImpl struct {
}
func (post PostImpl) Post(url string) string {
return "this is the post response:" + url
}
Http接口
//相当于接口的继承,可以继承其它接口,继承之后可以调用父接口中的方法
type Http interface {
//可包含一些其它接口,这些接口相当于当前接口的父接口
Get
Post
//可包含一些其它方法
}
type HttpImpl struct {
//父接口的实现类
GetImpl
PostImpl
}
//其它方法实现
使用
func f1(http Http) {
url := "http://www.baidu.com"
getResp := http.Get(url)
postResp := http.Post(url)
fmt.Println(getResp, postResp)
}
func main() {
http := HttpImpl{}
f1(http)
}
内建容器
数组 Array
//元素不够时,自动填充为该类型的初始值
var arr1 = [5]int{1, 2, 3}
//可以将长度写成... 自动取元素的实际个数
var arr2 = [...]int{1, 2, 3}
//可以先声明、后续赋值,已声明、但未赋值的数组,各元素为该类型的初始值
var arr3 [11]int
//二维数组,5行4列,元素是一维数组,元素不够时自动填充(该类型的初始值的一维数组)
var arr4 = [5][4]int{{}, {1}, {1, 2}, {1, 2, 3}}
func f1(arr [5]int) {
arr[0] = 100
}
func f2(arr [...]int) {
arr[0] = 100
}
func main() {
//arr1和f2的参数类型不匹配,不能传入
arr1 := [5]int{1, 2, 3}
f1(arr1)
f2(arr1)
//arr2的实际类型是[3]int,和f1、f2的参数类型均不匹配,不能传入
arr2 := [...]int{1, 2, 3}
f1(arr2)
f2(arr2)
//arr3已声明、未赋值,类型还是[...]int,可以传给f2,不能传给f1
var arr3 [...]int
f1(arr3)
f2(arr3)
}
- 数组属于值传递,传的并不是数组的地址、引用,而是把整个数组copy一份传给形参,对形参数组的修改不会影响到实参数组。
- [3]int、[5]int、[…]int是三种数据类型,传入的数据类型要对应。
- golang中一般不直接使用数组,而是使用切片代替。
切片 Slice
切片:从数组或其它切片上切下一个连续片段。
//与数组相比,切片不指定长度
var arr = [...]int{1, 2, 3, 4, 5}
var slice = []int{1, 2, 3, 4, 5}
func main() {
//切片的数据可以来源于数组,也可以来源于另一个切片,此处是来源于另一个切片
//[1,3)
var slice1 = slice[1:3]
//[0,3)
var slice2 = slice[:3]
//到末尾
var slice3 = slice[1:]
//全部元素
var slice4 = slice[:]
//不管来源于数组、还是另一个切片,切片的底层数据结构都是数组
//切片只是底层数组的一个视图,对切片的修改实际是对底层数组的修改,对底层数组的修改也会同步到对应的视图(切片)上
slice[0] = 100
slice1[1] = 1000
fmt.Println(slice1, slice2, slice3, slice4)
}
无论是数组还是切片,golang都不支持负数索引。
slice的组成
- ptr:slice中第一个元素的指针
- len:slice的长度(实际元素个数)
- cap:可用容量,ptr到底层数组末尾都属于可用范围,slice扩展时只能向后扩展,使用底层数组中的位置,数组本身不可扩展,如果底层数组容量不够,会自动扩容——声明一个容量更大的数组,将原数组的元素复制到新数组中。
slice的扩展
slice := []int{0, 1, 2, 3, 4}
//只有2个元素0、1
slice1 := slice[0:2]
//使用slice时只能使用slice本身的索引,不能越过slice的边界使用底层数组的索引,否则会报越界
//slice1[2] = 10
//slice不能向前扩展,但可以向后扩展。append()会将元素添加到slice尾部,即底层数组中slice的后面
//返回的是新分片,原分片不变
slice1 = append(slice1, 2, 3, 4)
fmt.Println(slice1)
//切片属于引用类型,已声明、未赋值时默认为nil,可以把nil理解为创建了容器对象,但这个容器中没有元素、是空的
var slice []int
//true
fmt.Print(slice == nil)
for i := 0; i < 100; i++ {
fmt.Println(len(slice), cap(slice), slice)
slice = append(slice, i)
}
slice的常见操作
//slice可以从数组、其它slice创建,也可以使用make()直接创建,会初始化各元素为该类型的默认值
//第一个参数指定slice的len,第二个参数指定底层数组的cap,第二个参数可选
slice1 := make([]int, 10)
slice2 := make([]int, 10, 20)
//修改元素
slice1[0] = 1
//在末尾添加元素
slice1 = append(slice1, 1)
//删除头部几个元素
slice1 = slice1[2:]
//删除尾部几个元素
slice1 = slice1[:len(slice1)-2]
source := []int{1, 3, 5, 7, 9}
target := []int{0, 2, 4, 6, 8, 10}
//复制分片元素,第一个参数指定目标slice,第二个参数指定源slice
//会将source中元素都复制到target中,是覆盖并非添加,如果target中放不下,不会自动扩容,放不下的部分就不要了
copy(target, source)
集合 Map
// map[key类型]value类型。支持复合map,value也可以是一个map
m1 := map[string]string{
"name": "chy",
//最后一个元素末尾需要加逗号
"age": "20",
}
//通过make创建时,创建的是空map,!=nil
m2 := make(map[string]string)
//已声明、未赋值的的map,==nil
var m3 map[string]string
//获取元素个数
count := len(m1)
//获取key对应的value。如果key不存在,会返回对应类型的默认初始值作为value
//name := m1["name"]
//所以一般用2个变量接收返回值,第一个是value,第二个是bool型 标识key是否存在
name, isExist := m1["name"]
fmt.Println(isExist)
if isExist {
//key存在
fmt.Println(name)
} else {
//key不存在
}
//删除指定的键值对
delete(m1, "name")
- map是无序的
- map使用哈希表存储元素,但不需要手动实现hashCode()、equals()方法
- 除slice、map、function以外的内建类型,以及成员字段均不是这三种类型的struct,都可以作为key
类型转换
go不支持类型的隐式转换,eg. 不会自动将0、1、空串等转换为bool型,需要手动转换。
var a float32=1.5
//目标类型(变量名),不能直接 目标类型(值)
var b int=int(a)
fmt.Println(a,b)
类型扩展
golang没有继承,可以通过组合、定义别名来扩展现有类型。
组合
type Node struct {
Value int
Pre, Next *node
}
type NodeExt struct {
Node *node
Score float32
}
func main() {
node := Node{10, nil, nil}
//如果定义中没有使用指针(加*),此处就不用&取地址,直接传入即可,但这种对内部字段的修改不会影响|同步到实参
nodeExt := NodeExt{&node, 10.0}
fmt.Println(nodeExt)
}
定义别名
//定义别名,Queue是[]int的别名
type Queue []int
//结构体的字段如果要暴露给其它包访问,需要将字段名首字母大写;如果要把方法暴露给其他包使用,只需将方法名首字母大写,不需要将形参名首字母大写
func (q *Queue) Push(value int) {
*q = append(*q, value)
}
func (q *Queue) Pop() int {
head := (*q)[0]
*q = (*q)[1:]
return head
}
func (q *Queue) IsEmpty() bool {
return len(*q) == 0
}
func main() {
queue := Queue{}
queue.Push(10)
fmt.Println(queue.Pop())
fmt.Println(queue.IsEmpty())
}