TKzizo / GO-lab

Messing arround with go modules

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

GO-lab

Variales

  • variable must always be used else u get an error.

  • variable declaration:

    1. implicit type declaration:
     var {name} {type} = [value] 
    1. let the compiler decide the type:
    {name} := {value}

  • Can't redeclare a varible but can shadow it be declaring inside another block.

  • variable scoope:
    1. first lower case variables:

      are only visible in their package .

    2. first upper case variables:

      are visible to other packages.


  • Converting variables:

    var i int = 42 
    var k float32
    
    k = float32(i)

  • Converting to utf-8:
     k := 42 
     sign = string(k)
    
     fmt.Print(sign)
    output:
    * //utf-8 value of 42

Packages

  • fmt contains all main functions

  • strconv string convertion package :
    • Itoa converts integer to string

  • reflect to use tags

  • math math functions:
    • Abs() absolute value.
    • Sqrt() square root.
    • Pow() power.

  • log show loggings:
    • Println() formated error
    • Fatal send -1 return

  • Net/http net tools:
    • Get() return response and err

  • io/ioutil input/output utilities:
    • ReadAll read bytes

  • sync Go routines:
    • WaitGroup{}
    • RWMutex{}
    • add add go routine to stack to wait for before terminating the execution.
    • Wait() wait for go routines to finish.
    • Done() send signal that the go routine finished.

  • time :
    • Sleep( )
    • time.Millesecond

  • runtime:
    • GOMAXPROCS() can set how many theads program can run on, if given negative value it returns how many threads are available.

Primitive types

  • Boolean
     var right_answer bool = true
     var wrong_answer bool = false    

  • Numerical
    • Integers
       int    //default min 32 can go up 64
       int8   // -128 _ 127
       int16  // -32 768 _ 32 767
       int32  // -2 147 483 648 _ 2 147 483 647
       int64  // -9 223 372 036 854 775 808 _ 9 223 372 036 854 775 807
    • Unsigned integers
       uint8  //0 255
       uint16
       uint32
    • Floating numbers
      var f1 float32 
      var f2 float64  
    • *complex numbers
      var i1 complex64 = 12 + 3i //both are float32
      var i2 complex128 = 5 + 2i //both are float64
      real(i1) // real part of complex
      imag(i2) // imaginary part of complex
      
      var i3 complex128 = complex(real(i1),real(i2)) //convert float to conplex

  • TEXT
    • String
      • string are immutable.
        s := "helllo world"
        b := s[3] // is a byte encoding of letter "l" Unsigned integer 
        c := string(s[3]) // is letter l  
      
        b := []byte(s) // [values of each char in utf-8]
    • Rune
      ///used when working with utf-32    
       var c rune = 'a' // c is int32 value 

Operations

  • Numerical operations
     +   //addition
     -   //substraction
     *   //multiplication
     /   //division
     %   //modulo     
  • bitwise operations
    &   // logic AND
    |   // logic OR
    ^   // XOR where either is set to 1 
    &^  // NOR where neither is set to 1
    value >> num // bit shift right num-times
    value << num // bit shift left num-time

Constants

  • Constant declaration
        const myConst int = 42 //if it starts with upper case i will be exported as gloabl const

  • Constant can be Shadowed just like variables.

  • the type can be determined at comopilation:
        const myConst = 44 
        var num int16 = 20
    
        var num2 = num + myConst // now myConst will be int16 as will be num

  • Enumerating constants:
        //declare a cosntant bloc 
        const (
            const1 = iota //iota is function used to keep track of const created
            const2 = iota 
            const3 // since we are in const bloc it uses same pattern and assign iota()
        )
    
        //const1 has value 0
        //cosnt2 has value 1
        //const3 has value 2
  • iota usage:
        //enumeration of values
        const (
            _     = iota //if we don't need 0
            user1 = iota
            user2
        )
        // since const must be defined at compilation and the power func is package
        // we can use bit shift with iota to replace that
        const (
            _   = iota 
            KB  = 1 << (10 * iota) 
            MB // this will be 1 << (10 * 2)
            GB
            TB
        )
        const (
            isAdmin = 1 << iota //00000001
            isManager //00000010
            isSalesman //00000100
    
            canUseMetro //00001000
            canUseCar //00010000
            canUseTrain //00100000
        )
        var employee byte = isManager | canUseTrain //00100010  

Data Structures

