一、go struct静态函数、结构体、函数、方法、接口
编程语言都是相通的,go和c的结构体structure,相当于java的class类,slice切片相当于java的ArrayList,同样的也有map;指针就是一个地址引用。也有接口,只是C语言和go没有对象的概念,但是却有类型的概念的type关键字。
go的struct结构体
结构体struct就相当于java中的类class,用于定义属性和方法。
定义一个Person,有string型的Name和int型的Age两个属性:
type Person struct {
Name string
Ageint
}
创建一个Person实例: 第一种方式:
var p Person = Person{}
这时,所有属性值都是零值。 直接打点给属性赋值:
p.Name = "zhangsan"
p.Age = 18
var p Person = Person{"zhangsan", 18}
一个个把属性值列出来,要求一个都不能少。 第二种方式:
var p Person = Person{Name: "zhansan", Age: 18}
这时,Name属性值为zhangsan,Age属性值为18
把属性名与值一对对列出来,可以只列需要的属性,其他的属性值为零值。注意,属性名没有用双引号括住。
第三种方式:
var pptr *Person = new(Person)
注意,与上面两种方式不同,用new函数生成的是指向实例的指针,注意,不是new关键字,而是new函数,java中是new关键字。new(Person)相当于&Person{},返回都是指针类型。 我们可以用reflect.TypeOf()函数来查看变量类型
func main() {
a := Person{"张三", 90}
log.Println(reflect.TypeOf(a))
var pptr *Person = new(Person)
log.Println(reflect.TypeOf(pptr))
}
会打印出
main.Person
*main.Person
可以看出,a是Person类型,pptr是指针类型,类型名以星号开头。
var p Person = new(Person)会报编译错误,提示Cannot use ‘new(Person)’ (type *Person) as type Person。
我们既可以通过实例访问其成员变量,又可以直接通过指向该实例的指针访问其成员变量
如pptr.Name = “lisi”
fmt.Println(pptr.Name)
定义一个Employee,有Person型的p和int型的salary两个属性:
type Employee struct {
p Person
salaryint}
创建一个Employee实例:
var e Employee = Employee{}
var e Employee = Employee{p: Person{}, salary: 10000}
var eptr *Employee = new(Employee)
如果属性类型是自定义的struct的话,属性名可以省略,如下:
type Employee struct {
Person
salaryint
}
此时创建一个Employee实例:
var e Employee = Employee{}
var e Employee = Employee{Person: Person{}, salary: 10000}
这里由于属性名省略了,所以花括号中的key只能是我们自定义的struct了。
var eptr *Employee = new(Employee)
通过e访问Name、Age属性:
在属性名不省略时,只能通过e打点获取Person实例,然后Person实例再打点操作Name、Age属性,示例如下:
func main() {
var e Employee= Employee{p: Person{}, salary: 10000}
e.p.Name= "zhangsan"fmt.Println(e)
}
在属性名省略时,我们既可以通过e打点获取Person实例,然后Person实例再打点操作Name、Age属性,也可以直接e打点操作Name、Age属性,这就有点像java的继承了。示例如下:
func main() {
var e Employee= Employee{Person: Person{}, salary: 10000}
e.Person.Name= "zhangsan"e.Age= 30fmt.Println(e)
}
函数
在go中,函数是一等公民。 函数可以有不定长入参,可以有多个返回值,可以赋值给变量,可以作为函数的入参和出参。
函数定义:
func func_name(i int, s string) int {}
定义函数f0,有一个int型入参、无出参:
func f0(i int) {
}
定义函数f1,有一个int型入参、一个string型入参,一个int型出参:
func f1(i int, s string) int{return 0}
定义函数f2,有两个int型入参,一个string型出参:
func f2(i, j int) string {return ""}
如果多个入参类型一样的话,可以省略前面几个参数的类型关键字,而只保留最后一个参数的类型关键字。
定义函数f3,有两个int型入参,一个string型出参,一个int型出参:
func f3(i, j int) (string, int) {return "", 0}
多个出参的话,要用括号包起来。
定义函数f4,有不定长个int型入参,一个int型出参:
func f4(p ...int) int{
returnlen(p)
}
定义函数f5,有一个string型入参和不定长个int型入参,一个int型出参:
func f5(s string, sl ...int) int{
return len(s) +len(sl)
}
函数赋值给变量:
func main() {
var f func(int, string) int =f1
f(1, "a")
}
变量的类型可以通过fmt.Println(reflect.TypeOf(f))打印出来看。
函数作为函数的入参和出参
func S(f func(i int) int) func(s string) string {
returnfunc(s string) string {returnstrconv.Itoa(f(len(s)))
}
}
func main() {
var p= S(func(i int) int{return i + 10})("100")
fmt.Println(p)
}
我们在定义函数时,如果入参是一个struct实例,则底层会复制一个struct实例作为入参,函数对实例的改动不会影响原来的struct实例。所以,入参最好是指向struct实例的指针,这样就不用复制了,函数对实例的改动也会体现到原来的struct实例上。示例如下:
func changeName(e Person) {
e.Name= "zhangsan"}
func changeNamePtr(e*Person) {
e.Name= "zhangsan"}
func main() {
var p Person=Person{}
changeName(p)
fmt.Println(p)
var ptr*Person = new(Person)
changeNamePtr(ptr)
fmt.Println(ptr)
}
方法
方法和函数长得差不多,区别是方法定义时func关键字后面紧跟的是括号,括号里面是调用者形参及调用者类型,之后才是func_name,再之后是括号,括号里面是入参形参及入参类型(没有入参的情况下括号不可以省略),最后是出参类型,如果有多个出参,则出参类型要用括号括住。
从上面描述可以看出,在方法定义时,方法的调用者类型就已经是确定了的,只有这个调用者类型的实例或者指针才能调用这个方法。方法定义时的调用者类型既可以是普通类型,也可以是指针类型,方法最终的调用者也是既可以是普通类型,也可以是指针类型。
示例如下:
type JavaProgrammer struct {
}
func (jp*JavaProgrammer) helloWorld() {
fmt.Println("System.out.println(\"Hello World!\");")
}
type GoProgrammer struct {
}
func (gp GoProgrammer) helloWorld() {
fmt.Println("fmt.Println(\"Hello World!\")")
}
func main() {new(JavaProgrammer).helloWorld()
javaProgrammer :=JavaProgrammer{}
javaProgrammer.helloWorld()new(GoProgrammer).helloWorld()
goProgrammer :=GoProgrammer{}
goProgrammer.helloWorld()
}
JavaProgrammer的 helloWorld方法是调用者类型定义成指针类型的方法,第一次调用是用指向JavaProgrammer实例的指针调用,第二次调用是用JavaProgrammer实例调用。
GoPrammer的helloWorld方法是调用者类型定义成普通类型的方法,第一次调用是用指向GoProgrammer实例的指针调用,第二次调用是用GoProgrammer实例调用。
相比来说,调用者类型定义成指针类型的方法要比定义成普通类型的方法好,因为在调用时可以避免内存复制。
func main() {
a := Person{"张三", 90}
log.Println(1, unsafe.Pointer(&a.Name))
a.Ps()
a.Ps2()
}
func (e Person) Ps() {
log.Println(0, unsafe.Pointer(&e.Name))
}
func (e*Person) Ps2() {
log.Println(0, unsafe.Pointer(&e.Name))
}
在Ps方法内部打印的入参a的Name属性地址和直接打印实例a的Name属性地址不一样,而在Ps2方法内部打印的入参a的Name属性地址和直接打印实例a的Name属性地址一样,由此可以证明调用者类型定义成指针类型的方法在调用时,不会有内存复制,而调用者类型定义成普通类型的方法在调用时,会有内存复制。
func main() {
a := Person{"张三", 90}
log.Println(1, unsafe.Pointer(&a))
a.Ps()
a.Ps2()
}
func (e Person) Ps() {
log.Println(0, unsafe.Pointer(&e))
}
func (e*Person) Ps2() {
log.Println(0, unsafe.Pointer(&e))
}
打印出来的三个值各不相同,难道说形参是指针类型的方法在调用时,也会有内存复制?
func (p *Person) exchange(p0 *Person) {}
-
*第一个括号中的p Person表示本方法调用者只能是Person实例或者指向Person实例的指针,且是引用传递,如果在方法中改变了调用者的属性,会在方法外体现。 -
*第二个括号中的p0 Person表示方法入参是引用传递,如果在方法中改变了入参的属性,会在方法外体现。
方法的继承
还是以上面的Person、Employee举例。
假设Person有个changeName方法,定义如下:
func (pptr *Person) changeName(name string) {
pptr.Name=name
}
假如在定义Employee时省略了Person类型属性的名称,则我们可以通过e直接打点调用changeName方法,示例1如下:
func main() {var e Employee= Employee{Person: Person{Name: "wanglaoji"}}e.changeName("lisi")
fmt.Printf("%+v\n", e)
}
假设Employee也有个changeName方法,则直接通过e打点调用changeName方法的话,调用的其实是Employee的changeName方法,而不是Person的changeName方法。示例2如下:
func (pptr *Person) changeName(name string) {
pptr.Name=name
}
func (eptr*Employee) changeName(name string) {
eptr.Name= name +name
}
func main() {var e Employee= Employee{Person: Person{Name: "wanglaoji"}}
fmt.Printf("%+v\n", e)
}
假如Person有个change方法,在change方法中调用了changeName方法,那么e调用change方法时,执行的是Employee的changeName方法呢,还是Person的changeName方法呢?示例3如下:
func (pptr *Person) changeName(name string) {
pptr.Name=name
}
func (pptr*Person) change(name string) {
pptr.changeName(name)
}
func (eptr*Employee) changeName(name string) {
eptr.Name= name +name
}
func main() {
var e Employee= Employee{Person: Person{Name: "wanglaoji"}}
e.change("lisi")
fmt.Printf("%+v\n", e)
}
实测执行的是Person的changeName方法。
假如Employee也有个change方法,在change方法中调用了changeName方法,那么e调用change方法时,执行的是Employee的changeName方法呢,还是Person的changeName方法呢?示例4如下:
func (pptr *Person) changeName(name string) {
pptr.Name=name
}
func (pptr*Person) change(name string) {
pptr.changeName(name)
}
func (eptr*Employee) changeName(name string) {
eptr.Name= name +name
}
func (eptr*Employee) change(name string) {
eptr.changeName(name)
}
func main() {
var e Employee= Employee{Person: Person{Name: "wanglaoji"}}
e.change("lisi")
fmt.Printf("%+v\n", e)
}
实测执行的是Employee的change方法和Employee的changeName方法。
- 总结:
在go中没有继承,不论是属性还是方法。都是太任性的省略搞的鬼。 在go中没有静态方法的概念。
接口
定义格式:
type RedPacketService interface{
add(s string)
delete(s string)
update(s string)
query(s string) string
}
go中没有implements或者相同作用的关键字。要实现某个接口,不是在定义struct的时候显式声明要实现某个接口,而是采用duck typing的方式,即只要实现了接口的所有方法,就认为这个struct实现了这个接口,就可以向上转型。示例如下:
type Programmer interface{
helloWorld()
}
type JavaProgrammer struct {
}
func (jp*JavaProgrammer) helloWorld() {
fmt.Println("System.out.println(\"Hello World!\");")
}
type GoProgrammer struct {
}
func (gpGoProgrammer) helloWorld() {
fmt.Println("fmt.Println(\"Hello World!\")")
}
func main() {
var p Programmer= new(JavaProgrammer)
p.helloWorld()
p= new(GoProgrammer)
p.helloWorld()
}
Programmer接口只有一个helloWorld方法,JavaProgrammer实现了这个方法,所以JavaProgrammer实现了Programmer接口,同理,GoProgrammer也实现了Programmer接口。
这里有一点需要注意,JavaProgrammer是pointer receiver实现,GoProgrammer是value receiver实现,而实现接口的方法时,pointer receiver和value receiver是不一样的,参考go官方文档https://golang.org/doc/effective_go.html#pointers_vs_values
The rule about pointers vs. values for receivers is that value methods can be invoked on pointers and values, but pointer methods can only be invoked on pointers. 也就是说,value receiver方式实现的方法可以被实例和指向实例的指针调用,而pointer receiver方式实现的方法只能被指向实例的指针调用。
上例中,直接用JavaProgrammer{}生成一个实例,然后赋值给p会报编译错误,提示Cannot use ‘JavaProgrammer{}’ (type JavaProgrammer) as type Programmer. Type does not implement ‘Programmer’ as ‘helloWorld’ method has a pointer receiver.
假如再给Programmer接口添加一个hi方法,由于JavaProgrammer和GoProgrammer都没有实现这个方法,所以这两个struct都没有实现Programmer接口,所以把指向JavaProgrammer实例的指针或指向GoProgrammer实例的指针赋值给Programmer类型变量时会报编译错误。
接口最佳实践:
使用小的接口定义,接口中方法数不要太多; 较大的接口定义,由多个小接口定义组合而成; 只依赖于含必要功能的最小接口
type Reader interface{
read() string
}
type Writerinterface{
write()
}
type ReaderWriterinterface{
Reader
Writer
}
func getMessage(reader Reader) string {returnreader.read()
}
func getMessage2(readerWriter ReaderWriter) string {returnreaderWriter.read()
}
如上例,Reader接口提供read()方法,Writer接口提供write()方法,而ReaderWriter接口是由Reader接口和Writer接口组合起来的。函数getMessage内部只需调用Reader的read()方法,那么形参类型只需是Reader,而不应该是比Reader接口功能更多的ReaderWriter接口。
话说ReaderWriter接口和Reader接口是啥关系呢?是ReaderWriter接口继承了Reader接口,还是说Reader接口是ReaderWriter接口的成员变量?
空接口 interface{}
interface{}可以用作任意类型的形参,示例如下:
func PX(s interface{}) {if s0, ok := s.(int); ok {
fmt.Println("int=", s0)
}else if s0, ok :=s.(string); ok {
fmt.Println("string=", s0)
}else if s0, ok :=s.(bool); ok {
fmt.Println("bool=", s0)
}else{
fmt.Println("unknown type")
}
}
func main() {
PX("1")
PX(1)
PX(false)
PX(new(Programmer))
}
以上,s可以是任意类型变量。s.(int)有2个返回值,第一个返回值是s,第二个返回值是true或者false,如果s是int型变量,就是true,否则就是false。所以通过判断第二个返回值是否是true,就能判定s是否是int型变量。
以上写法还可以通过switch来简化判断,如下:
func PX(s interface{}) {switchs.(type) {case int:
fmt.Println("int=", s)casestring:
fmt.Println("string=", s)casebool:
fmt.Println("bool=", s)default:
fmt.Println("unknown type")
}
}
func main() {
PX("1")
PX(1)
PX(false)
PX(new(Programmer))
}
s.(type) 必须跟在switch后面,否则会报编译错误。
error
error是go自带的一个接口,只有一个Error()方法,没有入参,有一个string类型的出参。 go自带了很多实现了error接口的struct,如errorString、TimeoutError等。 errorString只有一个string型的属性,我们可以通过errors包的New(text <font string函数获取指向errorString实例的指针。示例如下:
func Check(i int) (int, error) {
if i < 0{
return -1, errors.New("negative")
} else{
return 1, nil
}
}
func main() {
i, err := Check(-1)
if err !=nil {
fmt.Println("something wrong")
} else{
fmt.Println("going on,i=", i)
}
}
error实例能否用==比较?
github.com/pkg/errors是一个比较好用的error相关的包。
Java Thead为线程与go的Goroutine
Java Thead为线程,线程是操作系统调度的最小单位。
1、线程间切换(不出让情况下):抢占式调度,随机执行。
2、实现:继承Thread类或者实现Runnable接口,Callable类似,或者通过线程池。
3、线程切换代价大
4、一般通过共享内存通信
Goroutine 为go并发执行的调度单位。
1、Goroutine间切换:业务要求切换(runtime.Gosched())、发生IO,等待chanel导致执行阻塞,否则单核goroutine是串行执行。
2、实现:function前加 ‘go’关键字
3、goroutine切换代价小
4、一般通过通信共享内存 goroutine可以说是golang实现的协程,不归操作系统管理。
Golang-切片slice与Java的ArrayList集合对比
Golang里面的切片,很像Java中的ArrayList,可以实现数组的动态扩缩容。
创建
List<String> list = new ArrayList<String>();
String[] arrayRefVar = new String[5];
slice := make([]string, 5)
Java中是通过new创建一个ArrayList,Go中是通过make来创建,这边就是语法上的不同。
赋值
在语法上趋近与数组的编写形式,而不像ArrayList的语法那么重量级。但是GO语言的数组[]号在类型的前面,这个是Java开发语言的同学所不适应的。
初始化赋值
String[] str = {"Red","Blue"};
str := []string{"Red","Blue"}
直接赋值
int[] b = new int[3];
b[0] = 100;
b := make([]int,3)
b[0] = 100
是不是跟Java的数组赋值差不多,GO语法中位置好多都是跟Java中相反的。
增加元素
java的ArrayList底层扩容是按照原数组的1.5倍,go的切片额外追加赋值的话,如果原数组容量不够,会在原来数组的基础上扩容2倍。
String[] strs = new String[4];
strs[0] = "1";
strs[1] = "2";
strs[2] = "3";
strs[3] = "4";
List<String> list = new ArrayList<>(Arrays.asList(strs));
List<String> newList = list.subList(1,3);
System.out.println(newList.toString());
list.add("5");
System.out.println(list.toString());
strs := make([]string,4)
strs[0] = "1";
strs[1] = "2";
strs[2] = "3";
strs[3] = "4";
newStrs := strs[1:3]
t.Log(newStrs)
strs = append(strs,"5")
t.Log(strs)
这段代码的意思是创建一个动态数组,可以看到Go的语法要比Java要简洁的多。
遍历
GO语言for后面不用大括号,剩下的写法基本上一样。
String[] strs = {"1","2","3","4"};
List<String> list = new ArrayList<>(Arrays.asList(strs));
for(String str : list){
System.out.println(str);
}
System.out.println("---------------");
for(int i=0;i<list.size();i++){ System.out.println("index="+i+",value="+list.get(i));
}
strs := []string{"1","2","3","4"}
for index, value := range strs{
fmt.Printf( "index=%d, value=%s\n", index, value)
}
for index:=0;index<len(strs);index++{
fmt.Printf( "index=%d, value=%s\n", index, strs[index])
}
二、 go语言与java的语法对比
1.编译与运行
##java用jvm做到程序与操作系统的隔离. 一次性编译生成的class文件,处处可以运行
## 简单来说 windows上编译的出class文件,拿到安装了jvm的linux上可直接可以执行
javac Hello.java
java Hello
## go 在不同的操作系统build出对应的可执行文件
go build hello.go
## 当然直接编译+执行亦可,慢一些
go run hello.go
2.main方法与测试类
public class Hello {
public static void main(String[] args) {
System.out.println("hello java");
}
}
public class Test {
@org.junit.Test
public void test() {
System.out.println("this is test");
}
}
``
?```go
package main
import "fmt"
func main() {
fmt.Println("hello world")
}
import (
"fmt"
"testing"
)
func TestFirstTry(t *testing.T) {
fmt.Println("dadayu try")
}
3.变量与常量
public static final int MONDAY = 1;
public static final int TUESDAY = 2;
public static void main(String[] args) {
int a = 1;
int b = 2;
int tmp = a;
a = b;
b = tmp;
}
go语言
const (
Monday = iota + 1
Tuesday
Wednesday
)
func TestChange(t *testing.T) {
a := 1
b := 2
a,b = b,a
t.Log(a,b)
}
4. 类型 type
func TestImplicit(t *testing.T) {
var a int= 1
var b int64
b = int64(a)
aPrt := &a
var str string
t.Log(str == "")
}
5.条件判断 condition
if (n % 2 == 0) {
return "Even";
} else if (n % 2 == 1) {
return "Odd";
} else {
return "Unkonw";
}
func SwitchCaseCondition(i int) (string, bool) {
switch {
case i % 2 == 0:
return "Even", true
case i % 2 == 1:
return "Odd", true
default:
return "Unkonw", false
}
}
func TestSwitch(t *testing.T) {
if str,flag := SwitchCaseCondition(5); flag {
t.Log(str)
}
}
6.循环语句 loop
func TestWhileLoop(t *testing.T) {
n := 0
for n <5 {
t.Log(n)
n++
}
for i:= 0; i < 5; i++ {
t.Log(i)
}
}
7.数组 array
int[] arr= new int[5];
arr[1] = 2;
int[] arr1 = new int[]{1,2,3,4};
for (int i : arr1) {
System.out.println(i);
}
var arr [3]int
arr[2] = 3
arr1 := [3] int{1,2,3}
arr2 := [...] int {1,2,3}
t.Log(arr1 == arr2)
for i, v := range arr1 {
t.Log(i,v)
}
for _, v := range arr1 {
t.Log(v)
}
arr3 := arr1[1,len(arr1)]
8. 切片 slice
go的引用类型之一,简单说 切片就是可变长的数组引用 , 数组通过len函数可以得到长度. 而切片除了真实的长度 ,还有 用cap函数 获得其容量, 我们会初始化一个容量, 如果数据真实长度超过这个容量.切片就会发生扩容,这时候会发生值copy. 这里联想一下java, 这不就是 ArrayList
ArrayList<Integer> list = new ArrayList<>(8);
list.add(1);
System.out.println(list.size());
var sli []int
sli = append(sli, 1)
t.Log(len(sli), cap(sli))
sli1 := [] int{1,2,3,4}
t.Log(len(sli1), cap(sli1))
sli2 := make([]int, 3, 5)
t.Log(sli2 ,len(sli2), cap(sli2))
sli3 := [] int{1,2,3,4}
fmt.Printf("%p\n",sli3)
sli3 = append(sli3, 5)
fmt.Printf("%p\n",sli3)
t.Log(sli3 ,len(sli3), cap(sli3))
9. go的map与java的
map亦是go的基本引用类型之一,go比较有特色的一点是,java里面很多基本的集合(链表,堆),在go里面都是作为自定义类型,"自举"实现的(即用 Go 语言编写程序来实现 Go 语言自身), 可以直接阅读go的源码包学习,里面甚至有测试的案例: java代码
HashMap<Integer, Integer> map = new HashMap<>();
map.put(1,1);
boolean b = map.containsKey(3);
map.remove(1);
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + entry.getValue());
}
map2 := map[int]int{1: 1, 2: 2, 3: 9}
map2[2] = 4
map2[4] = 16
t.Log(map2,len(map2))
map3 := make(map[int]int,10)
t.Log(map3,len(map3))
for k, v := range map2 {
t.Log(k,v)
}
mySet := map[int] bool{}
mySet[1] = true
n := 3
if mySet[n] {
t.Logf("%d is exiting", n)
}else {
t.Logf("%d is not exiting", n)
}
funcMap := map[int] func(op int) int{}
funcMap[1] = func(op int) int { return op}
funcMap[2] = func(op int) int { return op * op}
funcMap[3] = func(op int) int { return op * op * op}
t.Log(funcMap[1](2),funcMap[2](2),funcMap[3](2))
10. go通道 channel
chan基本的引用类型之一,Go 语言最有特色的数据类型,通道(channel)完全可以与 goroutine( 协程)并驾齐驱,共同代表 Go 语言独有的并发编程模式和编程哲学。 channe类型的值本身就是并发安全的,这也是 Go 语言自带的、唯一一个可以满足并发安全性的类型。 一个通道相当于一个先进先出(FIFO)的队列。也就是说,通道中的各个元素值都是严格地按照发送的顺序排列的,先被发送通道的元素值一定会先被接收。元素值的发送和接收都需要用到操作符**<-**。我们也可以叫它接送操作符。一个左尖括号紧接着一个减号形象地代表了元素值的传输方向。
func TestChan(t *testing.T) {
ch1 := make(chan int, 1)
ch1 <- 1
elem1 := <-ch1
fmt.Printf("The first element r: %v\n", elem1)
delay(ch1)
fmt.Println("put elem to channel")
ch1 <- 2
time.Sleep(time.Second * 1)
}
func delay(ch chan int) {
go func() {
fmt.Printf("receive elem from chanel %d", <-ch)
}()
}
###################结果打印#####################
The first element r: 1
put elem to channel
receive elem from chanel 2
11. 函数 function
在 Go 语言中,函数可是一等的(first-class)公民 ,函数类型也是一等的数据类型.相较于java , 函数基本的语法差异在于 可以多返回值. 更关键的是 天然实现 了java中的 函数式编程, 1.接受其他的函数作为参数传入 2.把其他的函数作为结果返回. java代码:
static Predicate<Integer> judgeEven = e -> e % 2 == 0;
static Consumer<Integer> printlnInt = System.out::println;
static Function<Integer, Integer> timeSpent(Function<Integer, Integer> inner) {
return (Function<Integer, Integer>) integer -> {
Instant first = Instant.now();
Integer ret = inner.apply(integer);
Instant second = Instant.now();
System.out.println("time spent:"+Duration.between(first, second).toMillis());
return ret;
};
}
public static int simpleFunc(int n) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
}
return n;
}
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6);
list.stream().filter(judgeEven).forEach(printlnInt);
Function<Integer, Integer> func = timeSpent(funcInterface::simpleFunc);
func.apply(10);
}
go代码:
type myFunc func(op int) int
func timeSpent(inner myFunc) myFunc {
return func(n int) int{
start := time.Now()
ret := inner(n)
fmt.Println("time spent:", time.Since(start).Seconds())
return ret
}
}
func slowFun(op int) int{
time.Sleep(time.Second*1)
return op
}
func Clear() {
fmt.Println("Clear resources.")
}
func TestFn(t *testing.T) {
tsSF := timeSpent(slowFun)
t.Log(tsSF(10))
defer Clear()
fmt.Println("Start")
panic("err")
}
###############结果展示##############
Start
Clear resources.
12. go面向对象 (封装)
Go 严格上不能算是一个面向对象的语言,但是通过go 基本可以模拟出类似java的 面向对象的效果.
type Employee struct {
Id string
Name string
Age int
}
func (e *Employee) String() string{
return fmt.Sprintf("ID:%s/Name:%s/Age:%d", e.Id, e.Name, e.Age)
}
func TestCreateEmployeeObj(t *testing.T) {
e := Employee{"0", "Bob", 20}
e1 := Employee{Name: "Mike", Age: 30}
e2 := &Employee{Name: "Mike", Age: 30}
e3 := new(Employee)
e3.Id = "2"
e3.Name = "Rose"
e3.Age = 22
t.Log(e1)
t.Log(e2)
t.Logf("e is %T", e)
t.Logf("e2 is %T",e2)
}
13. go面向对象 (继承)
java中子类继承父类属性与行为的,通过实现子类的方法,重写父类方法, 实现了LSP 里氏替换原则.往往作为策略模式在代码中体现.Go 无法做到继承. 只能做到复用 父类方法. 再Go的语言哲学里. 组合 > 继承. 其实这也是 java的spring 框架中 依赖注入的思想.
type Pet struct {
}
func (p *Pet)Speak() {
fmt.Print("...")
}
func (p *Pet)SpeakTo(host string) {
p.Speak()
fmt.Println("", host)
}
type Dog struct {
Pet
}
func (d *Dog) Speak() {
fmt.Print("wang!!")
}
func TestDog(t *testing.T) {
dog := new(Dog)
dog.Speak()
dog.SpeakTo("chao")
}
#############执行结果##############
wang!!
... chao
14. go面向对象 (多态)
type Code string
type Programmer interface {
WriterHelloWorld() Code
}
type GoProgrammer struct {
}
func (g *GoProgrammer)WriterHelloWorld() Code {
return "fmt.Println(\"Hello World\")"
}
type JavaProgrammer struct {
}
func (j *JavaProgrammer) WriterHelloWorld() Code {
return "system.out.println(\"Hello World\")"
}
func writeFirstProgram(p Programmer) {
fmt.Printf("%T, %v\n", p, p.WriterHelloWorld())
}
func TestPolymorphic(t *testing.T) {
p := &JavaProgrammer{}
writeFirstProgram(p)
goPro := new(GoProgrammer)
writeFirstProgram(goPro)
}
###################执行结果#####################
*polym__test.JavaProgrammer, system.out.println("Hello World")
*polym__test.GoProgrammer, fmt.Println("Hello World")
15. go并发 concurrent
Java的并发线程模型
线程- Thread 是比 进程 - Progress 更轻量的调度单位. 众所周知, 操作系统会把内存分为内核空间和用户空间, 内核空间的指令代码具备直接调度计算机底层资源的能力.用户空间的代码没有访问计算底层资源的能力,需要通过系统调用等方式切换为内核态来实现对计算机底层资源的申请和调度. 线程作为操作系统能够调度的最小单位,也分为用户线程和内核线程. 常用的java的线程模型属于一对一线程模型. 下图中反映到Java中, Progress是jvm虚拟机. LWP就是程序开启的Thread, 1:1 对应上内核线程,最后通过操作系统的线程调度器, 操作底层资源. 进程内每创建一个新的线程都会调用操作系统的线程库在内核创建一个新的内核线程对应,线程的管理和调度由操作系统负责,这将导致每次线程切换上下文时都会从用户态切换到内核态,会有不小的资源消耗。好处是多线程能够充分利用 CPU 的多核并行计算能力,因为每个线程可以独立被操作系统调度分配到 CPU 上执行指令,同时某个线程的阻塞并不会影响到进程内其他线程工作的执行。以上就是 java并发的特色. 我们用代码测试一下.
class PrintIntTask implements Runnable{
int num;
CountDownLatch cnt;
public PrintIntTask(int num, CountDownLatch cnt) {
this.num = num;
this.cnt = cnt;
}
@Override
public void run() {
TimeUnit.SECONDS.sleep(1);
System.out.println(num);
cnt.countDown();
}
}
public static void main(String[] args) throws InterruptedException {
Instant first = Instant.now();
int num = 10000;
CountDownLatch countDownLatch = new CountDownLatch(num);
for (int i = 0; i < num; i++) {
new Thread(new PrintIntTask(i,countDownLatch)).start();
}
countDownLatch.await();
Instant second = Instant.now();
System.out.println("spent time:" + Duration.between(first, second).toMillis());
}
java线程池的作用
############# 上一页程序 测试结果 ###################
n = 1000, spent time:1106 ms
n = 10000, spent time:5970 ms
虽然每个线程之间是独立的,但是处于就绪状态的线程,需要被cpu调度才能进入运行态**, 当线程休眠时,会进入等待状态,让出cpu资源,执行其他线程**. 而开辟的线程数上升后,竞争也愈发明显.同时开启大量的线程,对于系统的内存资源也会有很大的负担,在Jvm内存模型中,线程主要有
1.程序计数器
线程数超过CPU内核数量时,线程之间就要根据时间片轮询抢夺CPU时间资源,线程等待让出cpu资源时,它就需要记录正在执行字节码指令的地址.
2.虚拟机栈
每个方法执行时,会开辟一个栈帧,存储局部变量表,操作数栈等.调用方法时,生成一个栈帧,压入栈中,退出方法时,将栈帧弹出栈. 通过配置jvm参数 -Xss可以设置栈的大小,一般为1M.这就是递归方法过多后StackOverFlowError 的原因.
所以在java 编程中,需要开启大量的线程时一定要控制, 这就是常说的 线程池, 配置同时执行的核心线程数, 多余的任务在内部队列中排队执行.
int processors = Runtime.getRuntime().availableProcessors() * 2 ;
Execu torService fixedThreadPool = Executors.newFixedThreadPool(processors);
for (int i = 0; i < num; i++) {
fixedThreadPool.submit(new PrintIntTask(i));
}
Go的并发线程模型
Go在一对一线程模型的基础上,做了一些改进,提出了MPG模型 ,让线程之间也可以灵活的调度。 machine,一个 machine 对应一个内核线程,相当于内核线程在 Golang 进程中的映射。 processor,一个 prcessor 表示执行 Go 代码片段的所必需的上下文环境,理解为代码逻辑的处理器。 goroutine,是对 Golang 中代码片段的封装,其实是一种轻量级的用户线程,我们叫协程。 下左图,每个M都会和一个内核线程绑定,M与P 也是一对一关系,而P与G时一对多的关系.M在生命周期内跟内核线程是至死不渝的.而 M 与 P ,P 与 G.那是自由恋爱.
下右图,M与P的组合为G 提供了运行环境, 可执行的G的挂载在P下,等待调度与执行为GO。 这里不得不提 java的ForkJoinPool的工作窃取算法(感兴趣的2021.5.4 朋友圈有篇). 这里把G看做任务,当P空闲时,首先去全局的执行队列中获取一些G。如果没有则去"窃取"其他P最后的G,保证每一个P都不摸鱼。
func TestGroutine(t *testing.T) {
start := time.Now()
num := 10000
wg := sync.WaitGroup{}
wg.Add(num)
for i := 0; i < num; i++ {
go func(i int) {
time.Sleep(time.Second * 1)
fmt.Println(i)
wg.Done()
}(i)
}
wg.Wait()
t.Logf("spent time :%d",time.Since(start).Milliseconds())
}
#################测试结果######################
n = 1000, spent time:1005 ms
n = 10000, spent time:1262 ms
协程其实就是更轻量的线程,go将cpu的调度转为用户空间协程的调度,避免了内核态和用户态的切换导致的成本,通过队列的形式挂载协程, 这些协程共享一个P获得cpu资源.这样我们可以大量的开启协程,而不用太担心协程之间的竞争压力,同时G的栈空间只有2k,无压力创建出大量的实例,高效的利用了系统资源.
资源锁
单纯的并发还蛮简单,但是事情总不是一帆风顺. 并发在做任务时,不可避免的会出现共享资源,当多个线程同时操作同一资源时,就会出现并发安全问题。 我们使用资源锁,拿到锁的线程才可以操作资源, 拿不到锁的线程,等待锁的释放,再去抢锁。 java代码:
public class Count {
final ReentrantLock lock = new ReentrantLock();
CountDownLatch cdl;
int count = 0;
public Count(CountDownLatch cdl) { this.cdl = cdl;}
public void read(){
lock.lock();
try {
Thread.sleep(1);
System.out.println("查询现在计数为:"+count);
} finally {
lock.unlock();
cdl.countDown();
}
}
public void count(){
lock.lock();
try {
count++;
Thread.sleep(5);
System.out.println("计数后现在计数为:"+count);
} finally {
lock.unlock();
cdl.countDown();
}
}
}
int num = 1000;
CountDownLatch cdl = new CountDownLatch(num);
Instant start = Instant.now();
Count1 count = new Count1(cdl);
for (int i = 0; i < num; i++) {
if (i % 10 == 0){
new Thread(count::count).start();
} else {
new Thread(count::read).start();
}
}
cdl.await();
Instant end = Instant.now();
System.out.println("spent time:" + Duration.between(start, end).toMillis());
####################测试结果#####################
spent time : 1624
锁的优化
加锁之后,并发的性能明显下降,所以使用锁时一定要慎重, 从优化的来看有两个维度:
★其一,缩小锁的粒度. 在分布式事务中,如果多个系统事务保证强一致性,并发能力必然很差.,若把一个大事务,分割几个独立的小事务, 只要能保证最终一致性,就能大大提示并发能力.
★其二,读写锁,第一个案例里面,提到一个读写比的概念,这里联想一下数据库的隔离级别,读,写操作都互斥时,不就是性能最低的串行化SERIALIZABLE.把读和写 设计成两钟锁,存在写锁时,才互斥,只有读锁时,不互斥,再读写比较大场景下, 并发性能得到提升.
final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
CountDownLatch cdl;
int count = 0;
public Count1(CountDownLatch cdl) { this.cdl = cdl;}
public void read(){
lock.readLock().lock();
try {
Thread.sleep(1);
System.out.println("查询现在计数为:"+count);
} finally {
lock.readLock().unlock();
cdl.countDown();
}
}
public void count(){
lock.writeLock().lock();
try {
count++;
Thread.sleep(5);
System.out.println("计数后现在计数为:"+count);
} finally {
lock.writeLock().unlock();
cdl.countDown();
}
}
################测试结果#################
spent time : 699
go实现读写锁
num := 1000
wg := sync.WaitGroup{}
wg.Add(num)
mutex := sync.RWMutex{}
count := 0
for i := 0; i < num; i++ {
if i % 10 ==0 {
go func() {
defer func() {
mutex.Unlock()
}()
mutex.Lock()
count = count + 1
fmt.Printf("write num:%d\n",count)
wg.Done()
}()
}else {
go func() {
defer func() {
mutex.RUnlock()
}()
mutex.RLock()
fmt.Printf("read num:%d\n",count)
wg.Done()
}()
}
}
wg.Wait()
go的CSP并发通信模型
go在并发上的突破不光在线程模型上, 传统的并发通信模型,是以共享内存的方式,通过原子类和管程进行同步控制,比如上述的锁,有锁就有竞争,go 还提供了另一种方案, 支持协程之间以消息传递(Message-Passing)的方式通信, Go有句格言, “不要以共享内存方式通信,要以通道方式共享内存”.
通道即channel. 有点类似消息队列。.分非缓冲通道和 缓冲通道, 前者协程之间建立通信后,同步收发消息, 后者初始化通道容量, 异步收发消息,通道满了或空了,阻塞等待.。
num := 1000
wg := sync.WaitGroup{}
wg.Add(num)
start := time.Now()
ch := make(chan int, 1)
ch <- 0
for i := 0; i < num; i++ {
if i % 10 ==0 {
go func() {
count := <- ch
time.Sleep(time.Millisecond * 5)
fmt.Printf("write num:%d\n",count)
ch <- count+1
wg.Done()
}()
}else {
go func() {
count := <- ch
ch <- count
time.Sleep(time.Millisecond * 1)
fmt.Printf("read num:%d\n",count)
wg.Done()
}()
}
}
wg.Wait()
t.Logf("spent time :%d",time.Since(start).Milliseconds())
线程协同 (任务并行)
并发中的关键场景之一.我们调用两个耗时却又毫无关联的两个组件时,不妨试试任务并行,就是烧水煮茶, 时间统筹,同时执行. java代码:
Instant start = Instant.now();
FutureTask<String> futureTask = new FutureTask<String>(()->{
TimeUnit.SECONDS.sleep(1);
return "haha";
});
new Thread(futureTask).start();
TimeUnit.SECONDS.sleep(1);
String s = futureTask.get(2, TimeUnit.SECONDS);
Instant end = Instant.now();
System.out.println("spend time :" + Duration.between(start, end).toMillis() + "ms");
#####################执行结果######################
spend time :1050ms
go代码:
select {
case ret:= <- syncTask():
t.Log(ret)
case <-time.After(time.Second *3):
t.Error("timeout")
}
func syncTask() chan string{
ret := make(chan string,1)
go func() {
time.Sleep(time.Second * 1)
ret <- "dada"
}()
return ret
}
拓展:java的FutureTask之(任务只执行一次)
private ConcurrentHashMap<String,FutureTask<Connection>>connectionPool = new ConcurrentHashMap<String, FutureTask<Connection>>();
public Connection getConnection(String key) throws Exception{
FutureTask<Connection>connectionTask=connectionPool.get(key);
if(connectionTask!=null){
return connectionTask.get();
}
else{
FutureTask<Connection> newTask = new FutureTask<Connection>(this::createConnection);
connectionTask = connectionPool.putIfAbsent(key, newTask);
if(connectionTask==null){
connectionTask = newTask;
connectionTask.run();0
}
return connectionTask.get();
}
}
private Connection createConnection(){
return null;
}
参考阅读多篇博客: 原文链接:https://www.cnblogs.com/shenguanpu/archive/2013/05/05/3060616.html 原文链接:https://blog.csdn.net/weixin_45469930/article/details/122434441
|