A channel can be used as a pool of resources that allows us to request them on demand. In the following example, we will create a small application that will look up which addresses are valid in a network, using a third-party client from the github.com/tatsushid/go-fastping package.
The pool will have two methods, one for getting a new client and another to return the client back to the pool. The Get method will try to get an existing client from the channel or return a new one if this is not available. The Put method will try to put the client back in the channel, or discard it otherwise:
const wait = time.Millisecond * 250
type pingPool chan *fastping.Pinger
func (p pingPool) Get() *fastping.Pinger {
select {
case v := <-p:
return v
case <-time.After(wait):
return fastping.NewPinger()
}
}
func (p pingPool) Put(v *fastping.Pinger) {
select {
case p <- v:
case <-time.After(wait):
}
return
}
The client will need to specify which network needs to be scanned, so it requires a list of available networks starting with the net.Interfaces function, ranging through the interfaces and their addresses:
ifaces, err := net.Interfaces()
if err != nil {
return nil, err
}
for _, iface := range ifaces {
// ...
addrs, err := iface.Addrs()
// ...
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
// ...
if ip = ip.To4(); ip != nil {
result = append(result, ip)
}
}
}
We can accept a command-line argument to select between interfaces, and we can show a list of interfaces to the user to select when the argument is either not present or wrong:
if len(os.Args) != 2 {
help(ifaces)
}
i, err := strconv.Atoi(os.Args[1])
if err != nil {
log.Fatalln(err)
}
if i < 0 || i > len(ifaces) {
help(ifaces)
}
The help function is just a print of the interfaces IP:
func help(ifaces []net.IP) {
log.Println("please specify a valid network interface number")
for i, f := range ifaces {
mask, _ := f.DefaultMask().Size()
fmt.Printf("%d - %s/%v ", i, f, mask)
}
os.Exit(0)
}
The next step is obtain the range of IPs that need to be checked:
m := ifaces[i].DefaultMask()
ip := ifaces[i].Mask(m)
log.Printf("Lookup in %s", ip)
Now that we have the IP, we can create a function to obtain other IPs in the same network. IPs in Go are a byte slice, so we will replace the least significant bits in order to obtain the final address. Since the IP is a slice, its value will be overwritten by each operation (slices are pointers). We are going to update a copy of the original IP—because slices are pointers to the same array—in order to avoid overwrites:
func makeIP(ip net.IP, i int) net.IP {
addr := make(net.IP, len(ip))
copy(addr, ip)
b := new(big.Int)
b.SetInt64(int64(i))
v := b.Bytes()
copy(addr[len(addr)-len(v):], v)
return addr
}
Then, we will need one channel for results and another for keeping a track of the goroutines; and for each IP, we need to check whether we can launch a goroutine for each address. We will use a pool of 10 clients and inside each goroutine—we will ask for each client, then return them to the pool. All valid IPs will be sent through the result channel:
done := make(chan struct{})
address := make(chan net.IP)
ones, bits := m.Size()
pool := make(pingPool, 10)
for i := 0; i < 1<<(uint(bits-ones)); i++ {
go func(i int) {
p := pool.Get()
defer func() {
pool.Put(p)
done <- struct{}{}
}()
p.AddIPAddr(&net.IPAddr{IP: makeIP(ip, i)})
p.OnRecv = func(a *net.IPAddr, _ time.Duration) { address <- a.IP }
p.Run()
}(i)
}
Each time a routine finishes, we send a value in the done channel so we can keep count of the done signals received before exiting the application. This will be the result loop:
i = 0
for {
select {
case ip := <-address:
log.Printf("Found %s", ip)
case <-done:
if i >= bits-ones {
return
}
i++
}
}
The loop will continue until the count from the channel reaches the number of goroutines. This concludes the more convoluted examples of the usage of channels and goroutines together.