Arrays and slices:

  • Array declaration:
       /// implicit
        name := [size]type {values, .. , ..}  
        /// same as:
        name := [...]type {values , .. , ..}
       /// explicit 
        var name [size]type  // empty array
    
        //Examples 
        name[2] = "hello"
    
        var length int = len(name) // length of the array
    
        var capaciy int = cap(name) // capacity of array 
    
        // multidiementional array 
        var 2d_table [3][3]int = [3][3]int {
                                               [3]int {3,3,3}, 
                                               [3]int {2,2,2},
                                               [3]int {1,1,1}
                                           }
  • arrays are passed as data:
      a := [...]int {1,2,3}
      b := a // now b has a copy of array a so if we change b nothing happens to a
      c := &a // c points to the same table a  
  • Slice declaration
     a := []int {1,3,4,5.10,22}
    
     b = a // point to same slice a 
    
     length := len(b) // legth of slice b
     capacityy := cap(b) // capacity of slcie b
     
     c := a[2:] // elemtst from 3rd index to last
     d := a[:6] // elemnts before 6th index
     e := a[1:5] //figure it out xDDD
    
    // create slice from array
     arr := [3]int {3,2,4}
     slic := arr[:]
    
     // add element to slice 
    a = append(a, values , ..)
    
    // concatinate slice to slace 
    a = append(a , []b{1,2,3}...) // the ... is called spreading 
  • Make function
       a := make( []string , length )
       a := make( []int , length , capacity)

Maps:

  • Note about Maps:
    1. map is pair of key string.

    2. the key can be any primitive type as well as arrays but not slice.

    3. the order of the elements in maps isn't guaranteed.

    4. Maps are passed by reference.

  • Maps declaration:
       a_map := map[key]type_of_values {
           "key1" : value1,
           "key2" : value2
       }
    
       //using the make function
       2nd_map := make(map[string]int) //gives an empty map
  • Maps manipulation:
       // to get an element from the map we  specify the key:
       a_map := map[key]type_of_values {
           "key1" : value1,
           "key2" : value2
       }
       var1 := a_map["key1"]
    
       // to check if the key actualy exits we can also do this:
       _ , exist? := a_map["key"]
       fmt.Printf(exist?) // return true if key exist else false
                           // _ will contain always 0 if the key doesn't exist 
    
       // to delete an element from the map we can use "delete " function:
       delete(name_of_map , the_key)
    
    
       // to add an element to the map:
       a_map["new key"] = new_value

Structs:

  • Notes about Structs:

    1. structs follow same capital rule as variables which means if struct name is catpital letter then it will be available to other packages however if its elemetns are lower case then the struct is visible but they are not.

    2. structs are passed by value.

    3. to pass struct pas reference we use & same as arrays.

  • structs declaration:

    // first we define the struct 
    type Name_of_str struct {
       first_element type1
       second_elemetn type2
    }
           //now
           var1 := Name_of_str {
               first_element : value,
               second_element : value2
           }  
       
       // Anomymous struct:
           str := struct{name string}{name : "kat"}
       }
  • Structs manipulation:

        // access element of struct:
        struct_name.name_element
    • similar to inheretance in other languages in Go we have composition:
           type Shape struct{
               surface float32
           }
           type triangle struct{
               Shape
               _type string
           }
           // now the type Shape is embedded in triangle and we can access Surface same as any other elment of triangle
    
           //now to initialize a var we have two methods
           //1st method is:
           b := triangle{}
    
               b.surface = 12
               b._type = "straight"
           //2nd method 
           b := triangle{
               Shape : Shape{surface : 64},
               _type : "straight"
           }
           
    • Tags we use them to provide a text relative to a certain value, we need reflect package :
           type Animal struct {
               origin string 
               Name string `required Max "25"` 
           } 
    
           //to use the tag
               t := reflect.TypeOf(Animal{})
               field , _ := t.FieldByName("Name")
           // field == required Max: "25"

Conditionals:

if statements:

  • must always have "{}".

  • if we are testing multiple statement using (OR " || " ) then the compliler stops at the first true statement , this is called short circuting.

  • format: "else if".


  • useful usage:
        maps := map[string]int {
            "key1" : 123,
            "key2" : 456
        }
    
    if value,ok := maps["key3"] /* this is called initialiser*/ ; ok {
        fmt.Print(value) //value is only available inside the scope
    }
    
    //return booleen test 
        var1 := 50
        var2 := 44
    
        fmt.Println(var1 > var2 , var1 == var2) 
        //output 
            true false

Switch statement:

  • the break is implicit so no need to write it ,but we can use it in some situations.
  • so if we want two cases to execute we use fallthrough.

 switch value /* can be initialiser same as if statement*/ {
     case value1:
         //do this
     case value2: 
         //do that
     case value3, value4:
         //do somthing
     default:
         //do shit
 }

  • Type switch
  var i interface{} = 1 // or "hello" or [3]int{} or any primitive type
  
  switch i.(type) {

      case int:
          //do this
              if bla_bla { break} //when we don't want the rest of the bloc to execute
          //else do another thing
      case string:
          //do that:
      default:
          //something
  }

Loops: "For"

  • In Go there is only the For loop.

  • simple loops

       //single index
       for i := 0; i < 5 ; i++ {
           //do somthing
       }
    
       //multiple indexes
       for i , j := 0,0 ; i < 5 ; i , j = i+1 , j+1{
           //do something crazy
       }
    
       //while looop
        var i int = 0
        for i < 5 {
            //do somethig
            i++
        }
    
       //do_while loop
       for {
           //do something
           if bla_bla {break}
       }
  • Lables:

       // if we want to break off nested loops
       i := 0
       Loop: 
           for i < 10 {
               for j:=0;j<5;j++{
                   //do bla bla
                   if bla_bla {break Loop}
               }
               i++
           }
  • looping collections

       //collection can be of any type array, slice, maps,String
       for key, value := range collection {
           fmt.Println(key , value)
       }     
       
       //if we just want the key like in maps we can
       for key := range maps{
           //print key
       }

Control Flow

Defer:

  • multiples defer are executed in LIFO.

   //suppose we made an http request and we need to work with the response before closing the response,
   //inorder not to forget that we can use "defer" to close it at any point but it will be closed just before the current function return.
    import (
        "fmt"
        "log"
        "net/http"
        "io/ioutil"
    )
    func main(){
       res , err := http.Get("http://www.google.com/robots.txt")

       if err != nil {
           log.Fatal(err) //couldn't get the document
       }

       defer res.Body.Close() //this should be last thing to do but since we putted defer it will be last to execute

       robots , err := ioutil.ReadAll(res.Body) //robots contains bytes
       if err != nil {
           log.Fatal(err) //couldn't read
       }
       fmt.Printf("%s",robots) // %v prints bytes

    }
 // here is notice:
  a := "string"
  defer fmt.Print(a)    
  a = "hello"

  //output: 
   "string"

Panic:

  • panic is to be used only in must situation and not just anywhere.

  • in Go we don't have exeptions but we have errors and panic throw an error.

  • all defer executes before terminating the function that panicked.

   // we can stop the execution of program by panic at some erreur 

   fmt.Println("hello beautful world")
   panic("shit we are in the underẃorld")
   fmt.Println("i love this place") // this will not printed

   //output:
       hello beautful world
       panic: shit we are in the underẃorld

Recover:

  • not to stop all the program from executing if a function panics we use recover on that function so the rest of the program can execute and we can get info about the err.
   func main() {
       fmt.Println("hello there !!!")
       panicker()
       fmt.Println("goodbye my darkest old friend")
   }

   func panicker() {
       fmt.Println("it's about to go down")
       
       defer func() {
           if err := recover(); err != nil {
               log.Println("Error:",err)
           }
       }() // anonymous function

       panic("this is hell")
       fmt.("i'm sure this not gonna be written")
   }

   //output:
   """
       hello there !!!
       it s about to go down
       yyyy/mm/dd hh:mm:ss Error: this is hell
       goodbye my darkest old friend
   """
   // in case we can't handle the error we just panic(err) after the log.

Pointers:

  • we can't do arithmetic with pointers.

       //declaring pointers:
       a := 42
       var point *int 
       point = &a // hexadecimal number

       // structs and pointers:
       type doc struct {
           name int
       }
       
       var point *doc // <nil> pointer to structer 
       //or 
       point := new(doc) // &{0} pointer to empty structure
       //or 
       point := &doc{}

       (*point).name = 42
       //same as
       point.name = 42

Functions:

  • Notes:

    1. functions are case sensitive same as variables.
  • declaring functions:

       func functions_name(parameteres,..) return_type {
           //body 
       }
    
       //example:
       func hello(msg1,msg2 string,) {
           fmt.Println(msg1,msg2)
       }
    
       //working with poiters:
       func hello(name,msg *string) {
           // do stuff
           //...
           //...
       }
       //calling the functions:
        hello (&variable,&variable2)
  • Go sugar functions xDD aka variadic parameters

     //we can send multiple data to a function without knowing how many until runtime
     //there is only one condition it has to be the last parameter:
    
     func koko(msg string, values ...int) int {
    
         fmt.Println(msg)
         result := 0
         for _ , val := range(values){
             result += val
         }
       return result
     }
     //calling the function
       sum := koko("hello world",1,2,3,5,9,8)
    
     //another way to declare the function is like this
     func koko(msg string, values ...int) (result int) { //result is instanced here
         
         for _,val := range values {
             result += val
         }
    
         return  // we dont have to precise because we already did in the begining
     }  
  • Returning pointers to local variables:

     // this may seem weid espicialy if you know how the local stack and the heap works
     // but in Go we have the abiliy to reference a local variable:
     func main() {
         variable := nameless() // this is pointer to varible name in func nameless()
         fmt.Println(*variable)
     }      
    
     func nameless() *string {
       name := "My name is Jeff"
       return &name
     }
  • Multiple parameters and errors

       func main() {
    
           d , err := divide(6.2 , 6.2)
           if err != nil {
               fmt.Println(err)
               return // to end the main function
           }
    
           fmt.Println(d)
       }
    
       func divide(val1 float64, val2 float64) (float64,error) {
           if b == 0 {
               return 0.0, fmt.Errorf("cannot divide by 0")
           }
           return a/b , nil
       }
  • anonymous function:

       // we can create nameless functions and execute them instantly by adding () after {}
    
       func() {
           //do stuff
           //do stuff
       }()     
    
       // we can also pass function as variables 
       var f func(float64,int ) (int , error)
    
       f = func(a ,b float64) (int , error) {
           //do stuff
           //do stuff
       }
    
       val , err := f(6.0, 5.2)
  • Methods in Go

       func main() {
           g := DOC {
               name : "john",
               spec: "surgery"
           }
    
           g.operate()
       } 
       
       type DOC struct {
           name string
           spec string
       }
    
       func (k DOC) operate() {  
           // know that the struct DOC is passed by value
           // we can pass it by refence by addig *, by doing that we can make changes to k
    
           fmt.Print("Dr.",k.name," is doing a ",k.spec)
       }
       

Interfaces:

  • Notes :

    1. unlike struct, interfaces don't store data but behavior instead.
    2. In Go interfaces are implemented implicitly by creating a method that replicate their behavior.
    3. by convention the name of an interface should end with a "er"
    4. declarations inside interfaces must start with capitas to be used outside of package.
  • declaring an Interface :

        type name(er) interface {
            // we declare a method without defining it:
            Create([]byte) (int)
        }
    
        type anything struct {
            // elemetns
        }
    
        func (obj (*)anything ) Create([]byte) int {
            // we must define
        }
  • Some use cases:

    • suppose we want to create a new type of int that has a certain method:

          func main() {
              it := Inter(0) // the brackets because it empty int 
              var ic IntCounter = &it
              for i := 0; i < 10 ; i++ {
                  fmt.Println( ic.incrementer() )
              }
          }
          type IntCounter interface {
              incrementer() int
          }
      
          type Inter int
      
          func (obj *Inter) incrementer() int {
                  *obj++
                  return int(*obj)
          }
    • composing multiple interfaces:

          import "bytes" // for the sac of the example 
      
          func main() {
              var bwc WriterCloser = NewBufferedWriterCloser() 
      
              bwc.Write([]byte("hello world from my first real usage interface"))
              // so this will print 8 characters by line 
              bwc.Close()
              // if there is a less than 8 characters it will print them
          }
      
          type Writer interface {
              Write([]byte) (int,error)
          }
          type Closer interface {
              Close() error
          }
      
          type WriterCloser interface {
              Writer
              Closer
          }
      
          type BufferedWriterCloser struct {
              buffer *byte.Buffer
          } 
      
          //now BufferedWriterCloser must implement both Write and Close methods
          // the writer method
          func (obj *BufferdWriterCloser) Write(data []byte) (int,error) {
              n,err := obj.buffer.Write(data) // write date to buffer
              if err != nil {
                  return 0,err
              }
              
              v := make([]byte , 8) // makes slice with array capacity of 8 bytes 
              for obj.buffer.len() > 8 {
      
                  _ ,err := obj.buffer.Read(v) // read from buffer to v 
                  if err != nil {
                      return 0,err
                  }
      
                  _, err := fmt.Println(string(v))
                  if err != nil {
                      return 0,err
                  }
              }
          return n,nil
          }
          
          // the Closer method
          func (obj *BufferredWriterClose) Close() err {
              for obj.buffer.len() > 0 {
                  data := obj.buffer.Next(8) // read 8 bytes or until end of buffer and flush it
                  _, err := fmt.Println(string(data)) 
                  if err != nil {
                      return 0, err
                  }
              }
              return nil
          }
      
          // this function is only used to initialise the buffer inside the struct
          func NewBufferedWriterCloser() *BufferedWriterCloser {
              return &BufferedWriterclosed {
                  buffer : bytes.NewBuffer([]byte{})
              }
          }
    • now in order to access directly to the struct attributs we need to typecat our interface type variable into the type of the instance and to do that:

          // we will continue use the same beforehand defined struct and interface
          func main() {
              var wc WriterCloser = NewBufferedWriterCloser()
              wc.Write([]byte("hello there again"))
              wc.close()
              
              // since NewBufferredWriterCloser() return a *BufferedWriterCloser we should type cast our wc variable to that, and we can assign it to another vaiable 
      
              bwc := wc.(*BufferedWriterCloser) 
              // Note:
              bwc := wc.(BufferedWriterCloser) //this will raise an error or panic we will know why later on
      
              // in order to be sure that there is no error we use same syntax as maps
              bwc,ok := wc.(*BufferedWriterCloser)
              if ok {
                  fmt.Println(bwc)
              }else {
                  fmt.Println("conversion failed")
              }
          }   
    • empty interfaces:

          // in case we need to work with multiple non-compatible interfaces, the empty interface comes in handy because it allows us to use them all, by typecasting without getting any error like we did in the example before, because it doesn't require us to implement any methor:
      
          var x interface{}/*this is how we declare it*/ = NewBufferedWriterCloser()
      
          if wc, ok := x.(WriterCloser); ok {
              wc.Write([]byte("hello for the third time"))
              wc.close()
          }
          // and now x is WriterClosr so is wc
          // and if we do this:
          y, ok := x.(io.Reader) // just an interface from io package
          if ok {
              fmt.Println(y)
          }else {
              fmt.Println("conversion failed)
          }
    • since the start we have been using * in our method definition instead of the type directly, and we said in example above that not using * will raise an error:

      • that's because when we implemented the methods Write and Close:
        func (obj *BufferedWriterClose) Close() error
         
        func (obj *BufferdWriterCloser) Write(data []byte) (int,error)
        we had pointer receiver (*Buf..,) and not value receiver (Buff...); we did that because we needed to access internel data the byte[] slice and to avoid creating a copy by passing it by value; now if implemented them using value reciver we still be able to access them using pointer.
            var x WriterReceiver = &BuffferedWriterReader{}
    • BEST PRACTICES:

      1. use small interfaces, that will avoid lot of conflict and debug,since strongest interfaces are always the smallest: io.Reader, io.Writer, interface.

      2. Don't export interfaces for types that will be consumed, unlike db package, by that u can is without implementing every method.

      3. Do export interfaces for types that will be used by the package, something you can't do in java because of its explicit implementation of interfaces.

      4. design function and method to accept interfaces whenever possible, when you don't need acces to internel attributs.

Go-Routines:

  • GOROUTINES is not paralellism.

  • GOROUTINES are high level abstraction of thread which makes manipulating them more easy and with less risk.

  • the main function is a goroutine, the main one.

    import ( "fmt"
           "time"
           "sync"
    )
    
    var wg = sync.WaitGroup{}
    
    func main() {
    
        var msg = "Routines are awesome"
        wg.Add(1)
    
        // the reason i used anonymous function is to access msg and not copy it as func paarmeter
        go func(){
            fmt.Println(msg)
        } ()
        // output: routines sucks
    
        msg = "routines sucks"
        //we can use time.sleep(100 * Millisecond) to replace the following but is really bad practine
        wg.Wait()
        
    }
    • Now to avoid the problem above or better say detect it we can use "-race" at compilation which signal multiple access to a variable at the same time.
  • Now let's talk about sync.RWMutex{} :

    when we have multiple go routine which want to access let say a variable at same time, some uncertain behavior may happen so to control that we limit the access one at time, we can have multiple reads, but only one write that happen after all reads are finished:

        import ("fmt"
                "sync"
        )
        var counter = 0
        var m = sync.RWMutex{} 
        var wg = sync.WaitGroup{}
    
        func main() {
            
            for i := 0; i < 10; i++ {
                wg.add(2)
                go increment()
                go printer()
            }
            wg.Wait()
    
            // this code may seem legit but what will happen is that once the Rlock() takes control it wont let the Lock of incrementer work Hence will get same value after first read lock
        }
        func increment(){
            m.Lock()
            counter++
            m.Unlock()
            wg.Done()
        } 
        func printer(){
            m.RLock()
            fmt.Printf("value of Counter %v\n",counter)
            m.RUnlock()
            wg.Done()
        }
  • BEST PRACTICES:

    • avoid making goroutines in libraries.

Channels:

  • Making channels:

    1. unlike other types we need to use "make" to create a channel
       // unlike other types we need to use "make" to create a channel.
       
       // make(chan [type_to_pass])
        ch := make(chan int)
    1. Example:
      import ("fmt"
                "sync"
                )
    
      wg := sync.WaitGroup{}
    
      func main() {
          ch := make(chan string)
          wg.add(2)
    
          func () {
              i := <- ch // receive date from channel
              fmt.Println(i)
              wg.Done()
          }()
    
          func () {
              i := 42 
              ch <- i //send a copy of data to channel 
              i++ 
              wg.Done()
          }
      }
    1. Control Flow:
      • sometimes or preferably we need to dedicate a channel to either read or write, inorder to have a more predictible behaviour:
        // let use the previous example but lets make the go routine unidirectionel
        wg := sync.WaitGroup{}
        
        func main(){
            ch := make(chan int)
            wg.add(2)
            // let make this routine receiving from channel
            func (ch <- chan int ) {
                i := <- ch
                fmt.Println(i)
                wg.done
            }(ch) //we must specify channel even if it is bidirectionnel, the go compiler can understand this kind of behaviour with channels
        
            //let make this routine  sending to channel
            func (ch chan <- int ) {
                i := 55 
                ch <- i
                wg.Done()
            }(ch)
        
        }
    2. Buffering Data:
      • sometime processing data take longer than receiving them, so not to lose data we make the channel buffered, the way to do that is:
          ch := make(chan int, [number of elements in buffer we want])
    3. Range through data:
      • now in order to parse through that buffer we can use the range funtion:
            for i := range ch {
                fmt.println(i)
            }
        
            func (ch chan <- int ) {
                i := 42 
                ch <- i
                i++
                ch <- i
                wg.Done()
            }
            // but this will rise an error sense we will get to empty channel
        
            // we can remedy this by adding to the sending routine:
            close(ch)
            //but this will definitely close the channel and we can't send data
        
        
            // a better way to do this is by cheking with a for loop:
            // i should note that channels send multiple data like maps so we can use same sytanx to loop them
            for {
                if i,ok := <- ch; ok {
                    fmt.Println(i)
                }else {
                    break
                }
            } 
    4. Select statement:
      • if we have many channels that we are monitoring and we want to know when to close them if they are done, so that they don't get shut forcebly and we don't have memory leak.
          ch := make(chan string, 50)
      
          func main() {
              go printer()
      
              ch <- "hell"
              ch <- "bye"
      
              time.sleep(500*Millisecond)
          }
          
          func printer() {
              for i := range ch {
                  fmt.println(i)
              }
          }
      
          //this will forcebly shut printer() at the end of main function, maybe leading to memory leakage in bigger programs
      
          // Non-conventional method:
          defer func() {
              close(ch)
          }()
      
          //convetional method:
          // we use an empty struct channle
          cch := make(chan struct{})
      
          func printer() {
              for {
                  select { // bloc statment until either channels receive a msg
                      case entry := <_ ch:
                          fmt.Println(i)
      
                      case <- cch:
                          break
                      /* optional if none receive anything
                      default: 
                          do something
                      */
                  }
              }
          }

Reference

About

Messing arround with go modules


Languages

Language:Go 100.0%